From 864dd6cef79a653976b0a7fe279f5f6c70e8a3b1 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 18 Jun 2026 12:29:32 +0300 Subject: [PATCH] fix(web): render UI icons as SVG instead of font glyphs UI chrome (dropdown carets, undo/redo, checkmarks, close/menu/osnap, layer/file-menu icons) and ribbon tool icons were drawn as Unicode glyphs. Desktop fills missing glyphs via system-font fallback, but the web build bundles only Fira Sans with no fallback, so they rendered as empty tofu boxes (issue #116). Add a shared src/ui/icons.rs (SVG-backed, tintable) plus bundled assets, and switch every glyph site and IconKind::Glyph tool def to SVG. Co-Authored-By: Claude Opus 4.8 --- assets/icons/add_scale.svg | 4 + assets/icons/box3d.svg | 6 ++ assets/icons/bylayer.svg | 5 + assets/icons/clear_tool.svg | 6 ++ assets/icons/color_palette.svg | 5 + assets/icons/cone3d.svg | 4 + assets/icons/cylinder3d.svg | 5 + assets/icons/import_obj.svg | 4 + assets/icons/intersect.svg | 5 + assets/icons/options_tool.svg | 4 + assets/icons/pagesetup.svg | 4 + assets/icons/plot.svg | 5 + assets/icons/scale_list.svg | 4 + assets/icons/sphere3d.svg | 5 + assets/icons/subtract.svg | 4 + assets/icons/sync.svg | 4 + assets/icons/torus3d.svg | 4 + assets/icons/ui/arrow_long_right.svg | 1 + assets/icons/ui/bolt.svg | 1 + assets/icons/ui/check.svg | 1 + assets/icons/ui/close.svg | 1 + assets/icons/ui/doc.svg | 1 + assets/icons/ui/doc_new.svg | 1 + assets/icons/ui/dot.svg | 1 + assets/icons/ui/folder.svg | 1 + assets/icons/ui/gear.svg | 1 + assets/icons/ui/heart.svg | 1 + assets/icons/ui/help.svg | 1 + assets/icons/ui/menu.svg | 1 + assets/icons/ui/move.svg | 1 + assets/icons/ui/plus.svg | 1 + assets/icons/ui/print.svg | 1 + assets/icons/ui/redo.svg | 43 ++++++++ assets/icons/ui/save.svg | 1 + assets/icons/ui/split_h.svg | 1 + assets/icons/ui/split_v.svg | 1 + assets/icons/ui/trash.svg | 1 + assets/icons/ui/tri_down.svg | 1 + assets/icons/ui/tri_left.svg | 1 + assets/icons/ui/tri_right.svg | 1 + assets/icons/ui/tri_up.svg | 1 + assets/icons/ui/undo.svg | 43 ++++++++ assets/icons/ui/up.svg | 1 + assets/icons/union.svg | 4 + assets/icons/viewport.svg | 4 + assets/icons/wedge3d.svg | 6 ++ src/app/view.rs | 127 ++++++++++++++++------ src/modules/annotate/mod.rs | 16 ++- src/modules/draw/layers/color.rs | 2 +- src/modules/draw/properties/bylayer.rs | 2 +- src/modules/insert/box_prim.rs | 2 +- src/modules/insert/clear.rs | 2 +- src/modules/insert/cylinder.rs | 2 +- src/modules/insert/open_obj.rs | 2 +- src/modules/insert/sphere.rs | 2 +- src/modules/layout/mod.rs | 8 +- src/modules/layout/mview.rs | 2 +- src/modules/manage/mview.rs | 2 +- src/modules/manage/options.rs | 2 +- src/modules/model/mod.rs | 34 +++--- src/ui/app_menu.rs | 21 ++-- src/ui/color_select.rs | 2 +- src/ui/command_line.rs | 14 +-- src/ui/icons.rs | 142 ++++++++++++++++++++++++ src/ui/layers.rs | 94 +++++++++------- src/ui/layout_manager.rs | 49 ++++++--- src/ui/mod.rs | 1 + src/ui/modal.rs | 21 ++-- src/ui/page_setup.rs | 12 +-- src/ui/plugin_manager.rs | 27 ++++- src/ui/properties.rs | 15 +-- src/ui/ribbon/mod.rs | 122 ++++++--------------- src/ui/ribbon/widgets.rs | 143 +++++++++++-------------- src/ui/scale_popup.rs | 9 +- src/ui/selection_filter_popup.rs | 5 +- src/ui/snap_popup.rs | 9 +- src/ui/statusbar.rs | 33 ++++-- src/ui/statusbar_menu.rs | 5 +- src/ui/style_list.rs | 8 +- src/ui/units_popup.rs | 5 +- src/ui/update_notice.rs | 10 +- 81 files changed, 782 insertions(+), 372 deletions(-) create mode 100644 assets/icons/add_scale.svg create mode 100644 assets/icons/box3d.svg create mode 100644 assets/icons/bylayer.svg create mode 100644 assets/icons/clear_tool.svg create mode 100644 assets/icons/color_palette.svg create mode 100644 assets/icons/cone3d.svg create mode 100644 assets/icons/cylinder3d.svg create mode 100644 assets/icons/import_obj.svg create mode 100644 assets/icons/intersect.svg create mode 100644 assets/icons/options_tool.svg create mode 100644 assets/icons/pagesetup.svg create mode 100644 assets/icons/plot.svg create mode 100644 assets/icons/scale_list.svg create mode 100644 assets/icons/sphere3d.svg create mode 100644 assets/icons/subtract.svg create mode 100644 assets/icons/sync.svg create mode 100644 assets/icons/torus3d.svg create mode 100644 assets/icons/ui/arrow_long_right.svg create mode 100644 assets/icons/ui/bolt.svg create mode 100644 assets/icons/ui/check.svg create mode 100644 assets/icons/ui/close.svg create mode 100644 assets/icons/ui/doc.svg create mode 100644 assets/icons/ui/doc_new.svg create mode 100644 assets/icons/ui/dot.svg create mode 100644 assets/icons/ui/folder.svg create mode 100644 assets/icons/ui/gear.svg create mode 100644 assets/icons/ui/heart.svg create mode 100644 assets/icons/ui/help.svg create mode 100644 assets/icons/ui/menu.svg create mode 100644 assets/icons/ui/move.svg create mode 100644 assets/icons/ui/plus.svg create mode 100644 assets/icons/ui/print.svg create mode 100644 assets/icons/ui/redo.svg create mode 100644 assets/icons/ui/save.svg create mode 100644 assets/icons/ui/split_h.svg create mode 100644 assets/icons/ui/split_v.svg create mode 100644 assets/icons/ui/trash.svg create mode 100644 assets/icons/ui/tri_down.svg create mode 100644 assets/icons/ui/tri_left.svg create mode 100644 assets/icons/ui/tri_right.svg create mode 100644 assets/icons/ui/tri_up.svg create mode 100644 assets/icons/ui/undo.svg create mode 100644 assets/icons/ui/up.svg create mode 100644 assets/icons/union.svg create mode 100644 assets/icons/viewport.svg create mode 100644 assets/icons/wedge3d.svg create mode 100644 src/ui/icons.rs diff --git a/assets/icons/add_scale.svg b/assets/icons/add_scale.svg new file mode 100644 index 000000000..d5a9aa869 --- /dev/null +++ b/assets/icons/add_scale.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/box3d.svg b/assets/icons/box3d.svg new file mode 100644 index 000000000..5d2e02e43 --- /dev/null +++ b/assets/icons/box3d.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/bylayer.svg b/assets/icons/bylayer.svg new file mode 100644 index 000000000..2d9f9e6f4 --- /dev/null +++ b/assets/icons/bylayer.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/clear_tool.svg b/assets/icons/clear_tool.svg new file mode 100644 index 000000000..4c3f4cad1 --- /dev/null +++ b/assets/icons/clear_tool.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/color_palette.svg b/assets/icons/color_palette.svg new file mode 100644 index 000000000..83e030924 --- /dev/null +++ b/assets/icons/color_palette.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/cone3d.svg b/assets/icons/cone3d.svg new file mode 100644 index 000000000..786be248c --- /dev/null +++ b/assets/icons/cone3d.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/cylinder3d.svg b/assets/icons/cylinder3d.svg new file mode 100644 index 000000000..971b7470a --- /dev/null +++ b/assets/icons/cylinder3d.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/import_obj.svg b/assets/icons/import_obj.svg new file mode 100644 index 000000000..0f33211ea --- /dev/null +++ b/assets/icons/import_obj.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/intersect.svg b/assets/icons/intersect.svg new file mode 100644 index 000000000..cb8d30eea --- /dev/null +++ b/assets/icons/intersect.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/options_tool.svg b/assets/icons/options_tool.svg new file mode 100644 index 000000000..00aacd91e --- /dev/null +++ b/assets/icons/options_tool.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/pagesetup.svg b/assets/icons/pagesetup.svg new file mode 100644 index 000000000..670071847 --- /dev/null +++ b/assets/icons/pagesetup.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/plot.svg b/assets/icons/plot.svg new file mode 100644 index 000000000..6919dfb54 --- /dev/null +++ b/assets/icons/plot.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/scale_list.svg b/assets/icons/scale_list.svg new file mode 100644 index 000000000..f69a11745 --- /dev/null +++ b/assets/icons/scale_list.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/sphere3d.svg b/assets/icons/sphere3d.svg new file mode 100644 index 000000000..e31784e6c --- /dev/null +++ b/assets/icons/sphere3d.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/subtract.svg b/assets/icons/subtract.svg new file mode 100644 index 000000000..467707538 --- /dev/null +++ b/assets/icons/subtract.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/sync.svg b/assets/icons/sync.svg new file mode 100644 index 000000000..a2862207e --- /dev/null +++ b/assets/icons/sync.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/torus3d.svg b/assets/icons/torus3d.svg new file mode 100644 index 000000000..58d5e55dc --- /dev/null +++ b/assets/icons/torus3d.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ui/arrow_long_right.svg b/assets/icons/ui/arrow_long_right.svg new file mode 100644 index 000000000..bb1c6af58 --- /dev/null +++ b/assets/icons/ui/arrow_long_right.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/bolt.svg b/assets/icons/ui/bolt.svg new file mode 100644 index 000000000..8a15b6db5 --- /dev/null +++ b/assets/icons/ui/bolt.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/check.svg b/assets/icons/ui/check.svg new file mode 100644 index 000000000..4b591b1ea --- /dev/null +++ b/assets/icons/ui/check.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/close.svg b/assets/icons/ui/close.svg new file mode 100644 index 000000000..2c99b30d3 --- /dev/null +++ b/assets/icons/ui/close.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/doc.svg b/assets/icons/ui/doc.svg new file mode 100644 index 000000000..80125d9c6 --- /dev/null +++ b/assets/icons/ui/doc.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/doc_new.svg b/assets/icons/ui/doc_new.svg new file mode 100644 index 000000000..4621403ae --- /dev/null +++ b/assets/icons/ui/doc_new.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/dot.svg b/assets/icons/ui/dot.svg new file mode 100644 index 000000000..7bd84fbda --- /dev/null +++ b/assets/icons/ui/dot.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/folder.svg b/assets/icons/ui/folder.svg new file mode 100644 index 000000000..e3070d521 --- /dev/null +++ b/assets/icons/ui/folder.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/gear.svg b/assets/icons/ui/gear.svg new file mode 100644 index 000000000..fefd22e21 --- /dev/null +++ b/assets/icons/ui/gear.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/heart.svg b/assets/icons/ui/heart.svg new file mode 100644 index 000000000..cf6eee6ae --- /dev/null +++ b/assets/icons/ui/heart.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/help.svg b/assets/icons/ui/help.svg new file mode 100644 index 000000000..6d780c085 --- /dev/null +++ b/assets/icons/ui/help.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/menu.svg b/assets/icons/ui/menu.svg new file mode 100644 index 000000000..4e35c167e --- /dev/null +++ b/assets/icons/ui/menu.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/move.svg b/assets/icons/ui/move.svg new file mode 100644 index 000000000..a9a0a99c1 --- /dev/null +++ b/assets/icons/ui/move.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/plus.svg b/assets/icons/ui/plus.svg new file mode 100644 index 000000000..da0f8d59e --- /dev/null +++ b/assets/icons/ui/plus.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/print.svg b/assets/icons/ui/print.svg new file mode 100644 index 000000000..a638b027f --- /dev/null +++ b/assets/icons/ui/print.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/redo.svg b/assets/icons/ui/redo.svg new file mode 100644 index 000000000..5c18e6988 --- /dev/null +++ b/assets/icons/ui/redo.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/assets/icons/ui/save.svg b/assets/icons/ui/save.svg new file mode 100644 index 000000000..63c3b94e2 --- /dev/null +++ b/assets/icons/ui/save.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/split_h.svg b/assets/icons/ui/split_h.svg new file mode 100644 index 000000000..3255bbd52 --- /dev/null +++ b/assets/icons/ui/split_h.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/split_v.svg b/assets/icons/ui/split_v.svg new file mode 100644 index 000000000..1535d053f --- /dev/null +++ b/assets/icons/ui/split_v.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/trash.svg b/assets/icons/ui/trash.svg new file mode 100644 index 000000000..4f8ba53dc --- /dev/null +++ b/assets/icons/ui/trash.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/tri_down.svg b/assets/icons/ui/tri_down.svg new file mode 100644 index 000000000..3a249b0f7 --- /dev/null +++ b/assets/icons/ui/tri_down.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/tri_left.svg b/assets/icons/ui/tri_left.svg new file mode 100644 index 000000000..ed0e987d0 --- /dev/null +++ b/assets/icons/ui/tri_left.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/tri_right.svg b/assets/icons/ui/tri_right.svg new file mode 100644 index 000000000..247aeed96 --- /dev/null +++ b/assets/icons/ui/tri_right.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/tri_up.svg b/assets/icons/ui/tri_up.svg new file mode 100644 index 000000000..3f21b0174 --- /dev/null +++ b/assets/icons/ui/tri_up.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/undo.svg b/assets/icons/ui/undo.svg new file mode 100644 index 000000000..32f020142 --- /dev/null +++ b/assets/icons/ui/undo.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/assets/icons/ui/up.svg b/assets/icons/ui/up.svg new file mode 100644 index 000000000..b1e7b4521 --- /dev/null +++ b/assets/icons/ui/up.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/union.svg b/assets/icons/union.svg new file mode 100644 index 000000000..fcee8244c --- /dev/null +++ b/assets/icons/union.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/viewport.svg b/assets/icons/viewport.svg new file mode 100644 index 000000000..4f19585c0 --- /dev/null +++ b/assets/icons/viewport.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/wedge3d.svg b/assets/icons/wedge3d.svg new file mode 100644 index 000000000..753610968 --- /dev/null +++ b/assets/icons/wedge3d.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/app/view.rs b/src/app/view.rs index ffd5220e4..d792efc03 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -367,13 +367,17 @@ impl OpenCADStudio { if !is_paper && !tab.is_start { // Render-mode picker + viewport-split buttons (horizontal / // vertical divider of the active Model tile). - let split_btn = |glyph: &'static str, horizontal: bool| { - button(text(glyph).size(13).color(Color { - r: 0.85, - g: 0.85, - b: 0.85, - a: 1.0, - })) + let split_btn = |bytes: &'static [u8], horizontal: bool| { + button(crate::ui::icons::tinted( + bytes, + 14.0, + Color { + r: 0.85, + g: 0.85, + b: 0.85, + a: 1.0, + }, + )) .on_press(Message::SplitModelViewport(horizontal)) .padding([4, 8]) .style(|_: &Theme, status| iced::widget::button::Style { @@ -410,7 +414,12 @@ impl OpenCADStudio { ..Default::default() }) }; - let bar = row![info, split_btn("▤", true), split_btn("▥", false)].spacing(4); + let bar = row![ + info, + split_btn(crate::ui::icons::SPLIT_H, true), + split_btn(crate::ui::icons::SPLIT_V, false) + ] + .spacing(4); // Position the bar at the active model tile's top-left corner so // it follows the active panel in a tiled layout (full canvas when // a single tile fills the window). Leading Spaces offset it. @@ -636,9 +645,20 @@ impl OpenCADStudio { let mut col = column![].spacing(0).width(iced::Length::Fixed(row_w)); for (idx, name) in popup.items.iter().enumerate() { let is_cur = popup.current == Some(idx); - let label = format!("{} {}", if is_cur { "✓" } else { " " }, name); - let btn = button(text(label).size(12).color(Color::WHITE)) - .on_press(Message::VisibilityPick(idx)) + let mark: Element<'_, Message> = if is_cur { + crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, Color::WHITE) + } else { + Space::new().width(11).into() + }; + let btn = button( + row![ + container(mark).width(16), + text(name).size(12).color(Color::WHITE), + ] + .spacing(2) + .align_y(iced::Center), + ) + .on_press(Message::VisibilityPick(idx)) .padding([3, 10]) .width(Fill) .style(move |_: &Theme, status| iced::widget::button::Style { @@ -1851,13 +1871,28 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele for (idx, tab) in tabs.iter().enumerate() { let is_active = idx == active_tab; let name = crate::ui::text_util::elide(&tab.tab_display_name(), 24); - let label = if tab.dirty { - format!("● {}", name) + let title_inner: Element<'_, Message> = if tab.dirty { + row![ + crate::ui::icons::tinted( + crate::ui::icons::DOT, + 7.0, + Color { + r: 0.90, + g: 0.75, + b: 0.30, + a: 1.0, + }, + ), + text(name).size(12), + ] + .spacing(5) + .align_y(iced::Center) + .into() } else { - name + text(name).size(12).into() }; - let title_btn = button(text(label).size(12)) + let title_btn = button(title_inner) .on_press(Message::TabSwitch(idx)) .padding([5, 12]) .style(move |_: &Theme, status| button::Style { @@ -1888,12 +1923,16 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele let row_inner: Row<'_, Message> = if tab.is_start { row![title_btn].spacing(0).align_y(iced::Center) } else { - let close_btn = button(text("×").size(11).color(Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, - })) + let close_btn = button(crate::ui::icons::tinted( + crate::ui::icons::CLOSE, + 10.0, + Color { + r: 0.55, + g: 0.55, + b: 0.55, + a: 1.0, + }, + )) .on_press(Message::TabClose(idx)) .padding([3, 5]) .style(move |_: &Theme, status| button::Style { @@ -2825,12 +2864,35 @@ fn viewport_context_menu_overlay( Message::Command("COPY".to_string()), )); items.push(sep()); - let arrow = if draworder_open { - "Draw Order \u{25be}" + let do_caret = if draworder_open { + crate::ui::icons::arrow_down(9.0, TEXT_COL) } else { - "Draw Order \u{25b8}" + crate::ui::icons::arrow_right(9.0, TEXT_COL) }; - items.push(item(arrow.to_string(), Message::DrawOrderSubmenuToggle)); + items.push( + button( + row![ + text("Draw Order").size(12).color(TEXT_COL), + iced::widget::Space::new().width(Fill), + do_caret, + ] + .align_y(iced::Center), + ) + .on_press(Message::DrawOrderSubmenuToggle) + .style(|_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered | button::Status::Pressed => ITEM_HOVER, + _ => Color::TRANSPARENT, + })), + text_color: TEXT_COL, + border: Border::default(), + shadow: iced::Shadow::default(), + snap: false, + }) + .padding([4, 12]) + .width(Fill) + .into(), + ); if draworder_open { items.push(subitem( "Bring to Front".to_string(), @@ -3353,7 +3415,7 @@ fn save_as_dialog_window<'a>( let path_bar = row![ { let up_msg = up_path.map(Message::SaveDialogNavigate); - let b = button(text("↑").size(14).color(TEXT)) + let b = button(crate::ui::icons::tinted(crate::ui::icons::UP, 14.0, TEXT)) .style(|_: &Theme, st| button::Style { background: Some(Background::Color( if matches!(st, button::Status::Hovered | button::Status::Pressed) { @@ -3398,14 +3460,18 @@ fn save_as_dialog_window<'a>( let rows: Vec> = entries .iter() .map(|(name, is_dir, path)| { - let icon = if *is_dir { "📁" } else { "📄" }; + let icon_bytes = if *is_dir { + crate::ui::icons::FOLDER + } else { + crate::ui::icons::DOC + }; let color = if *is_dir { DIR_COL } else { FILE_COL }; let p = path.clone(); let d = *is_dir; mouse_area( container( row![ - text(icon).size(13), + crate::ui::icons::tinted(icon_bytes, 13.0, color), Space::new().width(6), text(crate::ui::text_util::elide(name.as_str(), 48)) .size(13) @@ -3792,9 +3858,10 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> { let donate_btn = { button( row![ - text("♥ ").size(15).color(Color::WHITE), + crate::ui::icons::tinted(crate::ui::icons::HEART, 14.0, Color::WHITE), text("Donate").size(14).color(Color::WHITE), ] + .spacing(5) .align_y(iced::Center), ) .on_press(Message::RibbonToolClick { @@ -4097,7 +4164,7 @@ pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Eleme }); let path_for_remove = path.clone(); - let remove_btn = button(text("×").size(12).color(MUTED)) + let remove_btn = button(crate::ui::icons::tinted(crate::ui::icons::CLOSE, 11.0, MUTED)) .on_press(Message::RecentRemove(path_for_remove)) .padding([4, 8]) .style(|_: &Theme, status| button::Style { diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index 0ec5b05b2..e303ff3de 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -193,25 +193,33 @@ impl CadModule for AnnotateModule { RibbonItem::Tool(crate::modules::ToolDef { id: "ANNOSCALE", label: "Scale List", - icon: crate::modules::IconKind::Glyph("⬡"), + icon: crate::modules::IconKind::Svg(include_bytes!( + "../../../assets/icons/scale_list.svg" + )), event: crate::modules::ModuleEvent::Command("ANNOSCALE".to_string()), }), RibbonItem::Tool(crate::modules::ToolDef { id: "OBJECTSCALE", label: "Add Scale", - icon: crate::modules::IconKind::Glyph("⊕"), + icon: crate::modules::IconKind::Svg(include_bytes!( + "../../../assets/icons/add_scale.svg" + )), event: crate::modules::ModuleEvent::Command("OBJECTSCALE".to_string()), }), RibbonItem::Tool(crate::modules::ToolDef { id: "SCALELISTEDIT", label: "Scale Edit", - icon: crate::modules::IconKind::Glyph("≡"), + icon: crate::modules::IconKind::Svg(include_bytes!( + "../../../assets/icons/scale_list.svg" + )), event: crate::modules::ModuleEvent::Command("SCALELISTEDIT".to_string()), }), RibbonItem::Tool(crate::modules::ToolDef { id: "SYNCPVIEWPORTS", label: "Sync Scales", - icon: crate::modules::IconKind::Glyph("⇄"), + icon: crate::modules::IconKind::Svg(include_bytes!( + "../../../assets/icons/sync.svg" + )), event: crate::modules::ModuleEvent::Command("SYNCPVIEWPORTS".to_string()), }), ], diff --git a/src/modules/draw/layers/color.rs b/src/modules/draw/layers/color.rs index 49e3dec9f..6ca5bdd7e 100644 --- a/src/modules/draw/layers/color.rs +++ b/src/modules/draw/layers/color.rs @@ -7,7 +7,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "COLOR", label: "Color", - icon: IconKind::Glyph("🎨"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/color_palette.svg")), event: ModuleEvent::Command("COLOR".to_string()), } } diff --git a/src/modules/draw/properties/bylayer.rs b/src/modules/draw/properties/bylayer.rs index b7f5cf9b9..fe9173d6d 100644 --- a/src/modules/draw/properties/bylayer.rs +++ b/src/modules/draw/properties/bylayer.rs @@ -6,7 +6,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "BYLAYER", label: "ByLayer", - icon: IconKind::Glyph("⊟"), + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/bylayer.svg")), event: ModuleEvent::Command("BYLAYER".to_string()), } } diff --git a/src/modules/insert/box_prim.rs b/src/modules/insert/box_prim.rs index 5c52306b4..3e88a33d2 100644 --- a/src/modules/insert/box_prim.rs +++ b/src/modules/insert/box_prim.rs @@ -3,7 +3,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "BOX", label: "Box", - icon: IconKind::Glyph("⬜"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/box3d.svg")), event: ModuleEvent::Command("BOX".to_string()), } } diff --git a/src/modules/insert/clear.rs b/src/modules/insert/clear.rs index ddd9c7203..27af3f043 100644 --- a/src/modules/insert/clear.rs +++ b/src/modules/insert/clear.rs @@ -3,7 +3,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "CLEAR", label: "Clear", - icon: IconKind::Glyph("🗑"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/clear_tool.svg")), event: ModuleEvent::ClearModels, } } diff --git a/src/modules/insert/cylinder.rs b/src/modules/insert/cylinder.rs index 6bd6904b8..dcd3350df 100644 --- a/src/modules/insert/cylinder.rs +++ b/src/modules/insert/cylinder.rs @@ -3,7 +3,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "CYLINDER", label: "Cylinder", - icon: IconKind::Glyph("⬡"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/cylinder3d.svg")), event: ModuleEvent::Command("CYLINDER".to_string()), } } diff --git a/src/modules/insert/open_obj.rs b/src/modules/insert/open_obj.rs index b6ec378fa..e82c4a556 100644 --- a/src/modules/insert/open_obj.rs +++ b/src/modules/insert/open_obj.rs @@ -3,7 +3,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "OPEN", label: "Open", - icon: IconKind::Glyph("📂"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/import_obj.svg")), event: ModuleEvent::OpenFileDialog, } } diff --git a/src/modules/insert/sphere.rs b/src/modules/insert/sphere.rs index 889ced35b..c17a9420b 100644 --- a/src/modules/insert/sphere.rs +++ b/src/modules/insert/sphere.rs @@ -3,7 +3,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "SPHERE", label: "Sphere", - icon: IconKind::Glyph("⬤"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/sphere3d.svg")), event: ModuleEvent::Command("SPHERE".to_string()), } } diff --git a/src/modules/layout/mod.rs b/src/modules/layout/mod.rs index b126ff1bd..78a5a63c9 100644 --- a/src/modules/layout/mod.rs +++ b/src/modules/layout/mod.rs @@ -14,13 +14,13 @@ pub fn paper_space_tools() -> Vec { ToolDef { id: "PAGESETUP", label: "Page Setup", - icon: IconKind::Glyph("📋"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/pagesetup.svg")), event: ModuleEvent::Command("PAGESETUP".to_string()), }, ToolDef { id: "PLOT", label: "Export PDF", - icon: IconKind::Glyph("🖨"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")), event: ModuleEvent::Command("PLOT".to_string()), }, ] @@ -48,14 +48,14 @@ impl CadModule for LayoutModule { ToolDef { id: "PAGESETUP", label: "Page Setup", - icon: IconKind::Glyph("📋"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/pagesetup.svg")), event: ModuleEvent::Command("PAGESETUP".to_string()), } .into(), ToolDef { id: "PLOT", label: "Export PDF", - icon: IconKind::Glyph("🖨"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")), event: ModuleEvent::Command("PLOT".to_string()), } .into(), diff --git a/src/modules/layout/mview.rs b/src/modules/layout/mview.rs index 399218a62..ea4c844f0 100644 --- a/src/modules/layout/mview.rs +++ b/src/modules/layout/mview.rs @@ -19,7 +19,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "MVIEW", label: "Viewport", - icon: IconKind::Glyph("⬜"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/viewport.svg")), event: ModuleEvent::Command("MVIEW".to_string()), } } diff --git a/src/modules/manage/mview.rs b/src/modules/manage/mview.rs index 559136e91..b7df843bd 100644 --- a/src/modules/manage/mview.rs +++ b/src/modules/manage/mview.rs @@ -19,7 +19,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "MVIEW", label: "Viewport", - icon: IconKind::Glyph("⬜"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/viewport.svg")), event: ModuleEvent::Command("MVIEW".to_string()), } } diff --git a/src/modules/manage/options.rs b/src/modules/manage/options.rs index de3e535ad..615f31d0a 100644 --- a/src/modules/manage/options.rs +++ b/src/modules/manage/options.rs @@ -4,7 +4,7 @@ pub fn tool() -> ToolDef { ToolDef { id: "OPTIONS", label: "Options", - icon: IconKind::Glyph("⚙"), + icon: IconKind::Svg(include_bytes!("../../../assets/icons/options_tool.svg")), event: ModuleEvent::Command("OPTIONS".to_string()), } } diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs index ab079fbad..b30d464f7 100644 --- a/src/modules/model/mod.rs +++ b/src/modules/model/mod.rs @@ -12,12 +12,22 @@ use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, pub struct ModelModule; +const BOX_ICON: &[u8] = include_bytes!("../../../assets/icons/box3d.svg"); +const CYLINDER_ICON: &[u8] = include_bytes!("../../../assets/icons/cylinder3d.svg"); +const CONE_ICON: &[u8] = include_bytes!("../../../assets/icons/cone3d.svg"); +const SPHERE_ICON: &[u8] = include_bytes!("../../../assets/icons/sphere3d.svg"); +const WEDGE_ICON: &[u8] = include_bytes!("../../../assets/icons/wedge3d.svg"); +const TORUS_ICON: &[u8] = include_bytes!("../../../assets/icons/torus3d.svg"); +const UNION_ICON: &[u8] = include_bytes!("../../../assets/icons/union.svg"); +const SUBTRACT_ICON: &[u8] = include_bytes!("../../../assets/icons/subtract.svg"); +const INTERSECT_ICON: &[u8] = include_bytes!("../../../assets/icons/intersect.svg"); + /// Helper to declare a ribbon tool that fires a named command. -fn tool(id: &'static str, label: &'static str, glyph: &'static str) -> ToolDef { +fn tool(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef { ToolDef { id, label, - icon: IconKind::Glyph(glyph), + icon: IconKind::Svg(icon), event: ModuleEvent::Command(id.to_string()), } } @@ -35,16 +45,16 @@ impl CadModule for ModelModule { RibbonGroup { title: "Model", tools: vec![ - RibbonItem::LargeTool(tool("BOX", "Box", "▦")), - RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", "⬭")), - RibbonItem::LargeTool(tool("CONE", "Cone", "△")), - RibbonItem::LargeTool(tool("SPHERE", "Sphere", "◯")), + RibbonItem::LargeTool(tool("BOX", "Box", BOX_ICON)), + RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", CYLINDER_ICON)), + RibbonItem::LargeTool(tool("CONE", "Cone", CONE_ICON)), + RibbonItem::LargeTool(tool("SPHERE", "Sphere", SPHERE_ICON)), RibbonItem::Dropdown { id: "MODEL_MORE", - icon: IconKind::Glyph("◰"), + icon: IconKind::Svg(WEDGE_ICON), items: vec![ - ("WEDGE", "Wedge", IconKind::Glyph("◣")), - ("TORUS", "Torus", IconKind::Glyph("◎")), + ("WEDGE", "Wedge", IconKind::Svg(WEDGE_ICON)), + ("TORUS", "Torus", IconKind::Svg(TORUS_ICON)), ], default: "WEDGE", }, @@ -53,9 +63,9 @@ impl CadModule for ModelModule { RibbonGroup { title: "Design", tools: vec![ - RibbonItem::LargeTool(tool("UNION", "Union", "⊕")), - RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", "⊖")), - RibbonItem::LargeTool(tool("INTERSECT", "Intersect", "⊗")), + RibbonItem::LargeTool(tool("UNION", "Union", UNION_ICON)), + RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", SUBTRACT_ICON)), + RibbonItem::LargeTool(tool("INTERSECT", "Intersect", INTERSECT_ICON)), ], }, ] diff --git a/src/ui/app_menu.rs b/src/ui/app_menu.rs index 4d9f8e560..22840ae87 100644 --- a/src/ui/app_menu.rs +++ b/src/ui/app_menu.rs @@ -18,7 +18,7 @@ use crate::app::Message; // ── Menu item definitions ───────────────────────────────────────────────── struct MenuItem { - icon: &'static str, + icon: &'static [u8], label: &'static str, desc: &'static str, command: &'static str, @@ -26,43 +26,43 @@ struct MenuItem { const MENU_ITEMS: &[MenuItem] = &[ MenuItem { - icon: "◻", + icon: crate::ui::icons::DOC_NEW, label: "New", desc: "Create a new drawing", command: "NEW", }, MenuItem { - icon: "📂", + icon: crate::ui::icons::FOLDER, label: "Open", desc: "Open an existing file", command: "OPEN", }, MenuItem { - icon: "💾", + icon: crate::ui::icons::SAVE, label: "Save", desc: "Save the current drawing", command: "SAVE", }, MenuItem { - icon: "📄", + icon: crate::ui::icons::DOC, label: "Save As", desc: "Save as DWG or DXF", command: "SAVEAS", }, MenuItem { - icon: "🖨", + icon: crate::ui::icons::PRINT, label: "Print", desc: "Print or plot the drawing", command: "PRINT", }, MenuItem { - icon: "⚙", + icon: crate::ui::icons::GEAR, label: "Options", desc: "Open application settings", command: "OPTIONS", }, MenuItem { - icon: "❓", + icon: crate::ui::icons::HELP, label: "Help", desc: "Open help documentation", command: "HELP", @@ -336,7 +336,7 @@ impl AppMenu { fn menu_item_btn<'a>(item: &'a MenuItem) -> Element<'a, Message> { button( row![ - container(text(item.icon).size(16).color(ICON_COLOR)) + container(crate::ui::icons::tinted(item.icon, 16.0, ICON_COLOR)) .width(32) .padding([0, 4]), column![ @@ -386,7 +386,8 @@ fn recent_item_btn(path: &PathBuf) -> Element<'_, Message> { let path_str = path.display().to_string(); button( row![ - container(text("📄").size(13).color(ICON_COLOR)).width(24), + 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), diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs index d0859f45d..973335cca 100644 --- a/src/ui/color_select.rs +++ b/src/ui/color_select.rs @@ -125,7 +125,7 @@ pub fn color_selector<'a>( row![ swatch(cur_bg), text(cur_name).size(11).color(TEXT), - text(if open { " ▲" } else { " ▾" }).size(9).color(TEXT), + crate::ui::icons::arrow_toggle(open, 9.0, TEXT), ] .spacing(5) .align_y(iced::Center), diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 316e8f2b2..812d5697f 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -397,13 +397,13 @@ impl CommandLine { // full backlog (everything pushed since the app started) so the // user can recover anything that has already faded off the // overlay. - let dropdown_label = if self.history_open { "▾" } else { "▸" }; - let dropdown_btn = button( - text(dropdown_label) - .size(11) - .color(PROMPT_COLOR), - ) - .on_press(Message::CommandHistoryToggle) + let dropdown_icon = if self.history_open { + crate::ui::icons::arrow_down(11.0, PROMPT_COLOR) + } else { + crate::ui::icons::arrow_right(11.0, PROMPT_COLOR) + }; + let dropdown_btn = button(dropdown_icon) + .on_press(Message::CommandHistoryToggle) .style(|_: &Theme, _status| button::Style { background: Some(Background::Color(INPUT_ROW_BG)), text_color: Color::WHITE, diff --git a/src/ui/icons.rs b/src/ui/icons.rs new file mode 100644 index 000000000..fe57bb2ad --- /dev/null +++ b/src/ui/icons.rs @@ -0,0 +1,142 @@ +//! Shared monochrome UI-chrome icons rendered from bundled SVGs. +//! +//! Dropdown carets and the undo/redo controls used to be drawn as Unicode +//! glyphs (`▾`, `▲`, `↶`, `↷`). Those depend on the active text font carrying +//! the glyph: on desktop the system fallback fonts supply them, but the web +//! build bundles only Fira Sans, which lacks them, so they rendered as empty +//! boxes. Drawing them from SVG instead makes the chrome font-independent. + +use iced::widget::{container, svg, Space}; +use iced::{Color, Element, Length, Theme}; + +const TRI_DOWN: &[u8] = include_bytes!("../../assets/icons/ui/tri_down.svg"); +const TRI_UP: &[u8] = include_bytes!("../../assets/icons/ui/tri_up.svg"); +const TRI_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/tri_right.svg"); +const UNDO: &[u8] = include_bytes!("../../assets/icons/ui/undo.svg"); +const REDO: &[u8] = include_bytes!("../../assets/icons/ui/redo.svg"); + +const LAY_ON: &[u8] = include_bytes!("../../assets/icons/layers/layon.svg"); +const LAY_OFF: &[u8] = include_bytes!("../../assets/icons/layers/layoff.svg"); +const LAY_FRZ: &[u8] = include_bytes!("../../assets/icons/layers/layfrz.svg"); +const LAY_THW: &[u8] = include_bytes!("../../assets/icons/layers/laythw.svg"); +const LAY_LCK: &[u8] = include_bytes!("../../assets/icons/layers/laylck.svg"); +const LAY_ULK: &[u8] = include_bytes!("../../assets/icons/layers/layulk.svg"); + +// Monochrome chrome glyphs (replace Unicode glyphs in buttons / menus / toolbars). +// All are black-on-transparent; recolour them at the call site with [`tinted`]. +pub const CHECK: &[u8] = include_bytes!("../../assets/icons/ui/check.svg"); +pub const CLOSE: &[u8] = include_bytes!("../../assets/icons/ui/close.svg"); +pub const PLUS: &[u8] = include_bytes!("../../assets/icons/ui/plus.svg"); +pub const TRASH: &[u8] = include_bytes!("../../assets/icons/ui/trash.svg"); +pub const MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg"); +pub const BOLT: &[u8] = include_bytes!("../../assets/icons/ui/bolt.svg"); +pub const MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg"); +pub const SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg"); +pub const SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg"); +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 SAVE: &[u8] = include_bytes!("../../assets/icons/ui/save.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"); +pub const ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg"); + +/// Render one of the bundled SVGs tinted to `color` at a square `size`. +pub fn tinted<'a, M: 'a>(bytes: &'static [u8], size: f32, color: Color) -> Element<'a, M> { + svg(svg::Handle::from_memory(bytes)) + .width(size) + .height(size) + .style(move |_: &Theme, _| svg::Style { color: Some(color) }) + .into() +} + +/// Backwards-compatible alias used by the caret/undo/redo helpers below. +fn icon<'a, M: 'a>(bytes: &'static [u8], size: f32, color: Color) -> Element<'a, M> { + tinted(bytes, size, color) +} + +/// A fixed-width (14 px) "current row" check column: a green-tintable check +/// when `active`, otherwise an empty spacer that preserves alignment. Used by +/// the many dropdown / popup list rows that mark the selected entry. +pub fn check_cell<'a, M: 'a>(active: bool, color: Color) -> Element<'a, M> { + let inner: Element<'a, M> = if active { + tinted(CHECK, 11.0, color) + } else { + Space::new().width(0).into() + }; + container(inner).width(Length::Fixed(14.0)).into() +} + +/// Render a bundled SVG at its native colours (no tint) at a square `size`. +pub fn raw<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> { + svg(svg::Handle::from_memory(bytes)) + .width(size) + .height(size) + .into() +} + +/// Layer visibility icon bytes (on / off). +pub fn layer_visible(visible: bool) -> &'static [u8] { + if visible { + LAY_ON + } else { + LAY_OFF + } +} + +/// Layer freeze icon bytes (frozen / thawed). +pub fn layer_freeze(frozen: bool) -> &'static [u8] { + if frozen { + LAY_FRZ + } else { + LAY_THW + } +} + +/// Layer lock icon bytes (locked / unlocked). +pub fn layer_lock(locked: bool) -> &'static [u8] { + if locked { + LAY_LCK + } else { + LAY_ULK + } +} + +/// Downward dropdown caret (replaces `▾`). +pub fn arrow_down<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { + icon(TRI_DOWN, size, color) +} + +/// Upward dropdown caret, shown when a dropdown is open (replaces `▲`). +pub fn arrow_up<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { + icon(TRI_UP, size, color) +} + +/// Rightward caret for a collapsed item (replaces `▸`). +pub fn arrow_right<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { + icon(TRI_RIGHT, size, color) +} + +/// Caret that flips up/down with `open`. +pub fn arrow_toggle<'a, M: 'a>(open: bool, size: f32, color: Color) -> Element<'a, M> { + if open { + arrow_up(size, color) + } else { + arrow_down(size, color) + } +} + +/// Undo curved arrow (replaces `↶`). +pub fn undo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { + icon(UNDO, size, color) +} + +/// Redo curved arrow (replaces `↷`). +pub fn redo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> { + icon(REDO, size, color) +} diff --git a/src/ui/layers.rs b/src/ui/layers.rs index 06edb6568..0e03c49e9 100644 --- a/src/ui/layers.rs +++ b/src/ui/layers.rs @@ -170,9 +170,19 @@ impl LayerPanel { // ── Toolbar ─────────────────────────────────────────────────────── let toolbar = container( row![ - toolbar_btn("⊕ New", Message::LayerNew), - toolbar_btn_cond("⊗ Delete", Message::LayerDelete, has_sel && !sel_is_zero), - toolbar_btn_cond("✓ Set Current", Message::LayerSetCurrent, has_sel), + toolbar_btn(crate::ui::icons::PLUS, "New", Message::LayerNew), + toolbar_btn_cond( + crate::ui::icons::TRASH, + "Delete", + Message::LayerDelete, + has_sel && !sel_is_zero, + ), + toolbar_btn_cond( + crate::ui::icons::CHECK, + "Set Current", + Message::LayerSetCurrent, + has_sel, + ), ] .spacing(2), ) @@ -289,9 +299,16 @@ impl LayerPanel { // ── Toolbar buttons ─────────────────────────────────────────────────────── -fn toolbar_btn(label: &str, msg: Message) -> Element<'_, Message> { - button(text(label).size(11).color(Color::WHITE)) - .on_press(msg) +fn toolbar_btn<'a>(icon: &'static [u8], label: &'a str, msg: Message) -> Element<'a, Message> { + button( + row![ + crate::ui::icons::tinted(icon, 12.0, Color::WHITE), + text(label).size(11).color(Color::WHITE), + ] + .spacing(5) + .align_y(iced::Center), + ) + .on_press(msg) .style(|_: &Theme, status| button::Style { background: Some(Background::Color(match status { button::Status::Hovered => Color { @@ -325,8 +342,13 @@ fn toolbar_btn(label: &str, msg: Message) -> Element<'_, Message> { .into() } -fn toolbar_btn_cond(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> { - let mut b = button(text(label).size(11).color(if enabled { +fn toolbar_btn_cond<'a>( + icon: &'static [u8], + label: &'a str, + msg: Message, + enabled: bool, +) -> Element<'a, Message> { + let fg = if enabled { Color::WHITE } else { Color { @@ -335,7 +357,15 @@ fn toolbar_btn_cond(label: &str, msg: Message, enabled: bool) -> Element<'_, Mes b: 0.45, a: 1.0, } - })) + }; + let mut b = button( + row![ + crate::ui::icons::tinted(icon, 12.0, fg), + text(label).size(11).color(fg), + ] + .spacing(5) + .align_y(iced::Center), + ) .style(|_: &Theme, status| button::Style { background: Some(Background::Color(match status { button::Status::Hovered => Color { @@ -431,42 +461,32 @@ fn layer_row<'a>( .into() }; - let vis_svg: &'static [u8] = if layer.visible { - include_bytes!("../../assets/icons/layers/layon.svg") - } else { - include_bytes!("../../assets/icons/layers/layoff.svg") - }; - let frz_svg: &'static [u8] = if layer.frozen { - include_bytes!("../../assets/icons/layers/layfrz.svg") - } else { - include_bytes!("../../assets/icons/layers/laythw.svg") - }; - let lck_svg: &'static [u8] = if layer.locked { - include_bytes!("../../assets/icons/layers/laylck.svg") - } else { - include_bytes!("../../assets/icons/layers/layulk.svg") - }; + let vis_svg = crate::ui::icons::layer_visible(layer.visible); + let frz_svg = crate::ui::icons::layer_freeze(layer.frozen); + let lck_svg = crate::ui::icons::layer_lock(layer.locked); let status_dot: Element<'_, Message> = if is_current { - text("✓") - .size(13) - .color(Color { + crate::ui::icons::tinted( + crate::ui::icons::CHECK, + 13.0, + Color { r: 0.25, g: 0.85, b: 0.45, a: 1.0, - }) - .into() + }, + ) } else { - text("▣") - .size(11) - .color(Color { + crate::ui::icons::tinted( + crate::ui::icons::DOT, + 9.0, + Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0, - }) - .into() + }, + ) }; // Name cell @@ -693,11 +713,7 @@ fn layer_row<'a>( // Per-viewport freeze columns for (vp_idx, _vp_col) in vp_cols.iter().enumerate() { let is_vp_frozen = layer.vp_frozen.get(vp_idx).copied().unwrap_or(false); - let vp_frz_svg: &'static [u8] = if is_vp_frozen { - include_bytes!("../../assets/icons/layers/layfrz.svg") - } else { - include_bytes!("../../assets/icons/layers/laythw.svg") - }; + let vp_frz_svg = crate::ui::icons::layer_freeze(is_vp_frozen); row_content = row_content.push( container(svg_btn( vp_frz_svg, diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index cde6a8248..1c2316aa2 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -194,14 +194,28 @@ pub fn view_window<'a>( }) .padding([4, 10]), Space::new().width(Fill), - button(text("◀ Move Left").size(11)) - .on_press(Message::LayoutManagerMoveLeft) - .style(btn_s(false)) - .padding([4, 8]), - button(text("Move Right ▶").size(11)) - .on_press(Message::LayoutManagerMoveRight) - .style(btn_s(false)) - .padding([4, 8]), + button( + row![ + crate::ui::icons::tinted(crate::ui::icons::TRI_LEFT_B, 9.0, Color::WHITE), + text("Move Left").size(11), + ] + .spacing(4) + .align_y(iced::Center), + ) + .on_press(Message::LayoutManagerMoveLeft) + .style(btn_s(false)) + .padding([4, 8]), + button( + row![ + text("Move Right").size(11), + crate::ui::icons::arrow_right(9.0, Color::WHITE), + ] + .spacing(4) + .align_y(iced::Center), + ) + .on_press(Message::LayoutManagerMoveRight) + .style(btn_s(false)) + .padding([4, 8]), button(text("Set Current").size(11)) .on_press(Message::LayoutManagerSetCurrent) .style(btn_s(true)) @@ -223,12 +237,17 @@ pub fn view_window<'a>( .map(|name| { let is_sel = name.as_str() == selected; let is_cur = name.as_str() == current.as_str(); - let label = if is_cur { - format!("{name} ◀") - } else { - name.clone() - }; - button(text(label).size(12)) + let mut item_row = row![text(name.clone()).size(12)] + .spacing(5) + .align_y(iced::Center); + if is_cur { + item_row = item_row.push(crate::ui::icons::tinted( + crate::ui::icons::TRI_LEFT_B, + 8.0, + Color::WHITE, + )); + } + button(item_row) .on_press(Message::LayoutManagerSelect(name.clone())) .style(list_item(is_sel)) .padding([5, 10]) @@ -286,7 +305,7 @@ pub fn view_window<'a>( row![ text("Status:").size(11).color(DIM).width(80), text(if selected == current.as_str() { - "Active ◀" + "Active" } else { "Inactive" }) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d192bcad7..c33041ce6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,6 +8,7 @@ pub mod color_select; pub mod command_line; pub mod cycle_popup; pub mod dimstyle; +pub mod icons; pub mod isolate_popup; pub mod layers; pub mod layout_manager; diff --git a/src/ui/modal.rs b/src/ui/modal.rs index 1f79915eb..897669529 100644 --- a/src/ui/modal.rs +++ b/src/ui/modal.rs @@ -6,7 +6,7 @@ //! so both stack dialogs here — one code path for every platform. use crate::app::Message; -use iced::widget::{button, column, container, mouse_area, opaque, row, stack, text}; +use iced::widget::{button, column, container, mouse_area, opaque, row, stack}; use iced::{Background, Border, Color, Element, Length, Padding, Theme, Vector}; const PANEL: Color = Color { @@ -49,17 +49,26 @@ pub fn modal<'a>( on_close: Message, offset: Vector, ) -> Element<'a, Message> { - let close = button(text("✕").size(15)) - .on_press(on_close) - .padding([1, 7]) - .style(close_style); + let close = button(crate::ui::icons::tinted( + crate::ui::icons::CLOSE, + 13.0, + Color { + r: 0.85, + g: 0.85, + b: 0.85, + a: 1.0, + }, + )) + .on_press(on_close) + .padding([1, 7]) + .style(close_style); // Draggable title bar: a grip handle next to the ✕. Kept `Shrink` (no // `Fill`) so a single Fill child can't blow the dialog out to the full // screen width — the dialog stays sized to its content. Pressing the grip // starts a drag (handled in `update`). let grip = mouse_area( - container(text("✥").size(15).color(GRIP_C)) + container(crate::ui::icons::tinted(crate::ui::icons::MOVE, 14.0, GRIP_C)) .padding([1, 7]) .style(|_: &Theme| container::Style { background: Some(Background::Color(TITLE_C)), diff --git a/src/ui/page_setup.rs b/src/ui/page_setup.rs index dde0484c7..1ebc4b69b 100644 --- a/src/ui/page_setup.rs +++ b/src/ui/page_setup.rs @@ -329,12 +329,12 @@ pub fn view_window<'a>( hdivider(), section_label("Position"), button( - text(if center { - "✓ Center on page" - } else { - " Center on page" - }) - .size(11) + row![ + crate::ui::icons::check_cell(center, Color::WHITE), + text("Center on page").size(11), + ] + .spacing(2) + .align_y(iced::Center) ) .on_press(Message::PageSetupCenterToggle) .style(pill(center)) diff --git a/src/ui/plugin_manager.rs b/src/ui/plugin_manager.rs index 2bb724e60..bd8e43d97 100644 --- a/src/ui/plugin_manager.rs +++ b/src/ui/plugin_manager.rs @@ -199,6 +199,27 @@ fn pill_button<'a>(label: &str, msg: Message, bg: Color) -> Element<'a, Message> .into() } +/// Square icon variant of [`pill_button`] for glyph-free actions (e.g. remove). +fn pill_icon_button<'a>(icon: &'static [u8], msg: Message, bg: Color) -> Element<'a, Message> { + button(crate::ui::icons::tinted(icon, 11.0, WHITE)) + .padding([5, 9]) + .on_press(msg) + .style(move |_: &Theme, status| { + let c = if matches!(status, button::Status::Hovered | button::Status::Pressed) { + Color { r: bg.r + 0.08, g: bg.g + 0.08, b: bg.b + 0.08, a: 1.0 } + } else { + bg + }; + button::Style { + background: Some(Background::Color(c)), + text_color: WHITE, + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + } + }) + .into() +} + const GREEN: Color = Color { r: 0.2, g: 0.45, b: 0.28, a: 1.0 }; const RED: Color = Color { r: 0.4, g: 0.25, b: 0.25, a: 1.0 }; @@ -229,7 +250,11 @@ fn install_controls<'a>( .spacing(4); if removable { controls = controls.push(Space::new().width(6)); - controls = controls.push(pill_button("✕", Message::PluginRepoRemove(repo_s), RED)); + controls = controls.push(pill_icon_button( + crate::ui::icons::CLOSE, + Message::PluginRepoRemove(repo_s), + RED, + )); } controls.into() } diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 2ac5a78c1..ff936d0e0 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -676,13 +676,14 @@ pub fn color_picker_dropdown<'a>( // "More Colors…" toggle button let more_btn = button( - text(if palette_open { - "▲ Less" - } else { - "▼ More Colors…" - }) - .size(10) - .color(HINT_COLOR), + row![ + crate::ui::icons::arrow_toggle(palette_open, 9.0, HINT_COLOR), + text(if palette_open { "Less" } else { "More Colors…" }) + .size(10) + .color(HINT_COLOR), + ] + .spacing(4) + .align_y(iced::Center), ) .on_press(palette_toggle_msg) .style(|_: &Theme, _| button::Style { diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 452ad8150..2f211f40d 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -234,20 +234,8 @@ impl Ribbon { // ── Tab buttons ─────────────────────────────────────────────────── let history_controls = row![ - render_history_control( - "↶", - "Undo", - UNDO_HISTORY_ID, - undo_count, - &self.open_dropdown - ), - render_history_control( - "↷", - "Redo", - REDO_HISTORY_ID, - redo_count, - &self.open_dropdown - ), + render_history_control("Undo", UNDO_HISTORY_ID, undo_count, &self.open_dropdown), + render_history_control("Redo", REDO_HISTORY_ID, redo_count, &self.open_dropdown), ] .spacing(TOP_HIST_GAP) .align_y(iced::Center); @@ -609,14 +597,13 @@ impl Ribbon { .iter() .map(|(cmd, label, item_icon)| { let is_current = *cmd == last_cmd; - let checkmark = text(if is_current { "✓" } else { " " }) - .size(11) - .color(if is_current { - CHECK_COLOR - } else { - Color::TRANSPARENT - }) - .width(Length::Fixed(14.0)); + let checkmark: Element<'_, Message> = container(if is_current { + crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) + } else { + iced::widget::Space::new().width(0).into() + }) + .width(Length::Fixed(14.0)) + .into(); let icon_el: Element = match *item_icon { IconKind::Glyph(s) => text(s) .size(13) @@ -717,59 +704,16 @@ impl Ribbon { .width(12) .height(12); - let vis = text(if lv { "●" } else { "○" }).size(10).color(if lv { - Color { - r: 0.95, - g: 0.85, - b: 0.20, - a: 1.0, - } + let vis = crate::ui::icons::raw(crate::ui::icons::layer_visible(lv), 14.0); + let freeze = crate::ui::icons::raw(crate::ui::icons::layer_freeze(lf), 14.0); + let lock = crate::ui::icons::raw(crate::ui::icons::layer_lock(ll), 14.0); + let checkmark: Element<'_, Message> = container(if is_active { + crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) } else { - Color { - r: 0.45, - g: 0.45, - b: 0.45, - a: 1.0, - } - }); - let freeze = text("✱").size(10).color(if lf { - Color { - r: 0.40, - g: 0.80, - b: 1.00, - a: 1.0, - } - } else { - Color { - r: 0.95, - g: 0.85, - b: 0.20, - a: 1.0, - } - }); - let lock = text(if ll { "🔒" } else { "🔓" }).size(10).color(if ll { - Color { - r: 0.95, - g: 0.70, - b: 0.20, - a: 1.0, - } - } else { - Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, - } - }); - let checkmark = text(if is_active { "✓" } else { " " }) - .size(11) - .color(if is_active { - CHECK_COLOR - } else { - Color::TRANSPARENT - }) - .width(Length::Fixed(14.0)); + iced::widget::Space::new().width(0).into() + }) + .width(Length::Fixed(14.0)) + .into(); let label = text(&info.name) .size(11) @@ -893,14 +837,13 @@ impl Ribbon { .into_iter() .map(|lt| { let is_cur = lt.name == *active_lt; - let check = text(if is_cur { "✓" } else { " " }) - .size(11) - .color(if is_cur { - CHECK_COLOR - } else { - Color::TRANSPARENT - }) - .width(Length::Fixed(14.0)); + let check: Element<'_, Message> = container(if is_cur { + crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) + } else { + iced::widget::Space::new().width(0).into() + }) + .width(Length::Fixed(14.0)) + .into(); let name_col = text(lt.name.clone()) .size(11) .color(if is_cur { LABEL_ON } else { LABEL_OFF }) @@ -971,14 +914,13 @@ impl Ribbon { .map(|item| { let is_cur = item.0 == active_lw; let label = item.to_string(); - let check = text(if is_cur { "✓" } else { " " }) - .size(11) - .color(if is_cur { - CHECK_COLOR - } else { - Color::TRANSPARENT - }) - .width(Length::Fixed(14.0)); + let check: Element<'_, Message> = container(if is_cur { + crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) + } else { + iced::widget::Space::new().width(0).into() + }) + .width(Length::Fixed(14.0)) + .into(); button( row![ check, diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index c46a946b0..8a77f07c9 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -11,6 +11,7 @@ use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; use crate::app::Message; use crate::modules::{IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef}; +use crate::ui::icons; use crate::ui::properties::{acad_color_display, LwItem}; use super::LayerInfo; @@ -361,7 +362,7 @@ pub(super) fn render_small<'a>( let arr_tip = format!("{} options", cur_label); let arr_btn = button( - container(text("▾").size(7).color(ARROW_COLOR)) + container(icons::arrow_down(8.0, ARROW_COLOR)) .width(Fill) .height(Fill) .align_x(iced::Center) @@ -513,7 +514,7 @@ pub(super) fn render_large<'a>( }); let arr_btn = button( - container(text("▾").size(9).color(ARROW_COLOR)) + container(icons::arrow_down(9.0, ARROW_COLOR)) .width(Fill) .height(Fill) .align_x(iced::Center) @@ -562,51 +563,9 @@ pub(super) fn render_large<'a>( let ll = info.map(|l| l.locked).unwrap_or(false); let is_open = open_dd.as_deref() == Some(LAYER_COMBO_ID); - let vis_icon = text(if lv { "●" } else { "○" }).size(10).color(if lv { - Color { - r: 0.95, - g: 0.85, - b: 0.20, - a: 1.0, - } - } else { - Color { - r: 0.45, - g: 0.45, - b: 0.45, - a: 1.0, - } - }); - let freeze_icon = text("✱").size(10).color(if lf { - Color { - r: 0.40, - g: 0.80, - b: 1.00, - a: 1.0, - } - } else { - Color { - r: 0.95, - g: 0.85, - b: 0.20, - a: 1.0, - } - }); - let lock_icon = text(if ll { "🔒" } else { "🔓" }).size(10).color(if ll { - Color { - r: 0.95, - g: 0.70, - b: 0.20, - a: 1.0, - } - } else { - Color { - r: 0.65, - g: 0.65, - b: 0.65, - a: 1.0, - } - }); + let vis_icon = icons::raw(icons::layer_visible(lv), 14.0); + let freeze_icon = icons::raw(icons::layer_freeze(lf), 14.0); + let lock_icon = icons::raw(icons::layer_lock(ll), 14.0); let swatch = container(text("")) .style(move |_: &Theme| container::Style { background: Some(Background::Color(lc)), @@ -625,7 +584,7 @@ pub(super) fn render_large<'a>( .width(12) .height(12); - const ICONS_USED: f32 = 10.0 + 10.0 + 10.0 + 12.0 + 10.0 + 5.0 * 4.0 + 16.0 + 16.0; + const ICONS_USED: f32 = 14.0 + 14.0 + 14.0 + 12.0 + 10.0 + 5.0 * 4.0 + 16.0 + 16.0; let name_w = (COMBO_W - ICONS_USED).max(40.0); let combo_btn = button( @@ -637,12 +596,15 @@ pub(super) fn render_large<'a>( container(text(active_layer).size(11).color(Color::WHITE)) .width(name_w) .clip(true), - text("▾").size(9).color(Color { - r: 0.7, - g: 0.7, - b: 0.7, - a: 1.0 - }), + icons::arrow_down( + 9.0, + Color { + r: 0.7, + g: 0.7, + b: 0.7, + a: 1.0, + }, + ), ] .spacing(4) .align_y(iced::Center), @@ -801,12 +763,16 @@ pub(super) fn render_large<'a>( container(text(label).size(10).color(Color::WHITE)) .width(Fill) .clip(true), - text(if is_open { "▲" } else { "▼" }).size(7).color(Color { - r: 0.6, - g: 0.6, - b: 0.6, - a: 1.0 - }), + icons::arrow_toggle( + is_open, + 8.0, + Color { + r: 0.6, + g: 0.6, + b: 0.6, + a: 1.0, + }, + ), ] .spacing(4) .align_y(iced::Center), @@ -905,12 +871,16 @@ pub(super) fn render_large<'a>( container(text(active.clone()).size(11).color(Color::WHITE)) .width(Fill) .clip(true), - text(if is_open { "▲" } else { "▾" }).size(9).color(Color { - r: 0.7, - g: 0.7, - b: 0.7, - a: 1.0 - }), + icons::arrow_toggle( + is_open, + 9.0, + Color { + r: 0.7, + g: 0.7, + b: 0.7, + a: 1.0, + }, + ), ] .spacing(4) .align_y(iced::Center), @@ -971,18 +941,21 @@ pub(super) fn render_large<'a>( let key = style_key; button( row![ - text(if is_sel { "✓" } else { " " }) - .size(10) - .color(if is_sel { + container(if is_sel { + icons::tinted( + icons::CHECK, + 10.0, Color { r: 0.2, g: 0.8, b: 0.4, a: 1.0, - } - } else { - Color::TRANSPARENT - }), + }, + ) + } else { + iced::widget::Space::new().width(0).into() + }) + .width(12), text(name).size(11).color(Color::WHITE), ] .spacing(6) @@ -1281,7 +1254,6 @@ pub fn module_event_to_message(event: ModuleEvent) -> Message { // ── History control ──────────────────────────────────────────────────────── pub(super) fn render_history_control<'a>( - glyph: &'static str, label: &'static str, dropdown_id: &'static str, count: usize, @@ -1289,12 +1261,20 @@ pub(super) fn render_history_control<'a>( ) -> Element<'a, Message> { let dd_open = open_dropdown.as_deref() == Some(dropdown_id); let active = count > 0; + let icon_color = if active { Color::WHITE } else { LABEL_OFF }; let main_btn = { + let glyph = if dropdown_id == UNDO_HISTORY_ID { + icons::undo(15.0, icon_color) + } else { + icons::redo(15.0, icon_color) + }; let btn = button( - text(glyph) - .size(14) - .color(if active { Color::WHITE } else { LABEL_OFF }), + container(glyph) + .width(Fill) + .height(Fill) + .align_x(iced::Center) + .align_y(iced::Center), ) .style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status)) .width(Length::Fixed(TOP_HIST_W)) @@ -1321,11 +1301,10 @@ pub(super) fn render_history_control<'a>( let arrow_btn = { let btn = button( - container( - text("▾") - .size(7) - .color(if active { ARROW_COLOR } else { LABEL_OFF }), - ) + container(icons::arrow_down( + 8.0, + if active { ARROW_COLOR } else { LABEL_OFF }, + )) .width(Fill) .height(Fill) .align_x(iced::Center) diff --git a/src/ui/scale_popup.rs b/src/ui/scale_popup.rs index bfc964f2b..980cb577f 100644 --- a/src/ui/scale_popup.rs +++ b/src/ui/scale_popup.rs @@ -70,14 +70,7 @@ pub fn scale_popup_overlay( } fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> { - let check = text(if active { "✓" } else { " " }) - .size(11) - .color(if active { - CHECK_COLOR - } else { - Color::TRANSPARENT - }) - .width(Length::Fixed(14.0)); + let check = crate::ui::icons::check_cell(active, CHECK_COLOR); let lbl = text(label) .size(11) diff --git a/src/ui/selection_filter_popup.rs b/src/ui/selection_filter_popup.rs index c71819436..f497619ae 100644 --- a/src/ui/selection_filter_popup.rs +++ b/src/ui/selection_filter_popup.rs @@ -60,10 +60,7 @@ pub fn selection_filter_popup_overlay( } fn type_row(name: String, included: bool) -> Element<'static, Message> { - let check = text(if included { "✓" } else { " " }) - .size(11) - .color(if included { CHECK_COLOR } else { Color::TRANSPARENT }) - .width(Length::Fixed(14.0)); + let check = crate::ui::icons::check_cell(included, CHECK_COLOR); let lbl = text(name.clone()) .size(11) diff --git a/src/ui/snap_popup.rs b/src/ui/snap_popup.rs index a8aef1af2..d048010fa 100644 --- a/src/ui/snap_popup.rs +++ b/src/ui/snap_popup.rs @@ -107,14 +107,7 @@ fn snap_row<'a>( label: &'a str, active: bool, ) -> Element<'a, Message> { - let checkmark = text(if active { "✓" } else { " " }) - .size(11) - .color(if active { - CHECK_COLOR - } else { - Color::TRANSPARENT - }) - .width(Length::Fixed(14.0)); + let checkmark = crate::ui::icons::check_cell(active, CHECK_COLOR); let icon_el = text(icon) .size(11) diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index 2cb4c163b..378fb3c2c 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -76,7 +76,7 @@ impl StatusBar { // Which pills the user has chosen to show on the bar. config: &'a StatusBarConfig, ) -> Element<'a, Message> { - let menu_btn = button(text("≡").size(14).color(ICON_COLOR)) + let menu_btn = button(crate::ui::icons::tinted(crate::ui::icons::MENU, 14.0, ICON_COLOR)) .on_press(Message::Command("MENU".into())) .style(|_: &Theme, _| button::Style { background: Some(Background::Color(Color::TRANSPARENT)), @@ -285,7 +285,7 @@ fn format_coords(p: glam::Vec3) -> String { // ── Customization handle ────────────────────────────────────────────────── fn customize_btn() -> Element<'static, Message> { - button(text("≡").size(14).color(ICON_COLOR)) + button(crate::ui::icons::tinted(crate::ui::icons::MENU, 14.0, ICON_COLOR)) .on_press(Message::ToggleStatusBarMenu) .style(|_: &Theme, status| button::Style { background: Some(Background::Color(match status { @@ -466,8 +466,15 @@ fn osnap_btn(active: bool, snap_enabled: bool, open: bool) -> Element<'static, M OSNAP_OFF_TEXT }; - let left = button(text("⚡ OSNAP").size(10).color(text_color)) - .on_press(Message::ToggleSnapEnabled) + let left = button( + row![ + crate::ui::icons::tinted(crate::ui::icons::BOLT, 11.0, text_color), + text("OSNAP").size(10).color(text_color), + ] + .spacing(3) + .align_y(iced::Center), + ) + .on_press(Message::ToggleSnapEnabled) .style(move |_: &Theme, status| button::Style { background: Some(Background::Color(match status { button::Status::Hovered => { @@ -495,7 +502,7 @@ fn osnap_btn(active: bool, snap_enabled: bool, open: bool) -> Element<'static, M }) .padding([2, 6]); - let right = button(text("▾").size(9).color(text_color)) + let right = button(crate::ui::icons::arrow_down(9.0, text_color)) .on_press(Message::ToggleSnapPopup) .style(move |_: &Theme, status| button::Style { background: Some(Background::Color(match status { @@ -607,12 +614,16 @@ fn space_tab<'a>( .padding([2, 6]) .width(Length::Fixed(90.0)); - let cancel_btn = button(text("✕").size(10).color(Color { - r: 0.65, - g: 0.65, - b: 0.65, - a: 1.0, - })) + let cancel_btn = button(crate::ui::icons::tinted( + crate::ui::icons::CLOSE, + 10.0, + Color { + r: 0.65, + g: 0.65, + b: 0.65, + a: 1.0, + }, + )) .on_press(Message::LayoutRenameCancel) .style(|_: &Theme, _| button::Style { background: Some(Background::Color(Color::TRANSPARENT)), diff --git a/src/ui/statusbar_menu.rs b/src/ui/statusbar_menu.rs index 3c092d377..720cf2957 100644 --- a/src/ui/statusbar_menu.rs +++ b/src/ui/statusbar_menu.rs @@ -52,10 +52,7 @@ pub fn statusbar_menu_overlay(config: &StatusBarConfig) -> Element<'static, Mess } fn menu_row(label: &'static str, checked: bool, msg: Message) -> Element<'static, Message> { - let check = text(if checked { "✓" } else { " " }) - .size(11) - .color(if checked { CHECK_COLOR } else { Color::TRANSPARENT }) - .width(Length::Fixed(14.0)); + let check = crate::ui::icons::check_cell(checked, CHECK_COLOR); let lbl = text(label) .size(11) diff --git a/src/ui/style_list.rs b/src/ui/style_list.rs index c01c95f02..3e0ee8198 100644 --- a/src/ui/style_list.rs +++ b/src/ui/style_list.rs @@ -61,12 +61,8 @@ pub fn item<'a>( } else { // Fixed-width ✓ column keeps every name left-aligned whether or not the // row is current. - let check = if is_current { - text("✓").size(11).color(CURRENT_CHECK) - } else { - text("").size(11) - }; - let label = row![check.width(14), text(name.to_string()).size(11).color(TEXT)] + let check = crate::ui::icons::check_cell(is_current, CURRENT_CHECK); + let label = row![check, text(name.to_string()).size(11).color(TEXT)] .align_y(iced::Center); let cell = container(label) .padding([4, 8]) diff --git a/src/ui/units_popup.rs b/src/ui/units_popup.rs index ea14dce84..fc23d7aac 100644 --- a/src/ui/units_popup.rs +++ b/src/ui/units_popup.rs @@ -73,10 +73,7 @@ pub fn units_popup_overlay(current: i16) -> Element<'static, Message> { } fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> { - let check = text(if active { "✓" } else { " " }) - .size(11) - .color(if active { CHECK_COLOR } else { Color::TRANSPARENT }) - .width(Length::Fixed(14.0)); + let check = crate::ui::icons::check_cell(active, CHECK_COLOR); let lbl = text(label) .size(11) diff --git a/src/ui/update_notice.rs b/src/ui/update_notice.rs index 92b360c03..95e845632 100644 --- a/src/ui/update_notice.rs +++ b/src/ui/update_notice.rs @@ -109,7 +109,7 @@ fn render_notes_line<'a>(raw: &str) -> Element<'a, Message> { } if let Some(rest) = trimmed.strip_prefix("- ").or_else(|| trimmed.strip_prefix("* ")) { return row![ - text("•").size(11).color(DIM).width(14), + container(crate::ui::icons::tinted(crate::ui::icons::DOT, 5.0, DIM)).width(14), text(strip_inline_md(rest)).size(11).color(WHITE), ] .spacing(4) @@ -165,8 +165,12 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> { false, ); let latest_card = version_card("Latest", format!("v{}", latest), true); - let arrow = container(text("→").size(22).color(DIM)) - .width(iced::Length::Fixed(32.0)) + let arrow = container(crate::ui::icons::tinted( + crate::ui::icons::ARROW_LONG_RIGHT, + 20.0, + DIM, + )) + .width(iced::Length::Fixed(32.0)) .height(Fill) .align_x(iced::Center) .align_y(iced::Center);