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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-18 12:29:32 +03:00
commit 864dd6cef7
81 changed files with 782 additions and 372 deletions

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
assets/icons/box3d.svg Normal file
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
assets/icons/bylayer.svg Normal file
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

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

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
assets/icons/cone3d.svg Normal file
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

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

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

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

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

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
assets/icons/plot.svg Normal file
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

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

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

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
assets/icons/sync.svg Normal file
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
assets/icons/torus3d.svg Normal file
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

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
assets/icons/ui/bolt.svg Normal file
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

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

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
assets/icons/ui/doc.svg Normal file
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

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
assets/icons/ui/dot.svg Normal file
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

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
assets/icons/ui/gear.svg Normal file
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

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
assets/icons/ui/help.svg Normal file
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
assets/icons/ui/menu.svg Normal file
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
assets/icons/ui/move.svg Normal file
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
assets/icons/ui/plus.svg Normal file
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

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
assets/icons/ui/redo.svg Normal file
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
assets/icons/ui/save.svg Normal file
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

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

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

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

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

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

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

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
assets/icons/ui/undo.svg Normal file
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
assets/icons/ui/up.svg Normal file
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
assets/icons/union.svg Normal file
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

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
assets/icons/wedge3d.svg Normal file
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

View file

@ -367,13 +367,17 @@ impl OpenCADStudio {
if !is_paper && !tab.is_start { if !is_paper && !tab.is_start {
// Render-mode picker + viewport-split buttons (horizontal / // Render-mode picker + viewport-split buttons (horizontal /
// vertical divider of the active Model tile). // vertical divider of the active Model tile).
let split_btn = |glyph: &'static str, horizontal: bool| { let split_btn = |bytes: &'static [u8], horizontal: bool| {
button(text(glyph).size(13).color(Color { button(crate::ui::icons::tinted(
r: 0.85, bytes,
g: 0.85, 14.0,
b: 0.85, Color {
a: 1.0, r: 0.85,
})) g: 0.85,
b: 0.85,
a: 1.0,
},
))
.on_press(Message::SplitModelViewport(horizontal)) .on_press(Message::SplitModelViewport(horizontal))
.padding([4, 8]) .padding([4, 8])
.style(|_: &Theme, status| iced::widget::button::Style { .style(|_: &Theme, status| iced::widget::button::Style {
@ -410,7 +414,12 @@ impl OpenCADStudio {
..Default::default() ..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 // 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 // it follows the active panel in a tiled layout (full canvas when
// a single tile fills the window). Leading Spaces offset it. // 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)); let mut col = column![].spacing(0).width(iced::Length::Fixed(row_w));
for (idx, name) in popup.items.iter().enumerate() { for (idx, name) in popup.items.iter().enumerate() {
let is_cur = popup.current == Some(idx); let is_cur = popup.current == Some(idx);
let label = format!("{} {}", if is_cur { "✓" } else { " " }, name); let mark: Element<'_, Message> = if is_cur {
let btn = button(text(label).size(12).color(Color::WHITE)) crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, Color::WHITE)
.on_press(Message::VisibilityPick(idx)) } 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]) .padding([3, 10])
.width(Fill) .width(Fill)
.style(move |_: &Theme, status| iced::widget::button::Style { .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() { for (idx, tab) in tabs.iter().enumerate() {
let is_active = idx == active_tab; let is_active = idx == active_tab;
let name = crate::ui::text_util::elide(&tab.tab_display_name(), 24); let name = crate::ui::text_util::elide(&tab.tab_display_name(), 24);
let label = if tab.dirty { let title_inner: Element<'_, Message> = if tab.dirty {
format!("● {}", name) 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 { } 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)) .on_press(Message::TabSwitch(idx))
.padding([5, 12]) .padding([5, 12])
.style(move |_: &Theme, status| button::Style { .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 { let row_inner: Row<'_, Message> = if tab.is_start {
row![title_btn].spacing(0).align_y(iced::Center) row![title_btn].spacing(0).align_y(iced::Center)
} else { } else {
let close_btn = button(text("×").size(11).color(Color { let close_btn = button(crate::ui::icons::tinted(
r: 0.55, crate::ui::icons::CLOSE,
g: 0.55, 10.0,
b: 0.55, Color {
a: 1.0, r: 0.55,
})) g: 0.55,
b: 0.55,
a: 1.0,
},
))
.on_press(Message::TabClose(idx)) .on_press(Message::TabClose(idx))
.padding([3, 5]) .padding([3, 5])
.style(move |_: &Theme, status| button::Style { .style(move |_: &Theme, status| button::Style {
@ -2825,12 +2864,35 @@ fn viewport_context_menu_overlay(
Message::Command("COPY".to_string()), Message::Command("COPY".to_string()),
)); ));
items.push(sep()); items.push(sep());
let arrow = if draworder_open { let do_caret = if draworder_open {
"Draw Order \u{25be}" crate::ui::icons::arrow_down(9.0, TEXT_COL)
} else { } 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 { if draworder_open {
items.push(subitem( items.push(subitem(
"Bring to Front".to_string(), "Bring to Front".to_string(),
@ -3353,7 +3415,7 @@ fn save_as_dialog_window<'a>(
let path_bar = row![ let path_bar = row![
{ {
let up_msg = up_path.map(Message::SaveDialogNavigate); 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 { .style(|_: &Theme, st| button::Style {
background: Some(Background::Color( background: Some(Background::Color(
if matches!(st, button::Status::Hovered | button::Status::Pressed) { 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 let rows: Vec<Element<'_, Message>> = entries
.iter() .iter()
.map(|(name, is_dir, path)| { .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 color = if *is_dir { DIR_COL } else { FILE_COL };
let p = path.clone(); let p = path.clone();
let d = *is_dir; let d = *is_dir;
mouse_area( mouse_area(
container( container(
row![ row![
text(icon).size(13), crate::ui::icons::tinted(icon_bytes, 13.0, color),
Space::new().width(6), Space::new().width(6),
text(crate::ui::text_util::elide(name.as_str(), 48)) text(crate::ui::text_util::elide(name.as_str(), 48))
.size(13) .size(13)
@ -3792,9 +3858,10 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> {
let donate_btn = { let donate_btn = {
button( button(
row![ 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), text("Donate").size(14).color(Color::WHITE),
] ]
.spacing(5)
.align_y(iced::Center), .align_y(iced::Center),
) )
.on_press(Message::RibbonToolClick { .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 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)) .on_press(Message::RecentRemove(path_for_remove))
.padding([4, 8]) .padding([4, 8])
.style(|_: &Theme, status| button::Style { .style(|_: &Theme, status| button::Style {

View file

@ -193,25 +193,33 @@ impl CadModule for AnnotateModule {
RibbonItem::Tool(crate::modules::ToolDef { RibbonItem::Tool(crate::modules::ToolDef {
id: "ANNOSCALE", id: "ANNOSCALE",
label: "Scale List", 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()), event: crate::modules::ModuleEvent::Command("ANNOSCALE".to_string()),
}), }),
RibbonItem::Tool(crate::modules::ToolDef { RibbonItem::Tool(crate::modules::ToolDef {
id: "OBJECTSCALE", id: "OBJECTSCALE",
label: "Add Scale", 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()), event: crate::modules::ModuleEvent::Command("OBJECTSCALE".to_string()),
}), }),
RibbonItem::Tool(crate::modules::ToolDef { RibbonItem::Tool(crate::modules::ToolDef {
id: "SCALELISTEDIT", id: "SCALELISTEDIT",
label: "Scale Edit", 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()), event: crate::modules::ModuleEvent::Command("SCALELISTEDIT".to_string()),
}), }),
RibbonItem::Tool(crate::modules::ToolDef { RibbonItem::Tool(crate::modules::ToolDef {
id: "SYNCPVIEWPORTS", id: "SYNCPVIEWPORTS",
label: "Sync Scales", 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()), event: crate::modules::ModuleEvent::Command("SYNCPVIEWPORTS".to_string()),
}), }),
], ],

View file

@ -7,7 +7,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "COLOR", id: "COLOR",
label: "Color", label: "Color",
icon: IconKind::Glyph("🎨"), icon: IconKind::Svg(include_bytes!("../../../../assets/icons/color_palette.svg")),
event: ModuleEvent::Command("COLOR".to_string()), event: ModuleEvent::Command("COLOR".to_string()),
} }
} }

View file

@ -6,7 +6,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "BYLAYER", id: "BYLAYER",
label: "ByLayer", label: "ByLayer",
icon: IconKind::Glyph("⊟"), icon: IconKind::Svg(include_bytes!("../../../../assets/icons/bylayer.svg")),
event: ModuleEvent::Command("BYLAYER".to_string()), event: ModuleEvent::Command("BYLAYER".to_string()),
} }
} }

View file

@ -3,7 +3,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "BOX", id: "BOX",
label: "Box", label: "Box",
icon: IconKind::Glyph("⬜"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/box3d.svg")),
event: ModuleEvent::Command("BOX".to_string()), event: ModuleEvent::Command("BOX".to_string()),
} }
} }

View file

@ -3,7 +3,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "CLEAR", id: "CLEAR",
label: "Clear", label: "Clear",
icon: IconKind::Glyph("🗑"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/clear_tool.svg")),
event: ModuleEvent::ClearModels, event: ModuleEvent::ClearModels,
} }
} }

View file

@ -3,7 +3,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "CYLINDER", id: "CYLINDER",
label: "Cylinder", label: "Cylinder",
icon: IconKind::Glyph("⬡"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/cylinder3d.svg")),
event: ModuleEvent::Command("CYLINDER".to_string()), event: ModuleEvent::Command("CYLINDER".to_string()),
} }
} }

View file

@ -3,7 +3,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "OPEN", id: "OPEN",
label: "Open", label: "Open",
icon: IconKind::Glyph("📂"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/import_obj.svg")),
event: ModuleEvent::OpenFileDialog, event: ModuleEvent::OpenFileDialog,
} }
} }

View file

@ -3,7 +3,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "SPHERE", id: "SPHERE",
label: "Sphere", label: "Sphere",
icon: IconKind::Glyph("⬤"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/sphere3d.svg")),
event: ModuleEvent::Command("SPHERE".to_string()), event: ModuleEvent::Command("SPHERE".to_string()),
} }
} }

View file

@ -14,13 +14,13 @@ pub fn paper_space_tools() -> Vec<ToolDef> {
ToolDef { ToolDef {
id: "PAGESETUP", id: "PAGESETUP",
label: "Page Setup", label: "Page Setup",
icon: IconKind::Glyph("📋"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/pagesetup.svg")),
event: ModuleEvent::Command("PAGESETUP".to_string()), event: ModuleEvent::Command("PAGESETUP".to_string()),
}, },
ToolDef { ToolDef {
id: "PLOT", id: "PLOT",
label: "Export PDF", label: "Export PDF",
icon: IconKind::Glyph("🖨"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")),
event: ModuleEvent::Command("PLOT".to_string()), event: ModuleEvent::Command("PLOT".to_string()),
}, },
] ]
@ -48,14 +48,14 @@ impl CadModule for LayoutModule {
ToolDef { ToolDef {
id: "PAGESETUP", id: "PAGESETUP",
label: "Page Setup", label: "Page Setup",
icon: IconKind::Glyph("📋"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/pagesetup.svg")),
event: ModuleEvent::Command("PAGESETUP".to_string()), event: ModuleEvent::Command("PAGESETUP".to_string()),
} }
.into(), .into(),
ToolDef { ToolDef {
id: "PLOT", id: "PLOT",
label: "Export PDF", label: "Export PDF",
icon: IconKind::Glyph("🖨"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")),
event: ModuleEvent::Command("PLOT".to_string()), event: ModuleEvent::Command("PLOT".to_string()),
} }
.into(), .into(),

View file

@ -19,7 +19,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "MVIEW", id: "MVIEW",
label: "Viewport", label: "Viewport",
icon: IconKind::Glyph("⬜"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/viewport.svg")),
event: ModuleEvent::Command("MVIEW".to_string()), event: ModuleEvent::Command("MVIEW".to_string()),
} }
} }

View file

@ -19,7 +19,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "MVIEW", id: "MVIEW",
label: "Viewport", label: "Viewport",
icon: IconKind::Glyph("⬜"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/viewport.svg")),
event: ModuleEvent::Command("MVIEW".to_string()), event: ModuleEvent::Command("MVIEW".to_string()),
} }
} }

View file

@ -4,7 +4,7 @@ pub fn tool() -> ToolDef {
ToolDef { ToolDef {
id: "OPTIONS", id: "OPTIONS",
label: "Options", label: "Options",
icon: IconKind::Glyph("⚙"), icon: IconKind::Svg(include_bytes!("../../../assets/icons/options_tool.svg")),
event: ModuleEvent::Command("OPTIONS".to_string()), event: ModuleEvent::Command("OPTIONS".to_string()),
} }
} }

View file

@ -12,12 +12,22 @@ use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem,
pub struct ModelModule; 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. /// 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 { ToolDef {
id, id,
label, label,
icon: IconKind::Glyph(glyph), icon: IconKind::Svg(icon),
event: ModuleEvent::Command(id.to_string()), event: ModuleEvent::Command(id.to_string()),
} }
} }
@ -35,16 +45,16 @@ impl CadModule for ModelModule {
RibbonGroup { RibbonGroup {
title: "Model", title: "Model",
tools: vec![ tools: vec![
RibbonItem::LargeTool(tool("BOX", "Box", "▦")), RibbonItem::LargeTool(tool("BOX", "Box", BOX_ICON)),
RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", "⬭")), RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", CYLINDER_ICON)),
RibbonItem::LargeTool(tool("CONE", "Cone", "△")), RibbonItem::LargeTool(tool("CONE", "Cone", CONE_ICON)),
RibbonItem::LargeTool(tool("SPHERE", "Sphere", "◯")), RibbonItem::LargeTool(tool("SPHERE", "Sphere", SPHERE_ICON)),
RibbonItem::Dropdown { RibbonItem::Dropdown {
id: "MODEL_MORE", id: "MODEL_MORE",
icon: IconKind::Glyph("◰"), icon: IconKind::Svg(WEDGE_ICON),
items: vec![ items: vec![
("WEDGE", "Wedge", IconKind::Glyph("◣")), ("WEDGE", "Wedge", IconKind::Svg(WEDGE_ICON)),
("TORUS", "Torus", IconKind::Glyph("◎")), ("TORUS", "Torus", IconKind::Svg(TORUS_ICON)),
], ],
default: "WEDGE", default: "WEDGE",
}, },
@ -53,9 +63,9 @@ impl CadModule for ModelModule {
RibbonGroup { RibbonGroup {
title: "Design", title: "Design",
tools: vec![ tools: vec![
RibbonItem::LargeTool(tool("UNION", "Union", "⊕")), RibbonItem::LargeTool(tool("UNION", "Union", UNION_ICON)),
RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", "⊖")), RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", SUBTRACT_ICON)),
RibbonItem::LargeTool(tool("INTERSECT", "Intersect", "⊗")), RibbonItem::LargeTool(tool("INTERSECT", "Intersect", INTERSECT_ICON)),
], ],
}, },
] ]

View file

@ -18,7 +18,7 @@ use crate::app::Message;
// ── Menu item definitions ───────────────────────────────────────────────── // ── Menu item definitions ─────────────────────────────────────────────────
struct MenuItem { struct MenuItem {
icon: &'static str, icon: &'static [u8],
label: &'static str, label: &'static str,
desc: &'static str, desc: &'static str,
command: &'static str, command: &'static str,
@ -26,43 +26,43 @@ struct MenuItem {
const MENU_ITEMS: &[MenuItem] = &[ const MENU_ITEMS: &[MenuItem] = &[
MenuItem { MenuItem {
icon: "◻", icon: crate::ui::icons::DOC_NEW,
label: "New", label: "New",
desc: "Create a new drawing", desc: "Create a new drawing",
command: "NEW", command: "NEW",
}, },
MenuItem { MenuItem {
icon: "📂", icon: crate::ui::icons::FOLDER,
label: "Open", label: "Open",
desc: "Open an existing file", desc: "Open an existing file",
command: "OPEN", command: "OPEN",
}, },
MenuItem { MenuItem {
icon: "💾", icon: crate::ui::icons::SAVE,
label: "Save", label: "Save",
desc: "Save the current drawing", desc: "Save the current drawing",
command: "SAVE", command: "SAVE",
}, },
MenuItem { MenuItem {
icon: "📄", icon: crate::ui::icons::DOC,
label: "Save As", label: "Save As",
desc: "Save as DWG or DXF", desc: "Save as DWG or DXF",
command: "SAVEAS", command: "SAVEAS",
}, },
MenuItem { MenuItem {
icon: "🖨", icon: crate::ui::icons::PRINT,
label: "Print", label: "Print",
desc: "Print or plot the drawing", desc: "Print or plot the drawing",
command: "PRINT", command: "PRINT",
}, },
MenuItem { MenuItem {
icon: "⚙", icon: crate::ui::icons::GEAR,
label: "Options", label: "Options",
desc: "Open application settings", desc: "Open application settings",
command: "OPTIONS", command: "OPTIONS",
}, },
MenuItem { MenuItem {
icon: "❓", icon: crate::ui::icons::HELP,
label: "Help", label: "Help",
desc: "Open help documentation", desc: "Open help documentation",
command: "HELP", command: "HELP",
@ -336,7 +336,7 @@ impl AppMenu {
fn menu_item_btn<'a>(item: &'a MenuItem) -> Element<'a, Message> { fn menu_item_btn<'a>(item: &'a MenuItem) -> Element<'a, Message> {
button( button(
row![ row![
container(text(item.icon).size(16).color(ICON_COLOR)) container(crate::ui::icons::tinted(item.icon, 16.0, ICON_COLOR))
.width(32) .width(32)
.padding([0, 4]), .padding([0, 4]),
column![ column![
@ -386,7 +386,8 @@ fn recent_item_btn(path: &PathBuf) -> Element<'_, Message> {
let path_str = path.display().to_string(); let path_str = path.display().to_string();
button( button(
row![ 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![ column![
text(name).size(11).color(Color::WHITE), text(name).size(11).color(Color::WHITE),
text(type_label).size(9).color(DIM_COLOR), text(type_label).size(9).color(DIM_COLOR),

View file

@ -125,7 +125,7 @@ pub fn color_selector<'a>(
row![ row![
swatch(cur_bg), swatch(cur_bg),
text(cur_name).size(11).color(TEXT), 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) .spacing(5)
.align_y(iced::Center), .align_y(iced::Center),

View file

@ -397,13 +397,13 @@ impl CommandLine {
// full backlog (everything pushed since the app started) so the // full backlog (everything pushed since the app started) so the
// user can recover anything that has already faded off the // user can recover anything that has already faded off the
// overlay. // overlay.
let dropdown_label = if self.history_open { "▾" } else { "▸" }; let dropdown_icon = if self.history_open {
let dropdown_btn = button( crate::ui::icons::arrow_down(11.0, PROMPT_COLOR)
text(dropdown_label) } else {
.size(11) crate::ui::icons::arrow_right(11.0, PROMPT_COLOR)
.color(PROMPT_COLOR), };
) let dropdown_btn = button(dropdown_icon)
.on_press(Message::CommandHistoryToggle) .on_press(Message::CommandHistoryToggle)
.style(|_: &Theme, _status| button::Style { .style(|_: &Theme, _status| button::Style {
background: Some(Background::Color(INPUT_ROW_BG)), background: Some(Background::Color(INPUT_ROW_BG)),
text_color: Color::WHITE, text_color: Color::WHITE,

142
src/ui/icons.rs Normal file
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)
}

View file

@ -170,9 +170,19 @@ impl LayerPanel {
// ── Toolbar ─────────────────────────────────────────────────────── // ── Toolbar ───────────────────────────────────────────────────────
let toolbar = container( let toolbar = container(
row![ row![
toolbar_btn("⊕ New", Message::LayerNew), toolbar_btn(crate::ui::icons::PLUS, "New", Message::LayerNew),
toolbar_btn_cond("⊗ Delete", Message::LayerDelete, has_sel && !sel_is_zero), toolbar_btn_cond(
toolbar_btn_cond("✓ Set Current", Message::LayerSetCurrent, has_sel), 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), .spacing(2),
) )
@ -289,9 +299,16 @@ impl LayerPanel {
// ── Toolbar buttons ─────────────────────────────────────────────────────── // ── Toolbar buttons ───────────────────────────────────────────────────────
fn toolbar_btn(label: &str, msg: Message) -> Element<'_, Message> { fn toolbar_btn<'a>(icon: &'static [u8], label: &'a str, msg: Message) -> Element<'a, Message> {
button(text(label).size(11).color(Color::WHITE)) button(
.on_press(msg) 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 { .style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status { background: Some(Background::Color(match status {
button::Status::Hovered => Color { button::Status::Hovered => Color {
@ -325,8 +342,13 @@ fn toolbar_btn(label: &str, msg: Message) -> Element<'_, Message> {
.into() .into()
} }
fn toolbar_btn_cond(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> { fn toolbar_btn_cond<'a>(
let mut b = button(text(label).size(11).color(if enabled { icon: &'static [u8],
label: &'a str,
msg: Message,
enabled: bool,
) -> Element<'a, Message> {
let fg = if enabled {
Color::WHITE Color::WHITE
} else { } else {
Color { Color {
@ -335,7 +357,15 @@ fn toolbar_btn_cond(label: &str, msg: Message, enabled: bool) -> Element<'_, Mes
b: 0.45, b: 0.45,
a: 1.0, 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 { .style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status { background: Some(Background::Color(match status {
button::Status::Hovered => Color { button::Status::Hovered => Color {
@ -431,42 +461,32 @@ fn layer_row<'a>(
.into() .into()
}; };
let vis_svg: &'static [u8] = if layer.visible { let vis_svg = crate::ui::icons::layer_visible(layer.visible);
include_bytes!("../../assets/icons/layers/layon.svg") let frz_svg = crate::ui::icons::layer_freeze(layer.frozen);
} else { let lck_svg = crate::ui::icons::layer_lock(layer.locked);
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 status_dot: Element<'_, Message> = if is_current { let status_dot: Element<'_, Message> = if is_current {
text("✓") crate::ui::icons::tinted(
.size(13) crate::ui::icons::CHECK,
.color(Color { 13.0,
Color {
r: 0.25, r: 0.25,
g: 0.85, g: 0.85,
b: 0.45, b: 0.45,
a: 1.0, a: 1.0,
}) },
.into() )
} else { } else {
text("▣") crate::ui::icons::tinted(
.size(11) crate::ui::icons::DOT,
.color(Color { 9.0,
Color {
r: 0.55, r: 0.55,
g: 0.55, g: 0.55,
b: 0.55, b: 0.55,
a: 1.0, a: 1.0,
}) },
.into() )
}; };
// Name cell // Name cell
@ -693,11 +713,7 @@ fn layer_row<'a>(
// Per-viewport freeze columns // Per-viewport freeze columns
for (vp_idx, _vp_col) in vp_cols.iter().enumerate() { 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 is_vp_frozen = layer.vp_frozen.get(vp_idx).copied().unwrap_or(false);
let vp_frz_svg: &'static [u8] = if is_vp_frozen { let vp_frz_svg = crate::ui::icons::layer_freeze(is_vp_frozen);
include_bytes!("../../assets/icons/layers/layfrz.svg")
} else {
include_bytes!("../../assets/icons/layers/laythw.svg")
};
row_content = row_content.push( row_content = row_content.push(
container(svg_btn( container(svg_btn(
vp_frz_svg, vp_frz_svg,

View file

@ -194,14 +194,28 @@ pub fn view_window<'a>(
}) })
.padding([4, 10]), .padding([4, 10]),
Space::new().width(Fill), Space::new().width(Fill),
button(text("◀ Move Left").size(11)) button(
.on_press(Message::LayoutManagerMoveLeft) row![
.style(btn_s(false)) crate::ui::icons::tinted(crate::ui::icons::TRI_LEFT_B, 9.0, Color::WHITE),
.padding([4, 8]), text("Move Left").size(11),
button(text("Move Right ▶").size(11)) ]
.on_press(Message::LayoutManagerMoveRight) .spacing(4)
.style(btn_s(false)) .align_y(iced::Center),
.padding([4, 8]), )
.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)) button(text("Set Current").size(11))
.on_press(Message::LayoutManagerSetCurrent) .on_press(Message::LayoutManagerSetCurrent)
.style(btn_s(true)) .style(btn_s(true))
@ -223,12 +237,17 @@ pub fn view_window<'a>(
.map(|name| { .map(|name| {
let is_sel = name.as_str() == selected; let is_sel = name.as_str() == selected;
let is_cur = name.as_str() == current.as_str(); let is_cur = name.as_str() == current.as_str();
let label = if is_cur { let mut item_row = row![text(name.clone()).size(12)]
format!("{name} ◀") .spacing(5)
} else { .align_y(iced::Center);
name.clone() if is_cur {
}; item_row = item_row.push(crate::ui::icons::tinted(
button(text(label).size(12)) crate::ui::icons::TRI_LEFT_B,
8.0,
Color::WHITE,
));
}
button(item_row)
.on_press(Message::LayoutManagerSelect(name.clone())) .on_press(Message::LayoutManagerSelect(name.clone()))
.style(list_item(is_sel)) .style(list_item(is_sel))
.padding([5, 10]) .padding([5, 10])
@ -286,7 +305,7 @@ pub fn view_window<'a>(
row![ row![
text("Status:").size(11).color(DIM).width(80), text("Status:").size(11).color(DIM).width(80),
text(if selected == current.as_str() { text(if selected == current.as_str() {
"Active ◀" "Active"
} else { } else {
"Inactive" "Inactive"
}) })

View file

@ -8,6 +8,7 @@ pub mod color_select;
pub mod command_line; pub mod command_line;
pub mod cycle_popup; pub mod cycle_popup;
pub mod dimstyle; pub mod dimstyle;
pub mod icons;
pub mod isolate_popup; pub mod isolate_popup;
pub mod layers; pub mod layers;
pub mod layout_manager; pub mod layout_manager;

View file

@ -6,7 +6,7 @@
//! so both stack dialogs here — one code path for every platform. //! so both stack dialogs here — one code path for every platform.
use crate::app::Message; 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}; use iced::{Background, Border, Color, Element, Length, Padding, Theme, Vector};
const PANEL: Color = Color { const PANEL: Color = Color {
@ -49,17 +49,26 @@ pub fn modal<'a>(
on_close: Message, on_close: Message,
offset: Vector, offset: Vector,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let close = button(text("✕").size(15)) let close = button(crate::ui::icons::tinted(
.on_press(on_close) crate::ui::icons::CLOSE,
.padding([1, 7]) 13.0,
.style(close_style); 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 // 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 // `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 // screen width — the dialog stays sized to its content. Pressing the grip
// starts a drag (handled in `update`). // starts a drag (handled in `update`).
let grip = mouse_area( 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]) .padding([1, 7])
.style(|_: &Theme| container::Style { .style(|_: &Theme| container::Style {
background: Some(Background::Color(TITLE_C)), background: Some(Background::Color(TITLE_C)),

View file

@ -329,12 +329,12 @@ pub fn view_window<'a>(
hdivider(), hdivider(),
section_label("Position"), section_label("Position"),
button( button(
text(if center { row![
"✓ Center on page" crate::ui::icons::check_cell(center, Color::WHITE),
} else { text("Center on page").size(11),
" Center on page" ]
}) .spacing(2)
.size(11) .align_y(iced::Center)
) )
.on_press(Message::PageSetupCenterToggle) .on_press(Message::PageSetupCenterToggle)
.style(pill(center)) .style(pill(center))

View file

@ -199,6 +199,27 @@ fn pill_button<'a>(label: &str, msg: Message, bg: Color) -> Element<'a, Message>
.into() .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 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 }; 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); .spacing(4);
if removable { if removable {
controls = controls.push(Space::new().width(6)); 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() controls.into()
} }

View file

@ -676,13 +676,14 @@ pub fn color_picker_dropdown<'a>(
// "More Colors…" toggle button // "More Colors…" toggle button
let more_btn = button( let more_btn = button(
text(if palette_open { row![
"▲ Less" crate::ui::icons::arrow_toggle(palette_open, 9.0, HINT_COLOR),
} else { text(if palette_open { "Less" } else { "More Colors…" })
"▼ More Colors…" .size(10)
}) .color(HINT_COLOR),
.size(10) ]
.color(HINT_COLOR), .spacing(4)
.align_y(iced::Center),
) )
.on_press(palette_toggle_msg) .on_press(palette_toggle_msg)
.style(|_: &Theme, _| button::Style { .style(|_: &Theme, _| button::Style {

View file

@ -234,20 +234,8 @@ impl Ribbon {
// ── Tab buttons ─────────────────────────────────────────────────── // ── Tab buttons ───────────────────────────────────────────────────
let history_controls = row![ let history_controls = row![
render_history_control( render_history_control("Undo", UNDO_HISTORY_ID, undo_count, &self.open_dropdown),
"↶", render_history_control("Redo", REDO_HISTORY_ID, redo_count, &self.open_dropdown),
"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) .spacing(TOP_HIST_GAP)
.align_y(iced::Center); .align_y(iced::Center);
@ -609,14 +597,13 @@ impl Ribbon {
.iter() .iter()
.map(|(cmd, label, item_icon)| { .map(|(cmd, label, item_icon)| {
let is_current = *cmd == last_cmd; let is_current = *cmd == last_cmd;
let checkmark = text(if is_current { "✓" } else { " " }) let checkmark: Element<'_, Message> = container(if is_current {
.size(11) crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
.color(if is_current { } else {
CHECK_COLOR iced::widget::Space::new().width(0).into()
} else { })
Color::TRANSPARENT .width(Length::Fixed(14.0))
}) .into();
.width(Length::Fixed(14.0));
let icon_el: Element<Message> = match *item_icon { let icon_el: Element<Message> = match *item_icon {
IconKind::Glyph(s) => text(s) IconKind::Glyph(s) => text(s)
.size(13) .size(13)
@ -717,59 +704,16 @@ impl Ribbon {
.width(12) .width(12)
.height(12); .height(12);
let vis = text(if lv { "●" } else { "○" }).size(10).color(if lv { let vis = crate::ui::icons::raw(crate::ui::icons::layer_visible(lv), 14.0);
Color { let freeze = crate::ui::icons::raw(crate::ui::icons::layer_freeze(lf), 14.0);
r: 0.95, let lock = crate::ui::icons::raw(crate::ui::icons::layer_lock(ll), 14.0);
g: 0.85, let checkmark: Element<'_, Message> = container(if is_active {
b: 0.20, crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
a: 1.0,
}
} else { } else {
Color { iced::widget::Space::new().width(0).into()
r: 0.45, })
g: 0.45, .width(Length::Fixed(14.0))
b: 0.45, .into();
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));
let label = let label =
text(&info.name) text(&info.name)
.size(11) .size(11)
@ -893,14 +837,13 @@ impl Ribbon {
.into_iter() .into_iter()
.map(|lt| { .map(|lt| {
let is_cur = lt.name == *active_lt; let is_cur = lt.name == *active_lt;
let check = text(if is_cur { "✓" } else { " " }) let check: Element<'_, Message> = container(if is_cur {
.size(11) crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
.color(if is_cur { } else {
CHECK_COLOR iced::widget::Space::new().width(0).into()
} else { })
Color::TRANSPARENT .width(Length::Fixed(14.0))
}) .into();
.width(Length::Fixed(14.0));
let name_col = text(lt.name.clone()) let name_col = text(lt.name.clone())
.size(11) .size(11)
.color(if is_cur { LABEL_ON } else { LABEL_OFF }) .color(if is_cur { LABEL_ON } else { LABEL_OFF })
@ -971,14 +914,13 @@ impl Ribbon {
.map(|item| { .map(|item| {
let is_cur = item.0 == active_lw; let is_cur = item.0 == active_lw;
let label = item.to_string(); let label = item.to_string();
let check = text(if is_cur { "✓" } else { " " }) let check: Element<'_, Message> = container(if is_cur {
.size(11) crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
.color(if is_cur { } else {
CHECK_COLOR iced::widget::Space::new().width(0).into()
} else { })
Color::TRANSPARENT .width(Length::Fixed(14.0))
}) .into();
.width(Length::Fixed(14.0));
button( button(
row![ row![
check, check,

View file

@ -11,6 +11,7 @@ use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
use crate::app::Message; use crate::app::Message;
use crate::modules::{IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef}; use crate::modules::{IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef};
use crate::ui::icons;
use crate::ui::properties::{acad_color_display, LwItem}; use crate::ui::properties::{acad_color_display, LwItem};
use super::LayerInfo; use super::LayerInfo;
@ -361,7 +362,7 @@ pub(super) fn render_small<'a>(
let arr_tip = format!("{} options", cur_label); let arr_tip = format!("{} options", cur_label);
let arr_btn = button( let arr_btn = button(
container(text("▾").size(7).color(ARROW_COLOR)) container(icons::arrow_down(8.0, ARROW_COLOR))
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.align_x(iced::Center) .align_x(iced::Center)
@ -513,7 +514,7 @@ pub(super) fn render_large<'a>(
}); });
let arr_btn = button( let arr_btn = button(
container(text("▾").size(9).color(ARROW_COLOR)) container(icons::arrow_down(9.0, ARROW_COLOR))
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.align_x(iced::Center) .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 ll = info.map(|l| l.locked).unwrap_or(false);
let is_open = open_dd.as_deref() == Some(LAYER_COMBO_ID); let is_open = open_dd.as_deref() == Some(LAYER_COMBO_ID);
let vis_icon = text(if lv { "●" } else { "○" }).size(10).color(if lv { let vis_icon = icons::raw(icons::layer_visible(lv), 14.0);
Color { let freeze_icon = icons::raw(icons::layer_freeze(lf), 14.0);
r: 0.95, let lock_icon = icons::raw(icons::layer_lock(ll), 14.0);
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 swatch = container(text("")) let swatch = container(text(""))
.style(move |_: &Theme| container::Style { .style(move |_: &Theme| container::Style {
background: Some(Background::Color(lc)), background: Some(Background::Color(lc)),
@ -625,7 +584,7 @@ pub(super) fn render_large<'a>(
.width(12) .width(12)
.height(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 name_w = (COMBO_W - ICONS_USED).max(40.0);
let combo_btn = button( let combo_btn = button(
@ -637,12 +596,15 @@ pub(super) fn render_large<'a>(
container(text(active_layer).size(11).color(Color::WHITE)) container(text(active_layer).size(11).color(Color::WHITE))
.width(name_w) .width(name_w)
.clip(true), .clip(true),
text("▾").size(9).color(Color { icons::arrow_down(
r: 0.7, 9.0,
g: 0.7, Color {
b: 0.7, r: 0.7,
a: 1.0 g: 0.7,
}), b: 0.7,
a: 1.0,
},
),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -801,12 +763,16 @@ pub(super) fn render_large<'a>(
container(text(label).size(10).color(Color::WHITE)) container(text(label).size(10).color(Color::WHITE))
.width(Fill) .width(Fill)
.clip(true), .clip(true),
text(if is_open { "▲" } else { "▼" }).size(7).color(Color { icons::arrow_toggle(
r: 0.6, is_open,
g: 0.6, 8.0,
b: 0.6, Color {
a: 1.0 r: 0.6,
}), g: 0.6,
b: 0.6,
a: 1.0,
},
),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -905,12 +871,16 @@ pub(super) fn render_large<'a>(
container(text(active.clone()).size(11).color(Color::WHITE)) container(text(active.clone()).size(11).color(Color::WHITE))
.width(Fill) .width(Fill)
.clip(true), .clip(true),
text(if is_open { "▲" } else { "▾" }).size(9).color(Color { icons::arrow_toggle(
r: 0.7, is_open,
g: 0.7, 9.0,
b: 0.7, Color {
a: 1.0 r: 0.7,
}), g: 0.7,
b: 0.7,
a: 1.0,
},
),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Center), .align_y(iced::Center),
@ -971,18 +941,21 @@ pub(super) fn render_large<'a>(
let key = style_key; let key = style_key;
button( button(
row![ row![
text(if is_sel { "✓" } else { " " }) container(if is_sel {
.size(10) icons::tinted(
.color(if is_sel { icons::CHECK,
10.0,
Color { Color {
r: 0.2, r: 0.2,
g: 0.8, g: 0.8,
b: 0.4, b: 0.4,
a: 1.0, a: 1.0,
} },
} else { )
Color::TRANSPARENT } else {
}), iced::widget::Space::new().width(0).into()
})
.width(12),
text(name).size(11).color(Color::WHITE), text(name).size(11).color(Color::WHITE),
] ]
.spacing(6) .spacing(6)
@ -1281,7 +1254,6 @@ pub fn module_event_to_message(event: ModuleEvent) -> Message {
// ── History control ──────────────────────────────────────────────────────── // ── History control ────────────────────────────────────────────────────────
pub(super) fn render_history_control<'a>( pub(super) fn render_history_control<'a>(
glyph: &'static str,
label: &'static str, label: &'static str,
dropdown_id: &'static str, dropdown_id: &'static str,
count: usize, count: usize,
@ -1289,12 +1261,20 @@ pub(super) fn render_history_control<'a>(
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let dd_open = open_dropdown.as_deref() == Some(dropdown_id); let dd_open = open_dropdown.as_deref() == Some(dropdown_id);
let active = count > 0; let active = count > 0;
let icon_color = if active { Color::WHITE } else { LABEL_OFF };
let main_btn = { 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( let btn = button(
text(glyph) container(glyph)
.size(14) .width(Fill)
.color(if active { Color::WHITE } else { LABEL_OFF }), .height(Fill)
.align_x(iced::Center)
.align_y(iced::Center),
) )
.style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status)) .style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status))
.width(Length::Fixed(TOP_HIST_W)) .width(Length::Fixed(TOP_HIST_W))
@ -1321,11 +1301,10 @@ pub(super) fn render_history_control<'a>(
let arrow_btn = { let arrow_btn = {
let btn = button( let btn = button(
container( container(icons::arrow_down(
text("▾") 8.0,
.size(7) if active { ARROW_COLOR } else { LABEL_OFF },
.color(if active { ARROW_COLOR } else { LABEL_OFF }), ))
)
.width(Fill) .width(Fill)
.height(Fill) .height(Fill)
.align_x(iced::Center) .align_x(iced::Center)

View file

@ -70,14 +70,7 @@ pub fn scale_popup_overlay(
} }
fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> { fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> {
let check = text(if active { "✓" } else { " " }) let check = crate::ui::icons::check_cell(active, CHECK_COLOR);
.size(11)
.color(if active {
CHECK_COLOR
} else {
Color::TRANSPARENT
})
.width(Length::Fixed(14.0));
let lbl = text(label) let lbl = text(label)
.size(11) .size(11)

View file

@ -60,10 +60,7 @@ pub fn selection_filter_popup_overlay(
} }
fn type_row(name: String, included: bool) -> Element<'static, Message> { fn type_row(name: String, included: bool) -> Element<'static, Message> {
let check = text(if included { "✓" } else { " " }) let check = crate::ui::icons::check_cell(included, CHECK_COLOR);
.size(11)
.color(if included { CHECK_COLOR } else { Color::TRANSPARENT })
.width(Length::Fixed(14.0));
let lbl = text(name.clone()) let lbl = text(name.clone())
.size(11) .size(11)

View file

@ -107,14 +107,7 @@ fn snap_row<'a>(
label: &'a str, label: &'a str,
active: bool, active: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let checkmark = text(if active { "✓" } else { " " }) let checkmark = crate::ui::icons::check_cell(active, CHECK_COLOR);
.size(11)
.color(if active {
CHECK_COLOR
} else {
Color::TRANSPARENT
})
.width(Length::Fixed(14.0));
let icon_el = text(icon) let icon_el = text(icon)
.size(11) .size(11)

View file

@ -76,7 +76,7 @@ impl StatusBar {
// Which pills the user has chosen to show on the bar. // Which pills the user has chosen to show on the bar.
config: &'a StatusBarConfig, config: &'a StatusBarConfig,
) -> Element<'a, Message> { ) -> 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())) .on_press(Message::Command("MENU".into()))
.style(|_: &Theme, _| button::Style { .style(|_: &Theme, _| button::Style {
background: Some(Background::Color(Color::TRANSPARENT)), background: Some(Background::Color(Color::TRANSPARENT)),
@ -285,7 +285,7 @@ fn format_coords(p: glam::Vec3) -> String {
// ── Customization handle ────────────────────────────────────────────────── // ── Customization handle ──────────────────────────────────────────────────
fn customize_btn() -> Element<'static, Message> { 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) .on_press(Message::ToggleStatusBarMenu)
.style(|_: &Theme, status| button::Style { .style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status { 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 OSNAP_OFF_TEXT
}; };
let left = button(text("⚡ OSNAP").size(10).color(text_color)) let left = button(
.on_press(Message::ToggleSnapEnabled) 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 { .style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status { background: Some(Background::Color(match status {
button::Status::Hovered => { button::Status::Hovered => {
@ -495,7 +502,7 @@ fn osnap_btn(active: bool, snap_enabled: bool, open: bool) -> Element<'static, M
}) })
.padding([2, 6]); .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) .on_press(Message::ToggleSnapPopup)
.style(move |_: &Theme, status| button::Style { .style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status { background: Some(Background::Color(match status {
@ -607,12 +614,16 @@ fn space_tab<'a>(
.padding([2, 6]) .padding([2, 6])
.width(Length::Fixed(90.0)); .width(Length::Fixed(90.0));
let cancel_btn = button(text("✕").size(10).color(Color { let cancel_btn = button(crate::ui::icons::tinted(
r: 0.65, crate::ui::icons::CLOSE,
g: 0.65, 10.0,
b: 0.65, Color {
a: 1.0, r: 0.65,
})) g: 0.65,
b: 0.65,
a: 1.0,
},
))
.on_press(Message::LayoutRenameCancel) .on_press(Message::LayoutRenameCancel)
.style(|_: &Theme, _| button::Style { .style(|_: &Theme, _| button::Style {
background: Some(Background::Color(Color::TRANSPARENT)), background: Some(Background::Color(Color::TRANSPARENT)),

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> { fn menu_row(label: &'static str, checked: bool, msg: Message) -> Element<'static, Message> {
let check = text(if checked { "✓" } else { " " }) let check = crate::ui::icons::check_cell(checked, CHECK_COLOR);
.size(11)
.color(if checked { CHECK_COLOR } else { Color::TRANSPARENT })
.width(Length::Fixed(14.0));
let lbl = text(label) let lbl = text(label)
.size(11) .size(11)

View file

@ -61,12 +61,8 @@ pub fn item<'a>(
} else { } else {
// Fixed-width ✓ column keeps every name left-aligned whether or not the // Fixed-width ✓ column keeps every name left-aligned whether or not the
// row is current. // row is current.
let check = if is_current { let check = crate::ui::icons::check_cell(is_current, CURRENT_CHECK);
text("✓").size(11).color(CURRENT_CHECK) let label = row![check, text(name.to_string()).size(11).color(TEXT)]
} else {
text("").size(11)
};
let label = row![check.width(14), text(name.to_string()).size(11).color(TEXT)]
.align_y(iced::Center); .align_y(iced::Center);
let cell = container(label) let cell = container(label)
.padding([4, 8]) .padding([4, 8])

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> { fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> {
let check = text(if active { "✓" } else { " " }) let check = crate::ui::icons::check_cell(active, CHECK_COLOR);
.size(11)
.color(if active { CHECK_COLOR } else { Color::TRANSPARENT })
.width(Length::Fixed(14.0));
let lbl = text(label) let lbl = text(label)
.size(11) .size(11)

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("* ")) { if let Some(rest) = trimmed.strip_prefix("- ").or_else(|| trimmed.strip_prefix("* ")) {
return row![ 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), text(strip_inline_md(rest)).size(11).color(WHITE),
] ]
.spacing(4) .spacing(4)
@ -165,8 +165,12 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> {
false, false,
); );
let latest_card = version_card("Latest", format!("v{}", latest), true); let latest_card = version_card("Latest", format!("v{}", latest), true);
let arrow = container(text("→").size(22).color(DIM)) let arrow = container(crate::ui::icons::tinted(
.width(iced::Length::Fixed(32.0)) crate::ui::icons::ARROW_LONG_RIGHT,
20.0,
DIM,
))
.width(iced::Length::Fixed(32.0))
.height(Fill) .height(Fill)
.align_x(iced::Center) .align_x(iced::Center)
.align_y(iced::Center); .align_y(iced::Center);