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,