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

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M4 7 H16 M4 12 H12 M4 17 H10" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M17 14 V22 M13 18 H21" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M12 3 L20 7 L12 11 L4 7 Z" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M4 7 L4 16 L12 20 L12 11" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M20 7 L20 16 L12 20" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M12 11 L12 20" stroke="#4cc9f0" stroke-width="1.4" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 565 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M12 3 L21 7.5 L12 12 L3 7.5 Z" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M3 12 L12 16.5 L21 12" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M3 16.5 L12 21 L21 16.5" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 494 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M4 7 H20" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9 7 V5 a1 1 0 0 1 1-1 H14 a1 1 0 0 1 1 1 V7" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6 7 L7 20 a1 1 0 0 0 1 1 H16 a1 1 0 0 0 1-1 L18 7" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 11 V17 M14 11 V17" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 654 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<rect x="3.5" y="6" width="9" height="11" rx="1" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<rect x="8" y="8.5" width="9" height="11" rx="1" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<rect x="12.5" y="4.5" width="8" height="11" rx="1" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 538 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M12 3 L5 18 M12 3 L19 18" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<ellipse cx="12" cy="18" rx="7" ry="2.6" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<ellipse cx="12" cy="5.5" rx="6.5" ry="2.6" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5.5 5.5 L5.5 18 M18.5 5.5 L18.5 18" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5.5 18 A6.5 2.6 0 0 0 18.5 18" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 519 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M3 7 H9 L11 9 H21 V19 H3 Z" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M12 11 V16 M9.5 13.5 L12 16 L14.5 13.5" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="9" cy="12" r="6.2" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="15" cy="12" r="6.2" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<ellipse cx="12" cy="12" rx="2.1" ry="4.3" fill="#4cc9f0" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 428 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="12" cy="12" r="3.2" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M12 2.5 V5 M12 19 V21.5 M2.5 12 H5 M19 12 H21.5 M5 5 L6.8 6.8 M17.2 17.2 L19 19 M19 5 L17.2 6.8 M6.8 17.2 L5 19" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M6 3 H14 L19 8 V21 H6 Z M14 3 V8 H19" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M8.5 12 H15 M8.5 15.5 H15" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M7 9 V3 H17 V9" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M7 17 H4 V10 a1 1 0 0 1 1-1 H19 a1 1 0 0 1 1 1 V17 H17" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<rect x="7" y="14" width="10" height="7" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 519 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M5 6 H19 M5 12 H19 M5 18 H14" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5 6 V8 M9 6 V8 M13 6 V8 M17 6 V8" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="12" cy="12" r="8.2" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<ellipse cx="12" cy="12" rx="8.2" ry="3" stroke="#4cc9f0" stroke-width="1.3" fill="none"/>
<ellipse cx="12" cy="12" rx="3" ry="8.2" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 455 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="9" cy="12" r="6.2" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="15.5" cy="12" r="6.2" stroke="#4cc9f0" stroke-width="1.4" fill="none" stroke-dasharray="3 2.2"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M5 9 H19 M16 6 L19 9 L16 12" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M19 15 H5 M8 12 L5 15 L8 18" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<ellipse cx="12" cy="12" rx="9" ry="5.2" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<ellipse cx="12" cy="12" rx="3.6" ry="1.9" stroke="#4cc9f0" stroke-width="1.4" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12 H20 M14 6 L20 12 L14 18"/></svg>

After

Width:  |  Height:  |  Size: 201 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" stroke="none"><path d="M13 2 L4 13 H11 L10 22 L20 10 H12 Z"/></svg>

After

Width:  |  Height:  |  Size: 143 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12.5 L9.5 18 L20 6"/></svg>

After

Width:  |  Height:  |  Size: 195 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6 L18 18 M18 6 L6 18"/></svg>

After

Width:  |  Height:  |  Size: 197 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3 H14 L19 8 V21 H6 Z M14 3 V8 H19 M8.5 12 H15 M8.5 15.5 H15 M8.5 8.5 H11"/></svg>

After

Width:  |  Height:  |  Size: 249 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3 H14 L19 8 V21 H6 Z M14 3 V8 H19"/></svg>

After

Width:  |  Height:  |  Size: 210 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" stroke="none"><circle cx="12" cy="12" r="4.5"/></svg>

After

Width:  |  Height:  |  Size: 129 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6 H9 L11 8 H21 V19 H3 Z"/></svg>

After

Width:  |  Height:  |  Size: 200 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.2"/><path d="M12 2 V5 M12 19 V22 M2 12 H5 M19 12 H22 M4.9 4.9 L7 7 M17 17 L19.1 19.1 M19.1 4.9 L17 7 M7 17 L4.9 19.1"/></svg>

