feat(ui): replace app-menu logo with a quick-access file toolbar
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 <[email protected]>
No files matched your search
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3 H14 L19 8 V21 H6 Z M14 3 V8 H19"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/><path d="M12 11v6"/><path d="M9 14h6"/></svg>
|
||||
|
Before Width: | Height: | Size: 210 B After Width: | Height: | Size: 316 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M11.5 21h-4.5a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v5"/><path d="M13 19h7"/><path d="M17 16l3 3l-3 3"/></svg>
|
||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19l2.757 -7.351a1 1 0 0 1 .936 -.649h12.307a1 1 0 0 1 .986 1.164l-.996 5.211a2 2 0 0 1 -1.964 1.625h-14.03a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v2"/></svg>
|
||||
|
After Width: | Height: | Size: 340 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M7 9 V3 H17 V9 M7 17 H4 V10 a1 1 0 0 1 1-1 H19 a1 1 0 0 1 1 1 V17 H17 M7 14 H17 V21 H7 Z M16 12 H17"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 17h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-14a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h2"/><path d="M17 9v-4a2 2 0 0 0 -2 -2h-6a2 2 0 0 0 -2 2v4"/><path d="M9 13h6a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 273 B After Width: | Height: | Size: 395 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4 H17 L20 7 V20 H4 Z M8 4 V9 H15 V4 M7 20 V14 H17 V20"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"/><path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/><path d="M14 4v4h-6v-4"/></svg>
|
||||
|
Before Width: | Height: | Size: 230 B After Width: | Height: | Size: 317 B |
@@ -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).
|
||||
|
||||
@@ -3,25 +3,6 @@ use super::*;
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
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...");
|
||||
|
||||
@@ -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<std::path::PathBuf>,
|
||||
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<std::path::PathBuf>,
|
||||
},
|
||||
// ── 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(),
|
||||
|
||||
@@ -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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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::<Vec<_>>()
|
||||
.join("\n");
|
||||
std::fs::write(path, body)
|
||||
}
|
||||
@@ -240,7 +240,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<PathBuf>,
|
||||
}
|
||||
|
||||
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<PathBuf> {
|
||||
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::<Vec<_>>()
|
||||
.join("\n");
|
||||
std::fs::write(path, body)
|
||||
}
|
||||
|
||||
fn load_recents() -> Vec<PathBuf> {
|
||||
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()
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)| {
|
||||
|
||||
@@ -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,
|
||||
|
||||