diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ed08f6f..96fa1bd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,6 +7,11 @@ jobs: name: Check runs-on: ubuntu-latest steps: + + - name: Install desktop dependencies + run: | + sudo apt install -y libglib2.0-dev libatk1.0-dev libgtk-3-dev + - name: Checkout sources uses: actions/checkout@v3 @@ -79,6 +84,10 @@ jobs: swap-storage: true + - name: Install desktop dependencies + run: | + sudo apt install -y libglib2.0-dev libatk1.0-dev libgtk-3-dev + - name: Checkout sources uses: actions/checkout@v3 @@ -124,6 +133,11 @@ jobs: name: Lints runs-on: ubuntu-latest steps: + + - name: Install desktop dependencies + run: | + sudo apt install -y libglib2.0-dev libatk1.0-dev libgtk-3-dev + - name: Checkout sources uses: actions/checkout@v3 @@ -152,11 +166,10 @@ jobs: run: cargo fmt --all -- --check - name: Run cargo clippy - run: cargo clippy --workspace --tests --benches -- -D warnings - + run: cargo clippy --workspace --all-targets --tests --benches -- -D warnings - check-wasm32: - name: Check Wasm32 + wasm32: + name: Wasm32 runs-on: ubuntu-latest steps: - name: Checkout sources @@ -189,72 +202,18 @@ jobs: sudo apt-get update sudo apt install -y gcc-multilib - - name: Install stable toolchain - if: steps.install_llvm.outcome == 'success' && steps.install_llvm.conclusion == 'success' - uses: dtolnay/rust-toolchain@stable - - - name: Add wasm32 target - run: rustup target add wasm32-unknown-unknown - - - name: Cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - # append here any new wasm32 crate not already covered by the existing checks - - - name: Run cargo check for wasm32 target - run: cargo clippy --target wasm32-unknown-unknown - - build-wasm32: - name: Build Wasm32 - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install llvm - id: install_llvm - continue-on-error: true - run: | - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - sudo apt-get install -y clang-15 lldb-15 lld-15 clangd-15 clang-tidy-15 clang-format-15 clang-tools-15 llvm-15-dev lld-15 lldb-15 llvm-15-tools libomp-15-dev libc++-15-dev libc++abi-15-dev libclang-common-15-dev libclang-15-dev libclang-cpp15-dev libunwind-15-dev - # Make Clang 15 default - sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/lib/llvm-15/bin/clang++ 100 - sudo update-alternatives --install /usr/bin/clang clang /usr/lib/llvm-15/bin/clang 100 - sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/lib/llvm-15/bin/clang-format 100 - sudo update-alternatives --install /usr/bin/clang-tidy clang-tidy /usr/lib/llvm-15/bin/clang-tidy 100 - sudo update-alternatives --install /usr/bin/run-clang-tidy run-clang-tidy /usr/lib/llvm-15/bin/run-clang-tidy 100 - # Alias cc to clang - sudo update-alternatives --install /usr/bin/cc cc /usr/lib/llvm-15/bin/clang 0 - sudo update-alternatives --install /usr/bin/c++ c++ /usr/lib/llvm-15/bin/clang++ 0 - - - name: Install gcc-multilib - # gcc-multilib allows clang to find gnu libraries properly + - name: Install desktop dependencies run: | - sudo apt-get update - sudo apt install -y gcc-multilib + sudo apt install -y libglib2.0-dev libatk1.0-dev libgtk-3-dev - name: Install stable toolchain if: steps.install_llvm.outcome == 'success' && steps.install_llvm.conclusion == 'success' uses: dtolnay/rust-toolchain@stable - - name: Cargo install wasm-pack - run: cargo install wasm-pack - - - name: Cargo install trunk - run: cargo install trunk + - name: Cargo install wasm-pack and trunk + run: | + cargo install wasm-pack + cargo install trunk - name: Add wasm32 target run: rustup target add wasm32-unknown-unknown @@ -270,24 +229,37 @@ jobs: target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Build Chrome Browser Extension - run: rm -rf ./dist-chrome - run: mkdir ./dist-chrome - run: mkdir ./dist-chrome/icons + # - name: Run cargo check for wasm32 target + # run: cargo clippy --target wasm32-unknown-unknown - run: cp -r ./resources/icons ./dist-chrome/ - run: cp ./resources/style.css ./dist-chrome/ - run: cp ./extensions/chrome/manifest.json ./dist-chrome/ - run: cp ./extensions/chrome/popup.html ./dist-chrome/ - run: cp ./extensions/chrome/popup.js ./dist-chrome/ - run: cp ./extensions/chrome/background.js ./dist-chrome/ + - name: Build Web App + run: | + trunk build --release - run: wasm-pack build --target web --out-name kaspa-ng --out-dir ../../dist-chrome ./extensions/chrome + - name: Build Chrome Browser Extension + run: | + rm -rf ./dist-chrome + mkdir ./dist-chrome + mkdir ./dist-chrome/icons + # copy resources + cp -r ./resources/icons ./dist-chrome/ + cp ./resources/style.css ./dist-chrome/ + cp ./extensions/chrome/manifest.json ./dist-chrome/ + cp ./extensions/chrome/popup.html ./dist-chrome/ + cp ./extensions/chrome/popup.js ./dist-chrome/ + cp ./extensions/chrome/background.js ./dist-chrome/ + # build + wasm-pack build --target web --out-name kaspa-ng --out-dir ../../dist-chrome ./extensions/chrome build-release: - name: Build Ubuntu Release + name: Ubuntu Release runs-on: ubuntu-latest steps: + + - name: Install desktop dependencies + run: | + sudo apt install -y libglib2.0-dev libatk1.0-dev libgtk-3-dev + - name: Checkout sources uses: actions/checkout@v3 diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index b010bee..b85c0bd 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -18,6 +18,10 @@ jobs: if: runner.os == 'Windows' run: git config --global core.autocrlf false + - name: Install desktop dependencies + run: | + sudo apt install -y libglib2.0-dev libatk1.0-dev libgtk-3-dev librust-atk-dev + - name: Checkout sources uses: actions/checkout@v3 diff --git a/README.md b/README.md index dbc73f4..1b8bb18 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,18 @@ With Kaspa-ng you can run a full node and a wallet on your desktop as well as co To build this project, you need to be able to build Rusty Kaspa. If you have not built Rusty Kaspa before, please follow the Rusty Kaspa [build instructions](https://github.com/kaspanet/rusty-kaspa/blob/master/README.md). +In addition, on linux, you need to perform the following installs: + +#### Ubuntu/Debian: +```bash +sudo apt-get install libglib2.0-dev libatk1.0-dev libgtk-3-dev librust-atk-dev +``` + +#### Fedora: +```bash +sudo dnf install glib2-devel atk-devel gtk3-devel +``` + Once you have Rusty Kaspa built, you will be able to build and run this project as follows: #### Running as Native App diff --git a/core/Cargo.toml b/core/Cargo.toml index 1bfdf62..c7c5403 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -70,7 +70,7 @@ egui_plot.workspace = true egui_extras.workspace = true chrono.workspace = true eframe = { workspace = true, default-features = false, features = [ - # "accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies. + "accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies. "default_fonts", # Embed the default egui fonts. # "wgpu", # Use the glow rendering backend. Alternative: "wgpu". "glow", # Use the glow rendering backend. Alternative: "wgpu". diff --git a/core/src/app.rs b/core/src/app.rs index c526ca5..05bc5dc 100644 --- a/core/src/app.rs +++ b/core/src/app.rs @@ -220,14 +220,25 @@ cfg_if! { let runtime: Arc>> = Arc::new(Mutex::new(None)); let delegate = runtime.clone(); + + let window_frame = true; + + let mut viewport = egui::ViewportBuilder::default() + .with_resizable(true) + .with_title(i18n("Kaspa NG")) + .with_min_inner_size([400.0,320.0]) + .with_inner_size([1000.0,600.0]) + .with_icon(svg_to_icon_data(KASPA_NG_ICON_SVG, FitTo::Size(256,256))); + + if window_frame { + viewport = viewport + .with_decorations(false) + .with_transparent(true); + } + let native_options = eframe::NativeOptions { persist_window : true, - viewport: egui::ViewportBuilder::default() - .with_resizable(true) - .with_title(i18n("Kaspa NG")) - .with_min_inner_size([400.0,320.0]) - .with_inner_size([1000.0,600.0]) - .with_icon(svg_to_icon_data(KASPA_NG_ICON_SVG, FitTo::Size(256,256))), + viewport, ..Default::default() }; eframe::run_native( @@ -239,7 +250,7 @@ cfg_if! { runtime::signals::Signals::bind(&runtime); runtime.start(); - Box::new(kaspa_ng_core::Core::new(cc, runtime, settings)) + Box::new(kaspa_ng_core::Core::new(cc, runtime, settings, window_frame)) }), )?; @@ -307,7 +318,7 @@ cfg_if! { &JsValue::from(adaptor), ).expect("failed to set adaptor"); - Box::new(kaspa_ng_core::Core::new(cc, runtime, settings)) + Box::new(kaspa_ng_core::Core::new(cc, runtime, settings, false)) }), ) .await diff --git a/core/src/core.rs b/core/src/core.rs index cbfbdb1..ee5c263 100644 --- a/core/src/core.rs +++ b/core/src/core.rs @@ -1,3 +1,4 @@ +use crate::frame::window_frame; use crate::imports::*; use crate::market::*; use crate::mobile::MobileMenu; @@ -51,7 +52,7 @@ pub struct Core { pub market: Option, pub servers: Arc>, pub debug: bool, - + pub window_frame: bool, callback_map : CallbackMap, } @@ -61,6 +62,7 @@ impl Core { cc: &eframe::CreationContext<'_>, runtime: crate::runtime::Runtime, mut settings: Settings, + window_frame: bool, ) -> Self { crate::fonts::init_fonts(cc); egui_extras::install_image_loaders(&cc.egui_ctx); @@ -185,7 +187,7 @@ impl Core { market: None, servers: parse_default_servers().clone(), debug: false, - + window_frame, callback_map: CallbackMap::default(), }; @@ -340,6 +342,10 @@ impl eframe::App for Core { println!("{}", i18n("bye!")); } + fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { + egui::Rgba::TRANSPARENT.to_array() + } + /// Called each time the UI needs repainting, which may be many times per second. /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { @@ -390,174 +396,182 @@ impl eframe::App for Core { ctx.set_visuals(current_visuals); // --- - // cfg_if! { - // if #[cfg(target_arch = "wasm32")] { - // visuals.interact_cursor = Some(CursorIcon::PointingHand); - // } - // } + self.device_mut().set_screen_size(&ctx.screen_rect()); - if !self.settings.initialized { - cfg_if! { - if #[cfg(not(target_arch = "wasm32"))] { - egui::CentralPanel::default().show(ctx, |ui| { - self.modules - .get(&TypeId::of::()) - .unwrap() - .clone() - .render(self, ctx, frame, ui); - }); + self.render_frame(ctx, frame); - return; - } - } + if let Some(module) = self.deactivation.take() { + module.deactivate(self); } - // let device = runtime().device(); - - self.device_mut().set_screen_size(&ctx.screen_rect()); - - // if ctx.screen_rect().width() < ctx.screen_rect().height() || ctx.screen_rect().width() < 540.0 { - // self.device.orientation = Orientation::Portrait; - // } else { - // self.device.orientation = Orientation::Landscape; - // } - // device.enable_portrait_desired(ctx.screen_rect().width() < 540.0); - - // if !self.module.modal() && !device.single_pane() { - if !self.module.modal() && !self.device.mobile() { - egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { - Menu::new(self).render(ui); - // self.render_menu(ui, frame); - }); + #[cfg(not(target_arch = "wasm32"))] + if let Some(screenshot) = self.screenshot.clone() { + self.handle_screenshot(ctx, screenshot); } + } +} - if self.device.orientation() == Orientation::Portrait { - CentralPanel::default() - .frame(Frame::default().fill(ctx.style().visuals.panel_fill)) - .show(ctx, |ui| { - egui::TopBottomPanel::bottom("portrait_bottom_panel").show_inside(ui, |ui| { - Status::new(self).render(ui); - }); +impl Core { + fn render_frame(&mut self, ctx: &Context, frame: &mut eframe::Frame) { + let is_fullscreen = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false)); - if self.device.mobile() { - egui::TopBottomPanel::bottom("mobile_menu_panel").show_inside(ui, |ui| { - MobileMenu::new(self).render(ui); + window_frame(self.window_frame && !is_fullscreen, ctx, "Kaspa NG", |ui| { + if !self.settings.initialized { + cfg_if! { + if #[cfg(not(target_arch = "wasm32"))] { + egui::CentralPanel::default().show_inside(ui, |ui| { + self.modules + .get(&TypeId::of::()) + .unwrap() + .clone() + .render(self, ctx, frame, ui); }); + + return; } + } + } - egui::CentralPanel::default().show_inside(ui, |ui| { - self.module.clone().render(self, ctx, frame, ui); - }); - }); - } else if self.device.single_pane() { - if !self.device.mobile() { - egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| { - Status::new(self).render(ui); - egui::warn_if_debug_build(ui); + if !self.module.modal() && !self.device.mobile() { + egui::TopBottomPanel::top("top_panel").show_inside(ui, |ui| { + Menu::new(self).render(ui); + // self.render_menu(ui, frame); }); } - let device_width = 390.0; - let margin_width = (ctx.screen_rect().width() - device_width) * 0.5; - - SidePanel::right("portrait_right") - .exact_width(margin_width) - .resizable(false) - .show_separator_line(true) - .frame(Frame::default().fill(Color32::BLACK)) - .show(ctx, |_ui| {}); - SidePanel::left("portrait_left") - .exact_width(margin_width) - .resizable(false) - .show_separator_line(true) - .frame(Frame::default().fill(Color32::BLACK)) - .show(ctx, |_ui| {}); - - CentralPanel::default() - .frame(Frame::default().fill(ctx.style().visuals.panel_fill)) - .show(ctx, |ui| { - ui.set_max_width(device_width); - - egui::TopBottomPanel::bottom("mobile_bottom_panel").show_inside(ui, |ui| { + if self.device.orientation() == Orientation::Portrait { + CentralPanel::default() + .frame(Frame::default().fill(ctx.style().visuals.panel_fill)) + .show_inside(ui, |ui| { + egui::TopBottomPanel::bottom("portrait_bottom_panel").show_inside( + ui, + |ui| { + Status::new(self).render(ui); + }, + ); + + if self.device.mobile() { + egui::TopBottomPanel::bottom("mobile_menu_panel").show_inside( + ui, + |ui| { + MobileMenu::new(self).render(ui); + }, + ); + } + + egui::CentralPanel::default().show_inside(ui, |ui| { + self.module.clone().render(self, ctx, frame, ui); + }); + }); + } else if self.device.single_pane() { + if !self.device.mobile() { + egui::TopBottomPanel::bottom("bottom_panel").show_inside(ui, |ui| { Status::new(self).render(ui); + egui::warn_if_debug_build(ui); }); + } - if self.device.mobile() { - egui::TopBottomPanel::bottom("mobile_menu_panel").show_inside(ui, |ui| { - MobileMenu::new(self).render(ui); + let device_width = 390.0; + let margin_width = (ctx.screen_rect().width() - device_width) * 0.5; + + SidePanel::right("portrait_right") + .exact_width(margin_width) + .resizable(false) + .show_separator_line(true) + .frame(Frame::default().fill(Color32::BLACK)) + .show_inside(ui, |_ui| {}); + SidePanel::left("portrait_left") + .exact_width(margin_width) + .resizable(false) + .show_separator_line(true) + .frame(Frame::default().fill(Color32::BLACK)) + .show_inside(ui, |_ui| {}); + + CentralPanel::default() + .frame(Frame::default().fill(ctx.style().visuals.panel_fill)) + .show_inside(ui, |ui| { + ui.set_max_width(device_width); + + egui::TopBottomPanel::bottom("mobile_bottom_panel").show_inside(ui, |ui| { + Status::new(self).render(ui); }); - } - egui::CentralPanel::default() - .frame( - Frame::default() - .inner_margin(0.) - .outer_margin(4.) - .fill(ctx.style().visuals.panel_fill), - ) - .show_inside(ui, |ui| { - self.module.clone().render(self, ctx, frame, ui); - }); - }); - } else { - egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| { - Status::new(self).render(ui); - // self.render_status(ui); - egui::warn_if_debug_build(ui); - }); + if self.device.mobile() { + egui::TopBottomPanel::bottom("mobile_menu_panel").show_inside( + ui, + |ui| { + MobileMenu::new(self).render(ui); + }, + ); + } - egui::CentralPanel::default().show(ctx, |ui| { - self.module.clone().render(self, ctx, frame, ui); - }); - } + egui::CentralPanel::default() + .frame( + Frame::default() + .inner_margin(0.) + .outer_margin(4.) + .fill(ctx.style().visuals.panel_fill), + ) + .show_inside(ui, |ui| { + self.module.clone().render(self, ctx, frame, ui); + }); + }); + } else { + egui::TopBottomPanel::bottom("bottom_panel") + // TODO - review margins + .frame(Frame::default().rounding(4.).inner_margin(3.)) + .show_inside(ui, |ui| { + Status::new(self).render(ui); + egui::warn_if_debug_build(ui); + }); - if let Some(module) = self.deactivation.take() { - module.deactivate(self); - } + egui::CentralPanel::default().show_inside(ui, |ui| { + self.module.clone().render(self, ctx, frame, ui); + }); + } + }); + } - #[cfg(not(target_arch = "wasm32"))] - if let Some(screenshot) = self.screenshot.as_ref() { - match rfd::FileDialog::new().save_file() { - Some(mut path) => { - path.set_extension("png"); - let screen_rect = ctx.screen_rect(); - let pixels_per_point = ctx.pixels_per_point(); - let screenshot = screenshot.clone(); - let sender = self.sender(); - std::thread::Builder::new() - .name("screenshot".to_string()) - .spawn(move || { - let image = screenshot.region(&screen_rect, Some(pixels_per_point)); - image::save_buffer( - &path, - image.as_raw(), - image.width() as u32, - image.height() as u32, - image::ColorType::Rgba8, - ) - .unwrap(); - - sender - .try_send(Events::Notify { - user_notification: UserNotification::success(format!( - "Capture saved to\n{}", - path.to_string_lossy() - )), - }) - .unwrap() - }) - .expect("Unable to spawn screenshot thread"); - self.screenshot.take(); - } - None => { - self.screenshot.take(); - } + #[cfg(not(target_arch = "wasm32"))] + fn handle_screenshot(&mut self, ctx: &Context, screenshot: Arc) { + match rfd::FileDialog::new().save_file() { + Some(mut path) => { + path.set_extension("png"); + let screen_rect = ctx.screen_rect(); + let pixels_per_point = ctx.pixels_per_point(); + let screenshot = screenshot.clone(); + let sender = self.sender(); + std::thread::Builder::new() + .name("screenshot".to_string()) + .spawn(move || { + let image = screenshot.region(&screen_rect, Some(pixels_per_point)); + image::save_buffer( + &path, + image.as_raw(), + image.width() as u32, + image.height() as u32, + image::ColorType::Rgba8, + ) + .unwrap(); + + sender + .try_send(Events::Notify { + user_notification: UserNotification::success(format!( + "Capture saved to\n{}", + path.to_string_lossy() + )), + }) + .unwrap() + }) + .expect("Unable to spawn screenshot thread"); + self.screenshot.take(); + } + None => { + self.screenshot.take(); } } } -} -impl Core { fn _render_splash(&mut self, ui: &mut Ui) { let logo_rect = ui.ctx().screen_rect(); let logo_size = logo_rect.size(); diff --git a/core/src/frame.rs b/core/src/frame.rs new file mode 100644 index 0000000..8cd962a --- /dev/null +++ b/core/src/frame.rs @@ -0,0 +1,213 @@ +use crate::imports::*; + +pub fn window_frame( + enable: bool, + ctx: &egui::Context, + title: &str, + add_contents: impl FnOnce(&mut egui::Ui), +) { + if enable { + let mut stroke = ctx.style().visuals.widgets.noninteractive.fg_stroke; + // stroke.width = 0.5; + stroke.width = 1.0; + + let panel_frame = egui::Frame { + fill: ctx.style().visuals.window_fill(), + rounding: 10.0.into(), + stroke, + // stroke: ctx.style().visuals.widgets.noninteractive.fg_stroke, + outer_margin: 0.5.into(), // so the stroke is within the bounds + ..Default::default() + }; + + let outline_frame = egui::Frame { + // fill: ctx.style().visuals.window_fill(), + rounding: 10.0.into(), + stroke, + // stroke: ctx.style().visuals.widgets.noninteractive.fg_stroke, + outer_margin: 0.5.into(), // so the stroke is within the bounds + ..Default::default() + }; + + CentralPanel::default().frame(panel_frame).show(ctx, |ui| { + let app_rect = ui.max_rect(); + + let title_bar_height = 28.0; + let title_bar_rect = { + let mut rect = app_rect; + rect.max.y = rect.min.y + title_bar_height; + rect + }; + title_bar_ui(ui, title_bar_rect, title); + + // Add the contents: + let content_rect = { + let mut rect = app_rect; + rect.min.y = title_bar_rect.max.y; + rect + }; + // .shrink(4.0); + // .shrink2(vec2(8.0,4.0)); + let mut content_ui = ui.child_ui(content_rect, *ui.layout()); + add_contents(&mut content_ui); + + // panel_frame.show(ui); + ui.painter().add(outline_frame.paint(app_rect)); + }); + } else { + let panel_frame = egui::Frame { + fill: ctx.style().visuals.window_fill(), + inner_margin: 0.0.into(), + outer_margin: 0.0.into(), + ..Default::default() + }; + + CentralPanel::default().frame(panel_frame).show(ctx, |ui| { + let app_rect = ui.max_rect(); + let mut content_ui = ui.child_ui(app_rect, *ui.layout()); + add_contents(&mut content_ui); + }); + } +} + +fn title_bar_ui(ui: &mut egui::Ui, title_bar_rect: eframe::epaint::Rect, title: &str) { + use egui::*; + + let painter = ui.painter(); + + let title_bar_response = ui.interact(title_bar_rect, Id::new("title_bar"), Sense::click()); + + // Paint the title: + painter.text( + title_bar_rect.center(), + Align2::CENTER_CENTER, + title, + FontId::proportional(16.0), + ui.style().visuals.text_color(), + ); + + // Paint the line under the title: + painter.line_segment( + [ + title_bar_rect.left_bottom() + vec2(1.0, 0.0), + title_bar_rect.right_bottom() + vec2(-1.0, 0.0), + ], + ui.visuals().widgets.noninteractive.bg_stroke, + ); + + // Interact with the title bar (drag to move window): + if title_bar_response.double_clicked() { + let is_maximized = ui.input(|i| i.viewport().maximized.unwrap_or(false)); + ui.ctx() + .send_viewport_cmd(ViewportCommand::Maximized(!is_maximized)); + } else if title_bar_response.is_pointer_button_down_on() { + ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag); + } + + ui.allocate_ui_at_rect(title_bar_rect, |ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.visuals_mut().button_frame = false; + ui.add_space(8.0); + close_maximize_minimize(ui); + }); + }); +} + +/// Show some close/maximize/minimize buttons for the native window. +fn close_maximize_minimize(ui: &mut egui::Ui) { + use egui_phosphor::light::*; + + let spacing = 8.0; + let button_height = 16.0; + + let close_response = ui + .add(Button::new( + RichText::new(X.to_string()).size(button_height), + )) + // .add(Button::new(RichText::new("❌").size(button_height))) + .on_hover_text("Close the window"); + if close_response.clicked() { + ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); + } + + cfg_if! { + if #[cfg(target_os = "macos")] { + let support_fullscreen = true; + let support_maximize = true; + } else { + let support_fullscreen = true; + let support_maximize = true; + } + } + + if support_fullscreen { + ui.add_space(spacing); + + let is_fullscreen = ui.input(|i| i.viewport().fullscreen.unwrap_or(false)); + if is_fullscreen { + let fullscreen_response = ui + // .add(Button::new(RichText::new("🗗").size(button_height))) + .add(Button::new( + RichText::new(ARROWS_IN.to_string()).size(button_height), + )) + .on_hover_text("Exit Full Screen"); + if fullscreen_response.clicked() { + ui.ctx() + .send_viewport_cmd(ViewportCommand::Fullscreen(false)); + } + } else { + let fullscreen_response = ui + // .add(Button::new(RichText::new("🗗").size(button_height))) + .add(Button::new( + RichText::new(ARROWS_OUT.to_string()).size(button_height), + )) + // .add(Button::new(RichText::new(ARROWS_OUT.to_string()).size(button_height))) + .on_hover_text("Full Screen"); + if fullscreen_response.clicked() { + ui.ctx() + .send_viewport_cmd(ViewportCommand::Fullscreen(true)); + } + } + } + + if support_maximize { + ui.add_space(spacing); + + let is_maximized = ui.input(|i| i.viewport().maximized.unwrap_or(false)); + if is_maximized { + let maximized_response = ui + // .add(Button::new(RichText::new("🗗").size(button_height))) + .add(Button::new( + RichText::new(RECTANGLE.to_string()).size(button_height), + )) + .on_hover_text("Restore window"); + if maximized_response.clicked() { + ui.ctx() + .send_viewport_cmd(ViewportCommand::Maximized(false)); + } + } else { + let maximized_response = ui + // .add(Button::new(RichText::new("🗗").size(button_height))) + .add(Button::new( + RichText::new(SQUARE.to_string()).size(button_height), + )) + // .add(Button::new(RichText::new(ARROWS_OUT.to_string()).size(button_height))) + .on_hover_text("Maximize window"); + if maximized_response.clicked() { + ui.ctx().send_viewport_cmd(ViewportCommand::Maximized(true)); + } + } + } + + ui.add_space(spacing + 2.0); + + let minimized_response = ui + .add(Button::new(RichText::new("🗕").size(button_height))) + // .add(Button::new(RichText::new(ARROW_SQUARE_DOWN.to_string()).size(button_height))) + // .add(Button::new(RichText::new(ARROW_LINE_DOWN.to_string()).size(button_height))) + .on_hover_text("Minimize the window"); + if minimized_response.clicked() { + ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true)); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 987d1ff..98457bb 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -13,6 +13,7 @@ pub mod egui; pub mod error; pub mod events; pub mod fonts; +pub mod frame; pub mod imports; pub mod market; pub mod menu; diff --git a/core/src/modules/overview.rs b/core/src/modules/overview.rs index 03ade12..5049b59 100644 --- a/core/src/modules/overview.rs +++ b/core/src/modules/overview.rs @@ -37,22 +37,27 @@ impl ModuleT for Overview { _frame: &mut eframe::Frame, ui: &mut egui::Ui, ) { - let width = ui.available_width(); if core.device().single_pane() { self.render_details(core, ui); } else { - SidePanel::left("overview_left").exact_width(width/2.).resizable(false).show_separator_line(true).show_inside(ui, |ui| { - egui::ScrollArea::vertical() + let width = ui.available_width(); + + SidePanel::left("overview_left") + .exact_width(width*0.5) + .resizable(false) + .show_separator_line(true) + .show_inside(ui, |ui| { + egui::ScrollArea::vertical() .id_source("overview_metrics") .auto_shrink([false; 2]) .show(ui, |ui| { self.render_stats(core,ui); }); - }); + }); SidePanel::right("overview_right") - .exact_width(width/2.) + .exact_width(width*0.5) .resizable(false) .show_separator_line(false) .show_inside(ui, |ui| { @@ -87,7 +92,7 @@ impl Overview { let screen_rect = ui.ctx().screen_rect(); let logo_size = vec2(648., 994.,) * 0.25; let left = screen_rect.width() - logo_size.x - 8.; - let top = 32.; + let top = if core.window_frame { 64. } else { 32. }; let logo_rect = Rect::from_min_size(Pos2::new(left, top), logo_size); if screen_rect.width() > 768.0 && !core.device().single_pane() { @@ -101,195 +106,195 @@ impl Overview { .paint_at(ui, logo_rect); } - egui::ScrollArea::vertical() - .id_source("overview_metrics") - .auto_shrink([false; 2]) - .show(ui, |ui| { - - if core.settings.market_monitor { - if let Some(market) = core.market.as_ref() { - - CollapsingHeader::new(i18n("Market")) - .default_open(true) - .show(ui, |ui| { - - if let Some(price_list) = market.price.as_ref() { - let mut symbols = price_list.keys().collect::>(); - symbols.sort(); - symbols.into_iter().for_each(|symbol| { - if let Some(data) = price_list.get(symbol) { - let symbol = symbol.to_uppercase(); - CollapsingHeader::new(symbol.as_str()) - .default_open(true) - .show(ui, |ui| { - Grid::new("market_price_info_grid") - .num_columns(2) - .spacing([16.0,4.0]) - .show(ui, |ui| { - let MarketData { price, volume, change, market_cap , precision } = *data; - ui.label(i18n("Price")); - ui.colored_label(theme_color().market_default_color, RichText::new(format_currency_with_symbol(price, precision, symbol.as_str()))); // - ui.end_row(); - - ui.label(i18n("24h Change")); - if change > 0. { - ui.colored_label(theme_color().market_up_color, RichText::new(format!("+{:.2}% ",change))); - } else { - ui.colored_label(theme_color().market_down_color, RichText::new(format!("{:.2}% ",change))); - }; - ui.end_row(); - - ui.label(i18n("Volume")); - ui.colored_label(theme_color().market_default_color, RichText::new(format!("{} {}",volume.trunc().separated_string(),symbol.to_uppercase()))); - ui.end_row(); - - ui.label(i18n("Market Cap")); - ui.colored_label(theme_color().market_default_color, RichText::new(format!("{} {}",market_cap.trunc().separated_string(),symbol.to_uppercase()))); - ui.end_row(); - }); - - }); - } - }) - } - }); + egui::ScrollArea::vertical() + .id_source("overview_metrics") + .auto_shrink([false; 2]) + .show(ui, |ui| { + + if core.settings.market_monitor { + if let Some(market) = core.market.as_ref() { + + CollapsingHeader::new(i18n("Market")) + .default_open(true) + .show(ui, |ui| { + + if let Some(price_list) = market.price.as_ref() { + let mut symbols = price_list.keys().collect::>(); + symbols.sort(); + symbols.into_iter().for_each(|symbol| { + if let Some(data) = price_list.get(symbol) { + let symbol = symbol.to_uppercase(); + CollapsingHeader::new(symbol.as_str()) + .default_open(true) + .show(ui, |ui| { + Grid::new("market_price_info_grid") + .num_columns(2) + .spacing([16.0,4.0]) + .show(ui, |ui| { + let MarketData { price, volume, change, market_cap , precision } = *data; + ui.label(i18n("Price")); + ui.colored_label(theme_color().market_default_color, RichText::new(format_currency_with_symbol(price, precision, symbol.as_str()))); // + ui.end_row(); + + ui.label(i18n("24h Change")); + if change > 0. { + ui.colored_label(theme_color().market_up_color, RichText::new(format!("+{:.2}% ",change))); + } else { + ui.colored_label(theme_color().market_down_color, RichText::new(format!("{:.2}% ",change))); + }; + ui.end_row(); + + ui.label(i18n("Volume")); + ui.colored_label(theme_color().market_default_color, RichText::new(format!("{} {}",volume.trunc().separated_string(),symbol.to_uppercase()))); + ui.end_row(); + + ui.label(i18n("Market Cap")); + ui.colored_label(theme_color().market_default_color, RichText::new(format!("{} {}",market_cap.trunc().separated_string(),symbol.to_uppercase()))); + ui.end_row(); + }); + + }); + } + }) + } + }); + } } - } - CollapsingHeader::new(i18n("Resources")) - .default_open(true) - .show(ui, |ui| { - // egui::special_emojis - // use egui_phosphor::light::{DISCORD_LOGO,GITHUB_LOGO}; - ui.hyperlink_to_tab( - format!("• {}",i18n("Kaspa NG on GitHub")), - "https://github.com/aspectron/kaspa-ng" - ); - ui.hyperlink_to_tab( - format!("• {}",i18n("Rusty Kaspa on GitHub")), - "https://github.com/kaspanet/rusty-kaspa", - ); - ui.hyperlink_to_tab( - format!("• {}",i18n("NPM Modules for NodeJS")), - "https://www.npmjs.com/package/kaspa", - ); - ui.hyperlink_to_tab( - format!("• {}",i18n("WASM SDK for JavaScript and TypeScript")), - "https://github.com/kaspanet/rusty-kaspa/wasm", - ); - ui.hyperlink_to_tab( - format!("• {}",i18n("Rust Wallet SDK")), - "https://docs.rs/kaspa-wallet-core/", - ); - ui.hyperlink_to_tab( - format!("• {}",i18n("Kaspa Discord")), - "https://discord.com/invite/kS3SK5F36R", - ); - }); + CollapsingHeader::new(i18n("Resources")) + .default_open(true) + .show(ui, |ui| { + // egui::special_emojis + // use egui_phosphor::light::{DISCORD_LOGO,GITHUB_LOGO}; + ui.hyperlink_to_tab( + format!("• {}",i18n("Kaspa NG on GitHub")), + "https://github.com/aspectron/kaspa-ng" + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("Rusty Kaspa on GitHub")), + "https://github.com/kaspanet/rusty-kaspa", + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("NPM Modules for NodeJS")), + "https://www.npmjs.com/package/kaspa", + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("WASM SDK for JavaScript and TypeScript")), + "https://github.com/kaspanet/rusty-kaspa/wasm", + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("Rust Wallet SDK")), + "https://docs.rs/kaspa-wallet-core/", + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("Kaspa Discord")), + "https://discord.com/invite/kS3SK5F36R", + ); + }); - if let Some(release) = core.release.as_ref() { - if is_wasm() || release.version == crate::app::VERSION { - CollapsingHeader::new(i18n("Redistributables")) - .id_source("redistributables") - .default_open(true) - .show(ui, |ui| { - release.assets.iter().for_each(|asset| { - Hyperlink::from_label_and_url( - format!("• {}", asset.name), - asset.browser_download_url.clone(), - ).open_in_new_tab(true).ui(ui); + if let Some(release) = core.release.as_ref() { + if is_wasm() || release.version == crate::app::VERSION { + CollapsingHeader::new(i18n("Redistributables")) + .id_source("redistributables") + .default_open(true) + .show(ui, |ui| { + release.assets.iter().for_each(|asset| { + Hyperlink::from_label_and_url( + format!("• {}", asset.name), + asset.browser_download_url.clone(), + ).open_in_new_tab(true).ui(ui); + }); }); - }); - } else { - CollapsingHeader::new(RichText::new(format!("{} {}",i18n("Update Available to version"), release.version)).color(theme_color().warning_color).strong()) - .id_source("redistributables-update") - .default_open(true) - .show(ui, |ui| { + } else { + CollapsingHeader::new(RichText::new(format!("{} {}",i18n("Update Available to version"), release.version)).color(theme_color().warning_color).strong()) + .id_source("redistributables-update") + .default_open(true) + .show(ui, |ui| { + + if let Some(html_url) = &release.html_url { + Hyperlink::from_label_and_url( + format!("• {} {}", i18n("GitHub Release"), release.version), + html_url, + ).open_in_new_tab(true).ui(ui); + } + + release.assets.iter().for_each(|asset| { + Hyperlink::from_label_and_url( + format!("• {}", asset.name), + asset.browser_download_url.clone(), + ).open_in_new_tab(true).ui(ui); + }); - if let Some(html_url) = &release.html_url { - Hyperlink::from_label_and_url( - format!("• {} {}", i18n("GitHub Release"), release.version), - html_url, - ).open_in_new_tab(true).ui(ui); - } - - release.assets.iter().for_each(|asset| { - Hyperlink::from_label_and_url( - format!("• {}", asset.name), - asset.browser_download_url.clone(), - ).open_in_new_tab(true).ui(ui); }); - }); - + } } - } - CollapsingHeader::new(i18n("Build")) - .default_open(true) - .show(ui, |ui| { - ui.label(format!("Kaspa NG v{}-{} + Rusty Kaspa v{}", env!("CARGO_PKG_VERSION"),crate::app::GIT_DESCRIBE, kaspa_wallet_core::version())); - ui.label(format!("Timestamp: {}", crate::app::BUILD_TIMESTAMP)); - ui.label(format!("rustc {}-{} {} llvm {}", - crate::app::RUSTC_SEMVER, - crate::app::RUSTC_COMMIT_HASH.chars().take(8).collect::(), - crate::app::RUSTC_CHANNEL, - crate::app::RUSTC_LLVM_VERSION, - )); - ui.label(format!("architecture {}", - crate::app::CARGO_TARGET_TRIPLE - )); - }); - - if let Some(system) = runtime().system() { - system.render(ui); - } - - CollapsingHeader::new(i18n("License Information")) - .default_open(false) - .show(ui, |ui| { - ui.vertical(|ui|{ - ui.label("Rusty Kaspa"); - ui.label("Copyright (c) 2023 Kaspa Developers"); - ui.label("License: ISC"); - ui.hyperlink_url_to_tab("https://github.com/kaspanet/rusty-kaspa"); - ui.label(""); - ui.label("Kaspa NG"); - ui.label("Copyright (c) 2023 ASPECTRON"); - ui.label("Restricted MIT or Apache 2.0 License"); - ui.hyperlink_url_to_tab("https://github.com/aspectron/kaspa-ng"); - ui.label(""); - ui.label("WORKFLOW-RS"); - ui.label("Copyright (c) 2023 ASPECTRON"); - ui.label("License: MIT or Apache 2.0"); - ui.hyperlink_url_to_tab("https://github.com/workflow-rs/workflow-rs"); - ui.label(""); - ui.label("EGUI"); - ui.label("Copyright (c) 2023 Rerun"); - ui.label("License: MIT or Apache 2.0"); - ui.hyperlink_url_to_tab("https://github.com/emilk/egui"); - ui.label(""); - ui.label("PHOSPHOR ICONS"); - ui.label("Copyright (c) 2023 "); - ui.label("License: MIT"); - ui.hyperlink_url_to_tab("https://phosphoricons.com/"); - ui.label(""); - ui.label("Illustration Art"); - ui.label("Copyright (c) 2023 Rhubarb Media"); - ui.label("License: CC BY 4.0"); - ui.hyperlink_url_to_tab("https://rhubarbmedia.ca/"); - ui.label(""); + CollapsingHeader::new(i18n("Build")) + .default_open(true) + .show(ui, |ui| { + ui.label(format!("Kaspa NG v{}-{} + Rusty Kaspa v{}", env!("CARGO_PKG_VERSION"),crate::app::GIT_DESCRIBE, kaspa_wallet_core::version())); + ui.label(format!("Timestamp: {}", crate::app::BUILD_TIMESTAMP)); + ui.label(format!("rustc {}-{} {} llvm {}", + crate::app::RUSTC_SEMVER, + crate::app::RUSTC_COMMIT_HASH.chars().take(8).collect::(), + crate::app::RUSTC_CHANNEL, + crate::app::RUSTC_LLVM_VERSION, + )); + ui.label(format!("architecture {}", + crate::app::CARGO_TARGET_TRIPLE + )); }); - }); - CollapsingHeader::new(i18n("Donations")) - .default_open(true) + if let Some(system) = runtime().system() { + system.render(ui); + } + + CollapsingHeader::new(i18n("License Information")) + .default_open(false) .show(ui, |ui| { - if ui.link(i18n("Supporting Kaspa NG development")).clicked() { - core.select::(); - } + ui.vertical(|ui|{ + ui.label("Rusty Kaspa"); + ui.label("Copyright (c) 2023 Kaspa Developers"); + ui.label("License: ISC"); + ui.hyperlink_url_to_tab("https://github.com/kaspanet/rusty-kaspa"); + ui.label(""); + ui.label("Kaspa NG"); + ui.label("Copyright (c) 2023 ASPECTRON"); + ui.label("Restricted MIT or Apache 2.0 License"); + ui.hyperlink_url_to_tab("https://github.com/aspectron/kaspa-ng"); + ui.label(""); + ui.label("WORKFLOW-RS"); + ui.label("Copyright (c) 2023 ASPECTRON"); + ui.label("License: MIT or Apache 2.0"); + ui.hyperlink_url_to_tab("https://github.com/workflow-rs/workflow-rs"); + ui.label(""); + ui.label("EGUI"); + ui.label("Copyright (c) 2023 Rerun"); + ui.label("License: MIT or Apache 2.0"); + ui.hyperlink_url_to_tab("https://github.com/emilk/egui"); + ui.label(""); + ui.label("PHOSPHOR ICONS"); + ui.label("Copyright (c) 2023 "); + ui.label("License: MIT"); + ui.hyperlink_url_to_tab("https://phosphoricons.com/"); + ui.label(""); + ui.label("Illustration Art"); + ui.label("Copyright (c) 2023 Rhubarb Media"); + ui.label("License: CC BY 4.0"); + ui.hyperlink_url_to_tab("https://rhubarbmedia.ca/"); + ui.label(""); + }); }); - }); + + CollapsingHeader::new(i18n("Donations")) + .default_open(true) + .show(ui, |ui| { + if ui.link(i18n("Supporting Kaspa NG development")).clicked() { + core.select::(); + } + }); + }); } fn render_graphs(&mut self, core: &mut Core, ui : &mut Ui) { diff --git a/core/src/runtime/services/kaspa/config.rs b/core/src/runtime/services/kaspa/config.rs index fe2a538..0e2b95b 100644 --- a/core/src/runtime/services/kaspa/config.rs +++ b/core/src/runtime/services/kaspa/config.rs @@ -1,9 +1,20 @@ +use crate::app::{GIT_DESCRIBE, VERSION}; use crate::imports::*; use crate::utils::Arglist; - +use kaspa_core::kaspad_env; #[cfg(not(target_arch = "wasm32"))] pub use kaspad_lib::args::Args; +fn user_agent_comment() -> String { + format!( + "/{}:{}/kaspa-ng:{}-{}/", + kaspad_env::name(), + kaspad_env::version(), + VERSION, + GIT_DESCRIBE + ) +} + #[derive(Debug, Clone)] pub struct Config { network: Network, @@ -56,6 +67,10 @@ cfg_if! { args.rpclisten = Some(config.grpc_network_interface.into()); } + args.user_agent_comments = vec![user_agent_comment()]; + + // TODO - parse custom args and overlap on top of the defaults + Ok(args) } } @@ -92,6 +107,8 @@ cfg_if! { args.push("--rpclisten-borsh=default"); + args.push(format!("--uacomment={}", user_agent_comment())); + if config.kaspad_daemon_args_enable { config.kaspad_daemon_args.trim().split(' ').filter(|arg|!arg.trim().is_empty()).for_each(|arg| { args.push(arg); diff --git a/core/src/status.rs b/core/src/status.rs index 366e856..8c42b9d 100644 --- a/core/src/status.rs +++ b/core/src/status.rs @@ -118,10 +118,11 @@ impl<'core> Status<'core> { let status_area_width = ui.available_width() - 24.; let status_icon_size = theme_style().status_icon_size; let module = self.module().clone(); + let left_padding = 8.0; match state { ConnectionStatus::Disconnected => { - ui.add_space(4.); + ui.add_space(left_padding); match self.settings().node.node_kind { KaspadNodeKind::Disable => { @@ -195,7 +196,7 @@ impl<'core> Status<'core> { peers, tps: _, } => { - ui.add_space(4.); + ui.add_space(left_padding); ui.label( RichText::new(egui_phosphor::light::CPU) .size(status_icon_size) @@ -234,7 +235,7 @@ impl<'core> Status<'core> { ConnectionStatus::Syncing { sync_status, peers } => { ui.vertical(|ui| { ui.horizontal(|ui| { - ui.add_space(4.); + ui.add_space(left_padding); ui.label( RichText::new(egui_phosphor::light::CLOUD_ARROW_DOWN) .size(status_icon_size)