After

Width:  |  Height:  |  Size: 310 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" stroke="none"><path d="M12 21 C12 21 3 14.5 3 8.8 A4.8 4.8 0 0 1 12 6.5 A4.8 4.8 0 0 1 21 8.8 C21 14.5 12 21 12 21 Z"/></svg>

After

Width:  |  Height:  |  Size: 201 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9 9.5 a3 3 0 0 1 5.5 1.6 c0 2-2.5 2.4-2.5 4"/><path d="M12 17.5 L12 17.5"/></svg>

After

Width:  |  Height:  |  Size: 278 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round"><path d="M4 7 H20 M4 12 H20 M4 17 H20"/></svg>

After

Width:  |  Height:  |  Size: 176 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 V21 M3 12 H21 M12 3 L9 6 M12 3 L15 6 M12 21 L9 18 M12 21 L15 18 M3 12 L6 9 M3 12 L6 15 M21 12 L18 9 M21 12 L18 15"/></svg>

After

Width:  |  Height:  |  Size: 293 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2.2" stroke-linecap="round"><path d="M12 5 L12 19 M5 12 L19 12"/></svg>

After

Width:  |  Height:  |  Size: 175 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M7 9 V3 H17 V9 M7 17 H4 V10 a1 1 0 0 1 1-1 H19 a1 1 0 0 1 1 1 V17 H17 M7 14 H17 V21 H7 Z M16 12 H17"/></svg>

After

Width:  |  Height:  |  Size: 273 B

+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="#000000"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
version="1.1"
id="svg2"
sodipodi:docname="redo.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="17.948527"
inkscape:cx="22.007378"
inkscape:cy="9.4436721"
inkscape:window-width="1920"
inkscape:window-height="939"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="m 16,3 5,5 -5,5"
id="path1" />
<path
d="M 21,8 H 10 a 6,6 0 0 0 0,12 h 2"
id="path2" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4 H17 L20 7 V20 H4 Z M8 4 V9 H15 V4 M7 20 V14 H17 V20"/></svg>

After

Width:  |  Height:  |  Size: 230 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="1.5"/><path d="M4 12 H20" stroke-width="1.8"/></svg>

After

Width:  |  Height:  |  Size: 230 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="1.5"/><path d="M12 5 V19" stroke-width="1.8"/></svg>

After

Width:  |  Height:  |  Size: 230 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7 H20 M9 7 V5 a1 1 0 0 1 1-1 H14 a1 1 0 0 1 1 1 V7 M6 7 L7 20 a1 1 0 0 0 1 1 H16 a1 1 0 0 0 1-1 L18 7 M10 11 V17 M14 11 V17"/></svg>

After

Width:  |  Height:  |  Size: 300 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><path d="M2 4.5 L10 4.5 L6 9 Z" fill="#000000"/></svg>

After

Width:  |  Height:  |  Size: 115 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="#000000"><path d="M7.5 2 L7.5 10 L3 6 Z"/></svg>

After

Width:  |  Height:  |  Size: 115 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><path d="M4.5 2 L4.5 10 L9 6 Z" fill="#000000"/></svg>

After

Width:  |  Height:  |  Size: 115 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><path d="M2 7.5 L10 7.5 L6 3 Z" fill="#000000"/></svg>

After

Width:  |  Height:  |  Size: 115 B

+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="#000000"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
version="1.1"
id="svg2"
sodipodi:docname="undo.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="17.852059"
inkscape:cx="8.0102804"
inkscape:cy="15.376377"
inkscape:window-width="1920"
inkscape:window-height="939"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M 8,3 3,8 8,13"
id="path1" />
<path
d="m 3,8 h 11 a 6,6 0 0 1 0,12 h -2"
id="path2" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4 L12 20 M5 11 L12 4 L19 11"/></svg>

After

Width:  |  Height:  |  Size: 205 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<circle cx="9" cy="12" r="6.2" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="15" cy="12" r="6.2" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 352 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<rect x="3" y="5" width="18" height="14" rx="1" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<rect x="6.5" y="8" width="11" height="8" rx="0.5" stroke="#4cc9f0" stroke-width="1.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 388 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M4 18 L4 8 L16 18 Z" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M4 8 L8 6 L20 16 L16 18" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M20 16 L20 16" stroke="#e0e0e0" stroke-width="1.7" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M4 18 L8 6" stroke="#4cc9f0" stroke-width="1.3" fill="none" opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 561 B

+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);