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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-18 12:29:32 +03:00
1 parent 4954551808
commit 864dd6cef7
81 files changed
+782 -372

No files matched your search

+97 -30
View File
@@ -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<Element<'_, Message>> = 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 {
+12 -4
View File
@@ -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()),
}),
],
+1 -1
View File
@@ -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()),
}
}
+1 -1
View File
@@ -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()),
}
}
+1 -1
View File
@@ -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()),
}
}
+1 -1
View File
@@ -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,
}
}
+1 -1
View File
@@ -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()),
}
}
+1 -1
View File
@@ -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,
}
}
+1 -1
View File
@@ -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()),
}
}
+4 -4
View File
@@ -14,13 +14,13 @@ pub fn paper_space_tools() -> Vec<ToolDef> {
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(),
+1 -1
View File
@@ -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()),
}
}
+1 -1
View File
@@ -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()),
}
}
+1 -1
View File
@@ -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()),
}
}
+22 -12
View File
@@ -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)),
],
},
]
+11 -10
View File
@@ -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),
+1 -1
View File
@@ -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),
+7 -7
View File
@@ -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,
+142
View File
@@ -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)
}
+55 -39
View File
@@ -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,
+34 -15
View File
@@ -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"
})
+1
View File
@@ -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;
+15 -6
View File
@@ -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)),
+6 -6
View File
@@ -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))
+26 -1
View File
@@ -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()
}
+8 -7
View File
@@ -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 {
+32 -90
View File
@@ -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<Message> = 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,
+61 -82
View File
@@ -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)
+1 -8
View File
@@ -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)
+1 -4
View File
@@ -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)
+1 -8
View File
@@ -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)
+22 -11
View File
@@ -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)),
+1 -4
View File
@@ -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)
+2 -6
View File
@@ -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])
+1 -4
View File
@@ -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)
+7 -3
View File
@@ -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);