From 7bab7c7b8dcb310bd4a6dbe4455ad6b5b56bccce Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 3 Jul 2026 09:51:49 +0300 Subject: [PATCH] feat(ui): replace app-menu logo with a quick-access file toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the OCS-logo button and the dropdown menu it opened; the top strip now shows direct New / Open / Save / Save As / Print buttons ahead of the undo/redo controls. Drop the HELP and OPTIONS commands (HELP dumped text to the command line, OPTIONS aliased the drafting-settings popup). The app-menu overlay is gone, so its rendering code, MENU_ITEMS, four dead messages, and the LOGO_RED / GEAR / HELP assets are removed. The recents list it held is the only survivor — moved into OpenCADStudio.recent_files via a new app/recent.rs, backing the Start page's Recent Documents panel. Quick-access icons are redrawn in a cleaner style (file-plus, folder-open, device-floppy, file-export, printer) and tinted light so they read on the dark strip; FOLDER / DOC keep their old glyphs for the file browser. Co-Authored-By: Claude Opus 4.8 --- assets/icons/ui/doc_new.svg | 2 +- assets/icons/ui/file_export.svg | 1 + assets/icons/ui/folder_open.svg | 1 + assets/icons/ui/print.svg | 2 +- assets/icons/ui/save.svg | 2 +- src/app/commands/display.rs | 6 +- src/app/commands/view.rs | 19 -- src/app/mod.rs | 18 +- src/app/recent.rs | 75 +++++ src/app/update/file.rs | 2 +- src/app/update/mod.rs | 20 +- src/app/view/mod.rs | 3 +- src/ui/app_menu.rs | 549 -------------------------------- src/ui/icons.rs | 4 +- src/ui/mod.rs | 2 - src/ui/ribbon/mod.rs | 38 +-- src/ui/ribbon/widgets.rs | 53 ++- 17 files changed, 154 insertions(+), 643 deletions(-) create mode 100644 assets/icons/ui/file_export.svg create mode 100644 assets/icons/ui/folder_open.svg create mode 100644 src/app/recent.rs delete mode 100644 src/ui/app_menu.rs diff --git a/assets/icons/ui/doc_new.svg b/assets/icons/ui/doc_new.svg index 4621403ae..98063622e 100644 --- a/assets/icons/ui/doc_new.svg +++ b/assets/icons/ui/doc_new.svg @@ -1 +1 @@ - + diff --git a/assets/icons/ui/file_export.svg b/assets/icons/ui/file_export.svg new file mode 100644 index 000000000..48f0cf07f --- /dev/null +++ b/assets/icons/ui/file_export.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/folder_open.svg b/assets/icons/ui/folder_open.svg new file mode 100644 index 000000000..c579f472c --- /dev/null +++ b/assets/icons/ui/folder_open.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/print.svg b/assets/icons/ui/print.svg index a638b027f..4913169eb 100644 --- a/assets/icons/ui/print.svg +++ b/assets/icons/ui/print.svg @@ -1 +1 @@ - + diff --git a/assets/icons/ui/save.svg b/assets/icons/ui/save.svg index 63c3b94e2..cb0a7b766 100644 --- a/assets/icons/ui/save.svg +++ b/assets/icons/ui/save.svg @@ -1 +1 @@ - + diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index 19172bf98..2023a59fb 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -154,9 +154,9 @@ impl OpenCADStudio { "POLAR" => { return Some(Task::done(Message::TogglePolar)); } - // DSETTINGS / OSNAP / OPTIONS — open the drafting-settings popup, which - // is OCS's settings surface (the persisted DYN/ORTHO/POLAR/OSNAP prefs). - "DSETTINGS" | "OSNAP" | "OPTIONS" | "OP" => { + // DSETTINGS / OSNAP — open the drafting-settings popup, which is OCS's + // settings surface (the persisted DYN/ORTHO/POLAR/OSNAP prefs). + "DSETTINGS" | "OSNAP" => { return Some(Task::done(Message::ToggleSnapPopup)); } // UNITS — open the drawing-units picker (linear / angular format). diff --git a/src/app/commands/view.rs b/src/app/commands/view.rs index 2eead7def..99890c779 100644 --- a/src/app/commands/view.rs +++ b/src/app/commands/view.rs @@ -3,25 +3,6 @@ use super::*; impl OpenCADStudio { pub(super) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option> { match cmd { - "HELP" | "?" => { - self.command_line.push_output( - "Draw: LINE CIRCLE ARC PLINE RECTANG(RECT) POLYGON(POLY) POINT ELLIPSE SPLINE RAY XLINE HATCH DONUT REVCLOUD WIPEOUT MLINE ATTDEF | \ - Modify: MOVE COPY ROTATE SCALE MIRROR ERASE OFFSET EXTEND FILLET CHAMFER STRETCH EXPLODE TRIM BREAK JOIN LENGTHEN ALIGN PEDIT | \ - Array: ARRAY ARRAYRECT ARRAYPOLAR ARRAYPATH | \ - Text: TEXT MTEXT LEADER MLEADER | \ - Dimension: DIMLINEAR DIMALIGNED DIMANGULAR DIMRADIUS DIMDIAMETER DIMCONTINUE DIMBASELINE | \ - Annotation: TOLERANCE | \ - Inquiry: DIST ID AREA LIST FIND FINDALL COUNT QSELECT | Draw on entity: DIVIDE MEASURE | \ - Attributes: ATTEDIT ATTDISP | \ - Utilities: FLATTEN LAYISO LAYUNISO | \ - View: ZOOM EXTENTS ZOOM WINDOW VIEW LIST/SAVE/RESTORE/DELETE | \ - Layer: LAYER LIST/NEW/ON/OFF/FREEZE/THAW/LOCK/UNLOCK/COLOR/SET | \ - Viewport: MVIEW VPLAYER VPORTS MS PS DRAWORDER | \ - Tables: STYLE DIMSTYLE LINETYPE UCS RENAME PURGE | \ - File: NEW OPEN SAVE SAVEAS PRINT PURGE UNDO REDO" - ); - } - "DONATE" => { crate::sys::open_url("https://patreon.com/HakanSeven12"); self.command_line.push_info("Opening Patreon page..."); diff --git a/src/app/mod.rs b/src/app/mod.rs index 50078d511..2f113558b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -14,6 +14,7 @@ mod mtext_editor; #[cfg(not(target_arch = "wasm32"))] pub mod plugin_host; mod properties; +mod recent; mod settings; mod style_ops; mod text_inline; @@ -155,7 +156,7 @@ pub struct QSelectState { pub append: bool, } use crate::snap::Snapper; -use crate::ui::{AppMenu, CommandLine, Ribbon, StatusBar}; +use crate::ui::{CommandLine, Ribbon, StatusBar}; use acadrust::types::{Color as AcadColor, LineWeight}; use acadrust::CadDocument; @@ -192,7 +193,8 @@ pub(super) struct OpenCADStudio { active_tab: usize, tab_counter: usize, ribbon: Ribbon, - app_menu: AppMenu, + /// Recently opened files, newest first — backs the Start page panel. + recent_files: Vec, command_line: CommandLine, /// Paying Patreon supporters shown on the Start page (name, pledge cents), /// fetched once at boot, highest pledge first. @@ -1121,12 +1123,6 @@ pub enum Message { command: String, path: Option, }, - // ── Application menu ────────────────────────────────────────────────── - ToggleAppMenu, - CloseAppMenu, - /// Close the menu and immediately dispatch a CAD command. - CloseAppMenuAndRun(String), - AppMenuSearch(String), // ── Document tabs ────────────────────────────────────────────────────── /// Create a new empty document tab. TabNew, @@ -1918,16 +1914,14 @@ impl OpenCADStudio { // Boot with only the Welcome/Start tab. The user creates drawings // explicitly (File → New); we never auto-spawn Drawing1. let start_tab = DocumentTab::new_start(); - let mut app_menu = AppMenu::new(); - // Restore recents from disk so the Start page lists them across runs. - app_menu.load_persistent_recents(); let mut app = Self { start: Instant::now(), tabs: vec![start_tab], active_tab: 0, tab_counter: 0, ribbon: Ribbon::new(), - app_menu, + // Restore recents from disk so the Start page lists them across runs. + recent_files: recent::load_recent_files(), command_line: CommandLine::new(), patrons: Vec::new(), history_content: iced::widget::text_editor::Content::new(), diff --git a/src/app/recent.rs b/src/app/recent.rs new file mode 100644 index 000000000..4933bad17 --- /dev/null +++ b/src/app/recent.rs @@ -0,0 +1,75 @@ +//! Persistent recent-files store backing the Start page's Recent Documents +//! panel. Stored as plain text on disk (one path per line, newest first) next +//! to the other per-user config, so no serialization crate is pulled in just +//! for this. + +use super::OpenCADStudio; +use std::path::{Path, PathBuf}; + +impl OpenCADStudio { + /// Record a freshly opened file at the top of the recents list. + pub(super) fn push_recent(&mut self, path: PathBuf) { + self.recent_files.retain(|r| r != &path); + self.recent_files.insert(0, path); + self.recent_files.truncate(20); + // Best-effort persist; silent on failure (read-only home, full disk). + let _ = save_recents(&self.recent_files); + } + + /// Drop a path from the recents list (manual removal from the Start page). + pub(super) fn remove_recent(&mut self, path: &Path) { + self.recent_files.retain(|r| r.as_path() != path); + let _ = save_recents(&self.recent_files); + } +} + +/// Rehydrate the recents list from disk. Call once at app boot. +pub(super) fn load_recent_files() -> Vec { + let Some(path) = recents_file_path() else { + return vec![]; + }; + let Ok(body) = std::fs::read_to_string(path) else { + return vec![]; + }; + body.lines() + .filter(|l| !l.trim().is_empty()) + .map(PathBuf::from) + .collect() +} + +fn recents_file_path() -> Option { + let base: PathBuf = if cfg!(target_os = "windows") { + std::env::var_os("APPDATA").map(PathBuf::from)? + } else if cfg!(target_os = "macos") { + let home = std::env::var_os("HOME")?; + let mut p = PathBuf::from(home); + p.push("Library"); + p.push("Application Support"); + p + } else if let Some(d) = std::env::var_os("XDG_CONFIG_HOME") { + PathBuf::from(d) + } else { + let home = std::env::var_os("HOME")?; + let mut p = PathBuf::from(home); + p.push(".config"); + p + }; + let mut p = base; + p.push("OpenCADStudio"); + Some(p.join("recent.txt")) +} + +fn save_recents(list: &[PathBuf]) -> std::io::Result<()> { + let Some(path) = recents_file_path() else { + return Ok(()); + }; + if let Some(dir) = path.parent() { + std::fs::create_dir_all(dir)?; + } + let body: String = list + .iter() + .map(|p| p.to_string_lossy().into_owned()) + .collect::>() + .join("\n"); + std::fs::write(path, body) +} diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 60313a52e..5b57c686d 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -240,7 +240,7 @@ pub(super) fn on_open_file(&mut self) -> Task { caches.corrupt_dropped )); } - self.app_menu.push_recent(path.clone()); + self.push_recent(path.clone()); let current_is_empty = { let t = &self.tabs[self.active_tab]; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index aebfc1c31..02c959ed4 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -243,7 +243,7 @@ impl OpenCADStudio { } Message::RecentRemove(path) => { - self.app_menu.remove_recent(&path); + self.remove_recent(&path); Task::none() } @@ -616,24 +616,6 @@ impl OpenCADStudio { Task::none() } - // ── Application menu ────────────────────────────────────────── - Message::ToggleAppMenu => { - self.app_menu.toggle(); - Task::none() - } - Message::CloseAppMenu => { - self.app_menu.close(); - Task::none() - } - Message::CloseAppMenuAndRun(cmd) => { - self.app_menu.close(); - self.dispatch_command(&cmd.clone()) - } - Message::AppMenuSearch(s) => { - self.app_menu.search = s; - Task::none() - } - // ── Document tabs ───────────────────────────────────────────── Message::TabNew => { self.tab_counter += 1; diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 1509e1501..8314d30a8 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1084,7 +1084,7 @@ impl OpenCADStudio { // Properties / layers panels carry no useful state on the Start tab. // Replace the properties panel with a Recent Documents list there. let properties_el: Element<'_, Message> = if tab.is_start { - recent_files_panel(&self.app_menu.recent) + recent_files_panel(&self.recent_files) } else if self.show_properties && !self.clean_screen { tab.properties.view() } else { @@ -1304,7 +1304,6 @@ impl OpenCADStudio { let composed = stack![ main_ui, - self.app_menu.view(), snap_layer, scale_layer, statusbar_menu_layer, diff --git a/src/ui/app_menu.rs b/src/ui/app_menu.rs deleted file mode 100644 index 22840ae87..000000000 --- a/src/ui/app_menu.rs +++ /dev/null @@ -1,549 +0,0 @@ -//! Application menu — OpenCADStudio-style dropdown that opens when the OCS logo -//! button is clicked. Overlays the entire viewport as a floating panel. -//! -//! Layout: -//! ┌─────────────────────────────────────────────┐ -//! │ [Search bar] │ -//! ├──────────────────┬──────────────────────────┤ -//! │ Command list │ Recent files │ -//! │ (left column) │ (right column) │ -//! └──────────────────┴──────────────────────────┘ - -use iced::widget::{button, column, container, row, scrollable, text, text_input}; -use iced::{Background, Border, Color, Element, Length, Theme}; -use std::path::PathBuf; - -use crate::app::Message; - -// ── Menu item definitions ───────────────────────────────────────────────── - -struct MenuItem { - icon: &'static [u8], - label: &'static str, - desc: &'static str, - command: &'static str, -} - -const MENU_ITEMS: &[MenuItem] = &[ - MenuItem { - icon: crate::ui::icons::DOC_NEW, - label: "New", - desc: "Create a new drawing", - command: "NEW", - }, - MenuItem { - icon: crate::ui::icons::FOLDER, - label: "Open", - desc: "Open an existing file", - command: "OPEN", - }, - MenuItem { - icon: crate::ui::icons::SAVE, - label: "Save", - desc: "Save the current drawing", - command: "SAVE", - }, - MenuItem { - icon: crate::ui::icons::DOC, - label: "Save As", - desc: "Save as DWG or DXF", - command: "SAVEAS", - }, - MenuItem { - icon: crate::ui::icons::PRINT, - label: "Print", - desc: "Print or plot the drawing", - command: "PRINT", - }, - MenuItem { - icon: crate::ui::icons::GEAR, - label: "Options", - desc: "Open application settings", - command: "OPTIONS", - }, - MenuItem { - icon: crate::ui::icons::HELP, - label: "Help", - desc: "Open help documentation", - command: "HELP", - }, -]; - -// ── Public state ────────────────────────────────────────────────────────── - -#[derive(Clone, Default)] -pub struct AppMenu { - pub open: bool, - pub search: String, - /// Recently opened file paths (most recent first). - pub recent: Vec, -} - -impl AppMenu { - pub fn new() -> Self { - Self { - open: false, - search: String::new(), - recent: vec![], - } - } - - /// Call when a file is successfully loaded to push its path into recents. - pub fn push_recent(&mut self, path: PathBuf) { - self.recent.retain(|r| r != &path); - self.recent.insert(0, path); - self.recent.truncate(20); - // Best-effort persist; silent on failure (read-only home, full disk). - let _ = save_recents(&self.recent); - } - - /// Drop a path from the recents list (manual removal from Start page). - pub fn remove_recent(&mut self, path: &std::path::Path) { - self.recent.retain(|r| r.as_path() != path); - let _ = save_recents(&self.recent); - } - - /// Rehydrate the recents list from disk. Call once at app boot. - pub fn load_persistent_recents(&mut self) { - self.recent = load_recents(); - } - - pub fn toggle(&mut self) { - self.open = !self.open; - if self.open { - self.search.clear(); - } - } - - pub fn close(&mut self) { - self.open = false; - self.search.clear(); - } - - // ── View ───────────────────────────────────────────────────────────── - - pub fn view(&self) -> Element<'_, Message> { - if !self.open { - return container(text("")).width(0).height(0).into(); - } - - // Filter items by search query. - let query = self.search.to_lowercase(); - let items: Vec<&MenuItem> = MENU_ITEMS - .iter() - .filter(|m| { - query.is_empty() - || m.label.to_lowercase().contains(&query) - || m.desc.to_lowercase().contains(&query) - }) - .collect(); - - // ── Search bar ──────────────────────────────────────────────────── - let search = container( - text_input("Search commands...", &self.search) - .on_input(Message::AppMenuSearch) - .style(|_: &Theme, _| iced::widget::text_input::Style { - background: Background::Color(SEARCH_BG), - border: Border { - color: ACCENT, - width: 1.0, - radius: 2.0.into(), - }, - icon: Color::WHITE, - placeholder: Color { - r: 0.45, - g: 0.45, - b: 0.45, - a: 1.0, - }, - value: Color::WHITE, - selection: Color { - r: 0.20, - g: 0.44, - b: 0.72, - a: 0.5, - }, - }) - .size(12) - .padding([6, 10]), - ) - .padding([8, 10]) - .width(Length::Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(HEADER_BG)), - border: Border { - color: BORDER, - width: 0.0, - radius: 0.0.into(), - }, - ..Default::default() - }); - - // ── Left column — command list ───────────────────────────────────── - let left: Element<'_, Message> = if items.is_empty() { - container(text("No results").size(11).color(DIM_COLOR)) - .padding([12, 14]) - .into() - } else { - items - .iter() - .fold(column![].spacing(0), |col, item| { - col.push(menu_item_btn(item)) - }) - .into() - }; - - let left_col = container(scrollable(left)) - .width(220) - .height(Length::Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LEFT_BG)), - ..Default::default() - }); - - // ── Right column — recent files ─────────────────────────────────── - let recent_header = - container(text("Recent Documents").size(10).color(DIM_COLOR)).padding(iced::Padding { - top: 10.0, - right: 14.0, - bottom: 6.0, - left: 14.0, - }); - - let recent_list = if self.recent.is_empty() { - column![container(text("No recent files").size(11).color(DIM_COLOR)).padding([8, 14])] - } else { - self.recent.iter().fold(column![].spacing(0), |col, path| { - col.push(recent_item_btn(path)) - }) - }; - - let right_col = container(column![recent_header, scrollable(recent_list)]) - .width(Length::Fill) - .height(Length::Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(RIGHT_BG)), - border: Border { - color: BORDER, - width: 1.0, - radius: 0.0.into(), - }, - ..Default::default() - }); - - // ── Bottom bar — Exit button ─────────────────────────────────────── - let bottom = container( - row![ - iced::widget::Space::new().width(Length::Fill), - button(text("Exit Open CAD Studio").size(11).color(Color::WHITE)) - .on_press(Message::Command("EXIT".into())) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => Color { - r: 0.65, - g: 0.12, - b: 0.12, - a: 1.0 - }, - button::Status::Pressed => Color { - r: 0.45, - g: 0.08, - b: 0.08, - a: 1.0 - }, - _ => EXIT_BTN, - })), - text_color: Color::WHITE, - border: Border { - color: Color::TRANSPARENT, - width: 0.0, - radius: 2.0.into() - }, - shadow: iced::Shadow::default(), - snap: false, - }) - .padding([5, 14]), - ] - .align_y(iced::Center), - ) - .padding([6, 10]) - .width(Length::Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BOTTOM_BG)), - border: Border { - color: BORDER, - width: 1.0, - radius: 0.0.into(), - }, - ..Default::default() - }); - - // ── Outer panel ─────────────────────────────────────────────────── - let panel = container(column![ - search, - container(row![left_col, right_col]).height(Length::Fill), - bottom, - ]) - .width(520) - .height(420) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { - color: BORDER, - width: 1.0, - radius: 0.0.into(), - }, - ..Default::default() - }); - - // Backdrop — clicking outside closes the menu. - let backdrop = button( - container(panel) - // Offset so the panel sits just below the ribbon OCS button. - .padding(iced::Padding { - top: 56.0, - left: 0.0, - right: 0.0, - bottom: 0.0, - }) - .width(Length::Fill) - .height(Length::Fill), - ) - .on_press(Message::CloseAppMenu) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.55, - })), - text_color: Color::TRANSPARENT, - border: Border::default(), - shadow: iced::Shadow::default(), - snap: false, - }) - .padding(0) - .width(Length::Fill) - .height(Length::Fill) - .into(); - - backdrop - } -} - -// ── Item widgets ────────────────────────────────────────────────────────── - -fn menu_item_btn<'a>(item: &'a MenuItem) -> Element<'a, Message> { - button( - row![ - container(crate::ui::icons::tinted(item.icon, 16.0, ICON_COLOR)) - .width(32) - .padding([0, 4]), - column![ - text(item.label).size(12).color(Color::WHITE), - text(item.desc).size(10).color(DIM_COLOR), - ] - .spacing(1), - ] - .align_y(iced::Center) - .spacing(4), - ) - .on_press(Message::CloseAppMenuAndRun(item.command.into())) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => ITEM_HOVER, - button::Status::Pressed => ITEM_ACTIVE, - _ => Color::TRANSPARENT, - })), - text_color: Color::WHITE, - border: Border { - color: Color::TRANSPARENT, - width: 0.0, - radius: 0.0.into(), - }, - shadow: iced::Shadow::default(), - snap: false, - }) - .padding([7, 10]) - .width(Length::Fill) - .into() -} - -fn recent_item_btn(path: &PathBuf) -> Element<'_, Message> { - let name = path - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| path.display().to_string()); - let type_label = match path - .extension() - .map(|e| e.to_string_lossy().to_uppercase()) - .as_deref() - { - Some("DWG") => "DWG Drawing", - Some("DXF") => "DXF Drawing", - _ => "CAD File", - }; - let path_str = path.display().to_string(); - button( - row![ - container(crate::ui::icons::tinted(crate::ui::icons::DOC, 13.0, ICON_COLOR)) - .width(24), - column![ - text(name).size(11).color(Color::WHITE), - text(type_label).size(9).color(DIM_COLOR), - ] - .spacing(1), - ] - .align_y(iced::Center) - .spacing(6), - ) - .on_press(Message::CloseAppMenuAndRun(format!( - "OPEN_RECENT:{path_str}" - ))) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => ITEM_HOVER, - button::Status::Pressed => ITEM_ACTIVE, - _ => Color::TRANSPARENT, - })), - text_color: Color::WHITE, - border: Border { - color: Color::TRANSPARENT, - width: 0.0, - radius: 0.0.into(), - }, - shadow: iced::Shadow::default(), - snap: false, - }) - .padding([6, 14]) - .width(Length::Fill) - .into() -} - -// ── Colors ──────────────────────────────────────────────────────────────── - -const PANEL_BG: Color = Color { - r: 0.20, - g: 0.20, - b: 0.20, - a: 1.0, -}; -const HEADER_BG: Color = Color { - r: 0.17, - g: 0.17, - b: 0.17, - a: 1.0, -}; -const LEFT_BG: Color = Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, -}; -const RIGHT_BG: Color = Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, -}; -const BOTTOM_BG: Color = Color { - r: 0.16, - g: 0.16, - b: 0.16, - a: 1.0, -}; -const SEARCH_BG: Color = Color { - r: 0.14, - g: 0.14, - b: 0.14, - a: 1.0, -}; -const BORDER: Color = Color { - r: 0.30, - g: 0.30, - b: 0.30, - a: 1.0, -}; -const ITEM_HOVER: Color = Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, -}; -const ITEM_ACTIVE: Color = Color { - r: 0.18, - g: 0.42, - b: 0.70, - a: 1.0, -}; -const DIM_COLOR: Color = Color { - r: 0.50, - g: 0.50, - b: 0.50, - a: 1.0, -}; -const ICON_COLOR: Color = Color { - r: 0.70, - g: 0.80, - b: 0.95, - a: 1.0, -}; -const ACCENT: Color = Color { - r: 0.20, - g: 0.55, - b: 0.90, - a: 1.0, -}; -const EXIT_BTN: Color = Color { - r: 0.55, - g: 0.10, - b: 0.10, - a: 1.0, -}; - -// ── Recent-files persistence ───────────────────────────────────────────── -// -// Plain-text format, one path per line, newest first. Lives next to other -// per-user config so we don't pull in a TOML/JSON crate just for this. - -fn recents_file_path() -> Option { - let base: PathBuf = if cfg!(target_os = "windows") { - std::env::var_os("APPDATA").map(PathBuf::from)? - } else if cfg!(target_os = "macos") { - let home = std::env::var_os("HOME")?; - let mut p = PathBuf::from(home); - p.push("Library"); - p.push("Application Support"); - p - } else if let Some(d) = std::env::var_os("XDG_CONFIG_HOME") { - PathBuf::from(d) - } else { - let home = std::env::var_os("HOME")?; - let mut p = PathBuf::from(home); - p.push(".config"); - p - }; - let mut p = base; - p.push("OpenCADStudio"); - Some(p.join("recent.txt")) -} - -fn save_recents(list: &[PathBuf]) -> std::io::Result<()> { - let Some(path) = recents_file_path() else { return Ok(()); }; - if let Some(dir) = path.parent() { std::fs::create_dir_all(dir)?; } - let body: String = list - .iter() - .map(|p| p.to_string_lossy().into_owned()) - .collect::>() - .join("\n"); - std::fs::write(path, body) -} - -fn load_recents() -> Vec { - let Some(path) = recents_file_path() else { return vec![]; }; - let Ok(body) = std::fs::read_to_string(path) else { return vec![]; }; - body.lines() - .filter(|l| !l.trim().is_empty()) - .map(PathBuf::from) - .collect() -} diff --git a/src/ui/icons.rs b/src/ui/icons.rs index cea50277d..c4c04067b 100644 --- a/src/ui/icons.rs +++ b/src/ui/icons.rs @@ -60,10 +60,10 @@ pub const UP: &[u8] = include_bytes!("../../assets/icons/ui/up.svg"); pub const DOC_NEW: &[u8] = include_bytes!("../../assets/icons/ui/doc_new.svg"); pub const DOC: &[u8] = include_bytes!("../../assets/icons/ui/doc.svg"); pub const FOLDER: &[u8] = include_bytes!("../../assets/icons/ui/folder.svg"); +pub const FOLDER_OPEN: &[u8] = include_bytes!("../../assets/icons/ui/folder_open.svg"); pub const SAVE: &[u8] = include_bytes!("../../assets/icons/ui/save.svg"); +pub const FILE_EXPORT: &[u8] = include_bytes!("../../assets/icons/ui/file_export.svg"); pub const PRINT: &[u8] = include_bytes!("../../assets/icons/ui/print.svg"); -pub const GEAR: &[u8] = include_bytes!("../../assets/icons/ui/gear.svg"); -pub const HELP: &[u8] = include_bytes!("../../assets/icons/ui/help.svg"); pub const HEART: &[u8] = include_bytes!("../../assets/icons/ui/heart.svg"); pub const DOT: &[u8] = include_bytes!("../../assets/icons/ui/dot.svg"); pub const TRI_LEFT_B: &[u8] = include_bytes!("../../assets/icons/ui/tri_left.svg"); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 45c402d07..19da65675 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,7 +2,6 @@ /// Change this to scale the ribbon, layer manager rows, and property panel rows uniformly. pub const ROW_H: f32 = 26.0; -pub mod app_menu; pub mod color_select; pub mod command_line; pub mod icons; @@ -17,7 +16,6 @@ pub mod style; pub mod text_util; pub mod window; -pub use app_menu::AppMenu; pub use command_line::CommandLine; pub use properties::PropertiesPanel; pub use ribbon::Ribbon; diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index b55229fce..cbcb6bea5 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -217,32 +217,16 @@ impl Ribbon { undo_count: usize, redo_count: usize, ) -> Element<'_, Message> { - // ── Logo ────────────────────────────────────────────────────────── - let logo_svg = { - let handle = svg::Handle::from_memory(include_bytes!("../../../assets/logo.svg")); - svg(handle).width(30).height(28) - }; - let logo = button(logo_svg) - .on_press(Message::ToggleAppMenu) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => Color { - r: 0.80, - g: 0.25, - b: 0.15, - a: 1.0, - }, - _ => LOGO_RED, - })), - border: Border { - radius: 2.0.into(), - ..Default::default() - }, - shadow: iced::Shadow::default(), - snap: false, - ..Default::default() - }) - .padding([0, 4]); + // ── Quick-access file commands ───────────────────────────────────── + let quick_access = row![ + quick_access_btn(crate::ui::icons::DOC_NEW, "New", "NEW"), + quick_access_btn(crate::ui::icons::FOLDER_OPEN, "Open", "OPEN"), + quick_access_btn(crate::ui::icons::SAVE, "Save", "SAVE"), + quick_access_btn(crate::ui::icons::FILE_EXPORT, "Save As", "SAVEAS"), + quick_access_btn(crate::ui::icons::PRINT, "Print", "PRINT"), + ] + .spacing(TOP_HIST_GAP) + .align_y(iced::Center); // ── Tab buttons ─────────────────────────────────────────────────── let history_controls = row![ @@ -253,7 +237,7 @@ impl Ribbon { .align_y(iced::Center); let tab_buttons = self.modules.iter().enumerate().fold( - row![logo, history_controls] + row![quick_access, history_controls] .align_y(iced::Center) .spacing(6), |row_acc, (i, module)| { diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index bcc0a99e3..07ef34feb 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -54,10 +54,11 @@ pub(super) const PROP_LW_ID: &str = "PROP_LW"; // ── Colours ──────────────────────────────────────────────────────────────── -pub(super) const LOGO_RED: Color = Color { - r: 0.75, - g: 0.10, - b: 0.10, +/// Light chrome grey for the quick-access file-command icons on the top strip. +pub(super) const QA_ICON_COLOR: Color = Color { + r: 0.82, + g: 0.83, + b: 0.85, a: 1.0, }; pub(super) const TOPBAR_BG: Color = Color { @@ -1082,6 +1083,50 @@ pub fn module_event_to_message(event: ModuleEvent) -> Message { // ── History control ──────────────────────────────────────────────────────── +/// Quick-access chrome button (New / Open / Save / Save As / Print) in the top +/// strip: an SVG icon that dispatches a command string, with a hover tooltip. +pub(super) fn quick_access_btn<'a>( + icon_bytes: &'static [u8], + label: &'static str, + cmd: &'static str, +) -> Element<'a, Message> { + // The bundled UI SVGs are black-stroked; tint them to a light chrome grey so + // they read on the dark top strip (raw black is invisible there). + let icon = icons::tinted(icon_bytes, 16.0, QA_ICON_COLOR); + let btn = button( + container(icon) + .width(Fill) + .height(Fill) + .align_x(iced::Center) + .align_y(iced::Center), + ) + .on_press(Message::Command(cmd.to_string())) + .style(|_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered | button::Status::Pressed => Color { + r: 0.30, + g: 0.30, + b: 0.30, + a: 1.0, + }, + _ => Color::TRANSPARENT, + })), + border: Border { + radius: 2.0.into(), + ..Default::default() + }, + ..Default::default() + }) + .width(Length::Fixed(TOP_HIST_W)) + .height(24) + .padding([2, 0]); + tooltip(btn, make_tip(label.to_string()), TipPos::Bottom) + .gap(6.0) + .delay(Duration::from_millis(400)) + .style(tip_style) + .into() +} + pub(super) fn render_history_control<'a>( label: &'static str, dropdown_id: &'static str,