From afee403dd082e0f4c860d4032334481b5963f3d6 Mon Sep 17 00:00:00 2001 From: Reboot-Codes Date: Tue, 3 Jun 2025 15:10:15 -0700 Subject: [PATCH] Add menu bar --- Cargo.lock | 117 +++++++++++ toolbox/ratchet/Cargo.toml | 2 + toolbox/ratchet/src/main.rs | 7 +- .../ratchet/src/screens/configurator/mod.rs | 70 ++++--- toolbox/ratchet/src/screens/welcome/mod.rs | 18 +- toolbox/ratchet/src/screens/wizard/mod.rs | 25 ++- toolbox/ratchet/src/util/get_app_data_dir.rs | 69 ++++--- toolbox/ratchet/src/util/menu.rs | 184 ++++++++++++++++++ toolbox/ratchet/src/util/mod.rs | 1 + 9 files changed, 422 insertions(+), 71 deletions(-) create mode 100644 toolbox/ratchet/src/util/menu.rs diff --git a/Cargo.lock b/Cargo.lock index df35e96..f1a69b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3189,6 +3189,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + [[package]] name = "flume" version = "0.11.1" @@ -4137,6 +4143,23 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "iced_aw" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "582c517a94ce3205da98e9c10b26bb71aa36b7d7d084441d826dc912711d1bac" +dependencies = [ + "cfg-if", + "chrono", + "getrandom 0.3.3", + "iced", + "iced_fonts 0.1.1", + "itertools 0.14.0", + "num-format", + "num-traits", + "web-time", +] + [[package]] name = "iced_core" version = "0.13.2" @@ -4157,6 +4180,25 @@ dependencies = [ "web-time", ] +[[package]] +name = "iced_fonts" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7deb0800a850ee25c8a42559f72c0f249e577feb3aad37b9b65dc1e517e52a" +dependencies = [ + "iced_core", +] + +[[package]] +name = "iced_fonts" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db9d45e87bc3ac22ff82e2d9dea9139f1c055137f1e0f39df3d68110fcee7a9" +dependencies = [ + "iced_core", + "iced_widget", +] + [[package]] name = "iced_futures" version = "0.13.2" @@ -4198,6 +4240,7 @@ dependencies = [ "iced_core", "iced_futures", "log", + "lyon_path", "once_cell", "raw-window-handle", "rustc-hash 2.1.1", @@ -4261,6 +4304,7 @@ dependencies = [ "iced_glyphon", "iced_graphics", "log", + "lyon", "once_cell", "rustc-hash 2.1.1", "thiserror 1.0.69", @@ -4577,6 +4621,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -4911,6 +4964,58 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +[[package]] +name = "lyon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8af69edc087272df438b3ee436c4bb6d7c04aa8af665cfd398feae627dbd8570" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0047f508cd7a85ad6bad9518f68cce7b1bf6b943fb71f6da0ee3bc1e8cb75f25" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579d42360a4b09846eff2feef28f538696c7d6c7439bfa65874ff3cbe0951b2c" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + [[package]] name = "macaddr" version = "1.0.1" @@ -5415,6 +5520,16 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -6415,6 +6530,8 @@ dependencies = [ "carbon-steel", "env_logger", "iced", + "iced_aw", + "iced_fonts 0.2.1", "log", "serde", "tokio", diff --git a/toolbox/ratchet/Cargo.toml b/toolbox/ratchet/Cargo.toml index e37d5dc..725f303 100644 --- a/toolbox/ratchet/Cargo.toml +++ b/toolbox/ratchet/Cargo.toml @@ -11,3 +11,5 @@ serde = "1.0.219" env_logger = "0.11.8" anyhow = "1.0.98" log = "0.4.27" +iced_aw = "0.12.2" +iced_fonts = "0.2.1" diff --git a/toolbox/ratchet/src/main.rs b/toolbox/ratchet/src/main.rs index a81e521..29cda16 100644 --- a/toolbox/ratchet/src/main.rs +++ b/toolbox/ratchet/src/main.rs @@ -1,7 +1,8 @@ #![feature(iter_collect_into)] +#![feature(stmt_expr_attributes)] pub mod screens; -// pub mod util; +pub mod util; use std::{ collections::HashMap, @@ -34,7 +35,7 @@ use crate::screens::{ }; fn theme(_state: &MainAppState) -> Theme { - Theme::TokyoNight + Theme::Oxocarbon } pub const APP_NAME: &str = "Ratchet"; @@ -72,6 +73,7 @@ impl Default for MainAppState { #[derive(Debug, Clone)] pub enum Message { + None, MoveToScreen(MoveToScreen), SetWizardStep(WizardStep), SetConfiguratorTab(ConfiguratorTab), @@ -253,6 +255,7 @@ impl MainAppState { } } }, + Message::None => Task::none(), } } diff --git a/toolbox/ratchet/src/screens/configurator/mod.rs b/toolbox/ratchet/src/screens/configurator/mod.rs index 1e62e2b..23ce160 100644 --- a/toolbox/ratchet/src/screens/configurator/mod.rs +++ b/toolbox/ratchet/src/screens/configurator/mod.rs @@ -1,8 +1,11 @@ use iced::{ + Element, Task, + Theme, widget::{ button, column, + container, row, scrollable, text, @@ -13,6 +16,7 @@ use iced::{ use crate::{ Message, screens::MoveToScreen, + util::menu::gen_menu_bar, }; #[derive(Debug, Clone)] @@ -51,11 +55,8 @@ impl ConfiguratorScreen { } pub fn view(&self, _state: &crate::MainAppState) -> iced::Element { - let mut elements = Vec::new(); - let mut sidebar = Vec::new(); - let mut content: Vec> = Vec::new(); - - sidebar.push( + let mut elements: Vec> = vec![]; + let sidebar = vec![ button("Overview") .on_press_maybe({ if self.tab != ConfiguratorTab::Overview { @@ -66,8 +67,6 @@ impl ConfiguratorScreen { }) .width(iced::Length::Fill) .into(), - ); - sidebar.push( button("Modules") .on_press_maybe({ if self.tab != ConfiguratorTab::Modules { @@ -78,8 +77,6 @@ impl ConfiguratorScreen { }) .width(iced::Length::Fill) .into(), - ); - sidebar.push( button("Apps") .on_press_maybe({ if self.tab != ConfiguratorTab::Apps { @@ -90,8 +87,6 @@ impl ConfiguratorScreen { }) .width(iced::Length::Fill) .into(), - ); - sidebar.push( button("Repos") .on_press_maybe({ if self.tab != ConfiguratorTab::Repos { @@ -102,14 +97,13 @@ impl ConfiguratorScreen { }) .width(iced::Length::Fill) .into(), - ); - sidebar.push(vertical_space().into()); - sidebar.push( + vertical_space().into(), button("Instances") .on_press(Message::MoveToScreen(MoveToScreen::Welcome)) .width(iced::Length::Fill) .into(), - ); + ]; + let mut content: Vec> = Vec::new(); match self.tab { ConfiguratorTab::Overview => { @@ -127,12 +121,19 @@ impl ConfiguratorScreen { } elements.push( - column(sidebar) - .height(iced::Length::Fill) - .width(256) - .spacing(12) - .padding(12) - .into(), + container( + column(sidebar) + .height(iced::Length::Fill) + .width(128) + .spacing(12) + .padding(12), + ) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + iced::widget::container::Style::default().background(palette.primary.base.text) + }) + .into(), ); elements.push( scrollable( @@ -144,26 +145,31 @@ impl ConfiguratorScreen { .width(iced::Length::Fill) .into(), ); - row(elements) - .height(iced::Length::Fill) - .width(iced::Length::Fill) - .into() + column(vec![ + gen_menu_bar(), + row(elements) + .height(iced::Length::Fill) + .width(iced::Length::Fill) + .into(), + ]) + .into() } - pub fn update(&mut self, message: crate::Message) -> Action { + pub fn update(&mut self, message: Message) -> Action { match message { - crate::Message::MoveToScreen(target_screen) => match target_screen { - super::MoveToScreen::Welcome => Action::MoveToScreen(MoveToScreen::Welcome), - super::MoveToScreen::Configurator(_id) => Action::None, - super::MoveToScreen::Wizard(starting_point) => { + Message::MoveToScreen(target_screen) => match target_screen { + MoveToScreen::Welcome => Action::MoveToScreen(MoveToScreen::Welcome), + MoveToScreen::Wizard(starting_point) => { Action::MoveToScreen(MoveToScreen::Wizard(starting_point)) } + _ => Action::None, }, - crate::Message::SetWizardStep(_wizard_step) => Action::None, - crate::Message::SetConfiguratorTab(tab) => { + Message::SetWizardStep(_wizard_step) => Action::None, + Message::SetConfiguratorTab(tab) => { self.tab = tab; Action::None } + Message::None => Action::None, } } } diff --git a/toolbox/ratchet/src/screens/welcome/mod.rs b/toolbox/ratchet/src/screens/welcome/mod.rs index 8603a6c..718e5db 100644 --- a/toolbox/ratchet/src/screens/welcome/mod.rs +++ b/toolbox/ratchet/src/screens/welcome/mod.rs @@ -11,9 +11,12 @@ use log::{ }; use std::collections::HashMap; -use crate::screens::{ - MoveToScreen, - wizard::WizardStep, +use crate::{ + screens::{ + MoveToScreen, + wizard::WizardStep, + }, + util::menu::gen_menu_bar, }; #[derive(Debug, Clone, Default)] @@ -38,8 +41,7 @@ impl WelcomeScreen { Action::MoveToScreen(MoveToScreen::Configurator(id)) } }, - crate::Message::SetWizardStep(_step) => Action::None, - crate::Message::SetConfiguratorTab(_tab) => Action::None, + _ => Action::None, } } @@ -95,7 +97,11 @@ impl WelcomeScreen { .into(), ); - column(elements).spacing(12).padding(12).into() + column(vec![ + gen_menu_bar(), + column(elements).padding(12).spacing(12).into(), + ]) + .into() } } diff --git a/toolbox/ratchet/src/screens/wizard/mod.rs b/toolbox/ratchet/src/screens/wizard/mod.rs index 4f7e0ca..1411d3c 100644 --- a/toolbox/ratchet/src/screens/wizard/mod.rs +++ b/toolbox/ratchet/src/screens/wizard/mod.rs @@ -1,4 +1,7 @@ -use crate::screens::MoveToScreen; +use crate::{ + screens::MoveToScreen, + util::menu::gen_menu_bar, +}; use iced::{ Task, widget::{ @@ -84,15 +87,26 @@ impl WizardScreen { } } - elements.push(vertical_space().into()); - elements.push( + column(vec![ + gen_menu_bar(), + column(elements) + .padding(iced::Padding { + top: 12.0, + bottom: 0.0, + right: 12.0, + left: 12.0, + }) + .spacing(12) + .into(), + vertical_space().into(), row(controls) .spacing(12) .padding(12) .width(iced::Length::Fill) .into(), - ); - column(elements).spacing(12).padding(12).into() + ]) + .spacing(12) + .into() } pub fn update(&mut self, message: crate::Message) -> Action { @@ -112,6 +126,7 @@ impl WizardScreen { Action::None } crate::Message::SetConfiguratorTab(_tab) => Action::None, + crate::Message::None => Action::None, } } } diff --git a/toolbox/ratchet/src/util/get_app_data_dir.rs b/toolbox/ratchet/src/util/get_app_data_dir.rs index fdd705d..9ecaa9c 100644 --- a/toolbox/ratchet/src/util/get_app_data_dir.rs +++ b/toolbox/ratchet/src/util/get_app_data_dir.rs @@ -1,6 +1,9 @@ use crate::APP_NAME; +use anyhow::Context; use anyhow::anyhow; +use log::error; use log::warn; +use std::env; use std::path::Path; use std::{ fs, @@ -8,6 +11,8 @@ use std::{ }; pub fn get_app_data_dir() -> Result { + // Not actually unused btw. + #[allow(unused_assignments)] let mut app_data_directory = Err(anyhow!( "App data dir unset from function init, this is a bug!" )); @@ -16,27 +21,28 @@ pub fn get_app_data_dir() -> Result { { match env::var("XDG_STATE_HOME") { // Use the set XDG_STATE_HOME directly if it's configured. - Some(local_state_dir) => match fs::canonicalize(PathBuf::from(local_state_dir)) { - Ok(path) => match fs::create_dir_all(path.push(APP_NAME)) { - Ok(_) => Ok(path.push(APP_NAME)), - Err(_) => todo!(), + Ok(local_state_dir) => match fs::canonicalize(PathBuf::from(local_state_dir)) { + Ok(path) => match fs::create_dir_all(path.join(APP_NAME)).with_context(|| format!("Tried to create application data directory in \"{}\", please fix your XDG_STATE_HOME permissions.", path.join(APP_NAME).to_str().unwrap())) { + Ok(_) => app_data_directory = Ok(path.join(APP_NAME).to_str().unwrap().to_string()), + Err(e) => app_data_directory = Err(e.into()), }, - Err(e) => Err(e.into()), + Err(e) => app_data_directory = Err(e.into()), }, - None => { + Err(_e) => { warn!( "$XDG_STATE_HOME not set... maybe set that! Trying to get to $HOME/.local/state manually..." ); let home_directory = match env::var("HOME") { // Prefered option, since this works 99% of the time, even if it's root, or non FHS... - Some(home_dir) => Some(home_dir), - None => { + Ok(home_dir) => Some(home_dir), + Err(_e) => { warn!( "Using standard home directory path of /home/$USER/.local/state. Seriously, set your environment variables." ); + match env::var("USER") { - Some(username) => { + Ok(username) => { let generated_home_path = format!("/home/{}", &username); if Path::new(&generated_home_path).exists() { @@ -45,6 +51,7 @@ pub fn get_app_data_dir() -> Result { None } } + Err(_e) => None, } } }; @@ -53,49 +60,53 @@ pub fn get_app_data_dir() -> Result { Some(home_dir) => { let xdg_state_home = PathBuf::from(format!("{}/.local/state", home_dir)); - match fs::canonicalize(xdg_state_home) { + match fs::canonicalize(&xdg_state_home) { Ok(real_path) => { - app_data_directory = real_path.push(APP_NAME).into(); + app_data_directory = Ok(real_path.join(APP_NAME).to_str().unwrap().to_string()); } Err(e) => { - use anyhow::Context; - warn!( "Error when canonicalizing \"{}\"; but we'll try and create it anyways: {}", - xdg_state_home, e + xdg_state_home.to_str().unwrap(), + e ); - match fs::create_dir_all(xdg_state_home.push(APP_NAME)).with_context(|| { - format!("Was unable to cannonicalize \"{}\", and tried to create it manually.") + match fs::create_dir_all(xdg_state_home.join(APP_NAME)).with_context(|| { + format!( + "Was unable to cannonicalize \"{}\", and tried to create it manually.", + xdg_state_home.join(APP_NAME).to_str().unwrap() + ) }) { Ok(_) => { // This should be fine to skip canonicalization since we just created the directory from this location. - app_data_directory = Ok(xdg_state_home.push(APP_NAME).into()); + app_data_directory = + Ok(xdg_state_home.join(APP_NAME).to_str().unwrap().to_string()); } Err(e) => { app_data_directory = Err(e.into()); } - } + }; } } } None => { - use anyhow::Context; - error!( "Unable to get the user's home directory! Using \"/tmp\" instead, this is really bad, configure your system please." ); let tmp_dir = PathBuf::from("/tmp"); - match fs::canonicalize(tmp_dir) { + match fs::canonicalize(&tmp_dir) { Ok(real_path) => { - match fs::create_dir_all(tmp_dir.push(APP_NAME)).with_context(|| { - format!("Attempted to create \"/tmp/{}\", to use instead of XDG_STATE_HOME.",) + match fs::create_dir_all(real_path.join(APP_NAME)).with_context(|| { + format!( + "Attempted to create \"/tmp/{}\", to use instead of XDG_STATE_HOME.", + real_path.join(APP_NAME).to_str().unwrap() + ) }) { Ok(_) => { // This should be fine to skip canonicalization since we just created the directory from this location. - app_data_directory = Ok(tmp_dir.push(APP_NAME).into()); + app_data_directory = Ok(real_path.join(APP_NAME).to_str().unwrap().to_string()); } Err(e) => { app_data_directory = Err(e.into()); @@ -107,8 +118,14 @@ pub fn get_app_data_dir() -> Result { } } } - } + }; } - } + }; } + + #[cfg(target_os = "windows")] + #[cfg(target_os = "macos")] + todo!(); + + app_data_directory } diff --git a/toolbox/ratchet/src/util/menu.rs b/toolbox/ratchet/src/util/menu.rs new file mode 100644 index 0000000..e1b5ea9 --- /dev/null +++ b/toolbox/ratchet/src/util/menu.rs @@ -0,0 +1,184 @@ +// This example demonstrates how to use the menu widget + +use iced::border::Radius; +use iced::widget::{ + button, + row, + text, +}; +use iced::{ + Border, + Color, + Element, + Length, + alignment, +}; + +use iced_aw::menu::{ + self, + Item, + Menu, +}; +use iced_aw::style::{ + Status, + menu_bar::primary, +}; +use iced_aw::{ + menu_bar, + menu_items, +}; +use iced_aw::{ + quad, + widgets::InnerBounds, +}; +use iced_fonts::REQUIRED_FONT; +use iced_fonts::required::{ + RequiredIcons, + icon_to_string, +}; + +use crate::Message; + +pub fn gen_menu_bar<'a>() -> iced::Element<'a, Message> { + let menu_tpl_1 = |items| Menu::new(items).max_width(180.0).offset(15.0).spacing(5.0); + let menu_tpl_2 = |items| Menu::new(items).max_width(180.0).offset(0.0).spacing(5.0); + + let file_menu = { + #[rustfmt::skip] + let sub1 = menu_tpl_2(menu_items!( + (debug_button("Item")) + (debug_button("Item")) + (debug_button("Item")) + (debug_button("Item")) + (debug_button("Item")) + )) + .width(220.0); + + #[rustfmt::skip] + menu_tpl_1(menu_items!( + (debug_button("New Connection")) + (debug_button("Item")) + (submenu_button("A sub menu"), sub1) + (debug_button("Item")) + (debug_button("Item")) + (debug_button("Item")) + )) + .width(140.0) + }; + + let mb = menu_bar!((debug_button_s("File"), file_menu)) + .draw_path(menu::DrawPath::Backdrop) + .style(|theme: &iced::Theme, status: Status| menu::Style { + path_border: Border { + radius: Radius::new(6.0), + ..Default::default() + }, + ..primary(theme, status) + }); + + mb.into() +} + +fn base_button<'a>( + content: impl Into>, + msg: Option, +) -> button::Button<'a, Message> { + button(content) + .padding([4, 8]) + .on_press_maybe(msg) + .style(iced::widget::button::text) +} + +fn debug_button(label: &str) -> button::Button { + labeled_button(label, Some(Message::None)).width(Length::Fill) +} + +fn debug_button_s(label: &str) -> button::Button { + labeled_button(label, Some(Message::None)).width(Length::Shrink) +} + +fn labeled_button( + label: &str, + msg: Option, +) -> button::Button { + base_button(text(label).align_y(alignment::Vertical::Center), msg) +} + +fn submenu_button(label: &str) -> button::Button { + base_button( + row![ + text(label) + .width(Length::Fill) + .align_y(alignment::Vertical::Center), + text(icon_to_string(RequiredIcons::CaretRightFill)) + .font(REQUIRED_FONT) + .width(Length::Shrink) + .align_y(alignment::Vertical::Center), + ] + .align_y(iced::Alignment::Center), + Some(Message::None), + ) + .width(Length::Fill) +} + +fn separator() -> quad::Quad { + quad::Quad { + quad_color: Color::from([0.5; 3]).into(), + quad_border: Border { + radius: Radius::new(4.0), + ..Default::default() + }, + inner_bounds: InnerBounds::Ratio(0.98, 0.2), + height: Length::Fixed(20.0), + ..Default::default() + } +} + +fn dot_separator<'a>(theme: &iced::Theme) -> Element<'a, Message, iced::Theme, iced::Renderer> { + row((0..20).map(|_| { + quad::Quad { + quad_color: theme.extended_palette().background.base.text.into(), + inner_bounds: InnerBounds::Square(4.0), + ..separator() + } + .into() + })) + .height(20.0) + .into() +} + +fn labeled_separator(label: &'_ str) -> Element<'_, Message, iced::Theme, iced::Renderer> { + let q_1 = quad::Quad { + height: Length::Fill, + ..separator() + }; + let q_2 = quad::Quad { + height: Length::Fill, + ..separator() + }; + + row![ + q_1, + text(label) + .height(Length::Fill) + .align_y(alignment::Vertical::Center), + q_2, + ] + .height(20.0) + .into() +} + +fn circle(color: Color) -> quad::Quad { + let radius = 10.0; + + quad::Quad { + quad_color: color.into(), + inner_bounds: InnerBounds::Square(radius * 2.0), + quad_border: Border { + radius: Radius::new(radius), + ..Default::default() + }, + height: Length::Fixed(20.0), + ..Default::default() + } +} diff --git a/toolbox/ratchet/src/util/mod.rs b/toolbox/ratchet/src/util/mod.rs index 847a776..7d4847d 100644 --- a/toolbox/ratchet/src/util/mod.rs +++ b/toolbox/ratchet/src/util/mod.rs @@ -1 +1,2 @@ pub mod get_app_data_dir; +pub mod menu; -- 2.51.2