diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index dac21a5a9..7ca06f10f 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -7,10 +7,13 @@ run-name: ${{ github.event.release.tag_name || github.ref_name }} Web
on:
release:
types: [published]
+ discussion:
+ types: [created, edited, deleted, transferred, pinned, unpinned, labeled, unlabeled, locked, unlocked, category_changed, answered, unanswered]
workflow_dispatch:
permissions:
contents: read
+ discussions: read
pages: write
id-token: write
@@ -79,6 +82,58 @@ jobs:
> dist/supporters.json || echo '[]' > dist/supporters.json
echo "supporters: $(jq 'length' dist/supporters.json)"
+ # GitHub's Discussions API is authenticated GraphQL. Generate a public,
+ # token-free snapshot next to the web app; discussion activity (including
+ # pin/unpin) triggers this workflow so pinned entries stay at the top.
+ - name: Generate discussions.json
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ QUERY='
+ query($owner: String!, $name: String!) {
+ repository(owner: $owner, name: $name) {
+ pinnedDiscussions(first: 10) {
+ nodes {
+ discussion {
+ number title url updatedAt
+ author { login }
+ }
+ }
+ }
+ discussions(first: 50, orderBy: {field: UPDATED_AT, direction: DESC}) {
+ nodes {
+ number title url updatedAt
+ author { login }
+ }
+ }
+ }
+ }'
+ gh api graphql \
+ -f query="$QUERY" \
+ -F owner="$GITHUB_REPOSITORY_OWNER" \
+ -F name="${GITHUB_REPOSITORY#*/}" \
+ | jq -c '
+ .data.repository as $repo
+ | [$repo.pinnedDiscussions.nodes[].discussion.number] as $pinned
+ | (
+ [$repo.pinnedDiscussions.nodes[].discussion + {pinned: true}]
+ + [
+ $repo.discussions.nodes[]
+ | select((.number as $number | $pinned | index($number)) == null)
+ | . + {pinned: false}
+ ]
+ )
+ | map({
+ number,
+ title,
+ url,
+ author: (.author.login // ""),
+ updated_at: .updatedAt,
+ pinned
+ })' \
+ > dist/discussions.json
+ echo "discussions: $(jq 'length' dist/discussions.json)"
+
- uses: actions/upload-pages-artifact@v3
with:
path: dist
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index b408576ea..000000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Agent Instructions
-
-- Never run `cargo fmt`, `rustfmt`, or any automatic source-formatting command in this repository.
-- Preserve the existing formatting and make only targeted edits.
diff --git a/COMMANDS.md b/COMMANDS.md
deleted file mode 100644
index 780c90ec9..000000000
--- a/COMMANDS.md
+++ /dev/null
@@ -1,371 +0,0 @@
-# Open CAD Studio β Command Reference
-
-Status of every standard CAD command in Open CAD Studio:
-
-- β
**Implemented** β fully working
-- πΆ **Partial** β command is accepted but logic is a stub
-- β **Missing** β not yet implemented
-
----
-
-## Draw
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `LINE` | L | Straight line segment | β
|
-| `PLINE` | PL | Polyline | β
|
-| `ARC` | A | Arc | β
|
-| `CIRCLE` | C | Circle | β
|
-| `ELLIPSE` | EL | Ellipse | β
|
-| `RECTANGLE` | REC | Rectangle | β
|
-| `POLYGON` | POL | Regular polygon | β
|
-| `XLINE` | XL | Infinite construction line | β
|
-| `RAY` | β | One-way infinite line | β
|
-| `SPLINE` | SPL | NURBS spline | β
|
-| `SPLINEFIT` | FITSPLINE | Fit a spline through a polyline's points | β
|
-| `MLINE` | ML | Multiline | β
|
-| `POINT` | PO | Point | β
|
-| `DONUT` | DO | Filled ring | β
|
-| `HATCH` | H | Hatch fill | β
|
-| `GRADIENT` | GD | Gradient fill | β
|
-| `BOUNDARY` | BO | Boundary polyline / region | β
|
-| `REVCLOUD` | β | Revision cloud | β
|
-| `WIPEOUT` | WO | Wipeout mask | β
|
-| `MTEXT` | MT | Multiline text | β
|
-| `TEXT` | DT | Single-line text | β
|
-| `TABLE` | β | Table entity | β
|
-| `DIVIDE` | DIV | Divide entity into equal parts | β
|
-| `MEASURE` | ME | Divide entity at measured intervals | β
|
-| `3DPOLY` | β | 3D polyline | β
|
-| `HELIX` | β | 3D helix | β
|
-| `TRACE` | β | Thick 2D line (legacy) | β
|
-| `SKETCH` | β | Freehand sketch | β
|
-| `SOLID` | SO | Filled 2D shape (legacy) | β
|
-| `MINSERT` | β | Matrix block insert | β
|
-| `REGION` | REG | 2D closed region | β
|
-| `FIELD` | β | Auto-updating text field | β |
-
----
-
-## Modify
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `MOVE` | M | Move | β
|
-| `COPY` | CO | Copy | β
|
-| `ROTATE` | RO | Rotate | β
|
-| `SCALE` | SC | Scale | β
|
-| `MIRROR` | MI | Mirror | β
|
-| `OFFSET` | O | Offset | β
|
-| `TRIM` | TR | Trim | β
|
-| `EXTEND` | EX | Extend | β
|
-| `STRETCH` | S | Stretch | β
|
-| `FILLET` | F | Fillet | β
|
-| `CHAMFER` | CHA | Chamfer | β
|
-| `ARRAY` | AR | Array | β
|
-| `ARRAYRECT` | β | Rectangular array | β
|
-| `ARRAYPOLAR` | β | Polar array | β
|
-| `ARRAYPATH` | β | Path array | β
|
-| `BREAK` | BR | Break entity | β
|
-| `BREAKATPOINT` | β | Break at point | β
|
-| `JOIN` | J | Join entities | β
|
-| `EXPLODE` | X | Explode compound entity | β
|
-| `ERASE` | E | Erase | β
|
-| `LENGTHEN` | LEN | Lengthen / shorten | β
|
-| `PEDIT` | PE | Edit polyline | β
|
-| `SPLINEDIT` | SPE | Edit spline | β
|
-| `MATCHPROP` | MA | Match properties | β
|
-| `SCALETEXT` | β | Scale text objects | β
|
-| `FLATTEN` | β | Flatten 3D to 2D | β
|
-| `DRAWORDER` | DR | Draw order | β
|
-| `ALIGN` | AL | Align | β
|
-| `GROUP` | G | Group | β
|
-| `UNGROUP` | UG | Ungroup | β
|
-| `OVERKILL` | β | Remove duplicate geometry | πΆ |
-| `3DMOVE` | β | 3D move | β
|
-| `3DROTATE` | ROTATE3D | 3D rotate (cached solid) | β
|
-| `3DMIRROR` | MIRROR3D | 3D mirror (cached solid) | β
|
-| `3DALIGN` | ALIGN3D | 3D align (cached solid) | β
|
-| `3DARRAY` | ARRAY3D | 3D array | β
|
-| `SLICE` | SL | Slice solid with plane | β
|
-| `SUBTRACT` | SU | Subtract solids | β
|
-| `UNION` | UNI | Union solids | β
|
-| `INTERSECT` | IN | Intersect solids | β
|
-| `CHAMFERSOLID` | β | Chamfer solid edge | β |
-| `FILLETEDGE` | β | Fillet solid edge | β |
-
----
-
-## Dimension
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `DIMLINEAR` | DLI | Linear dimension | β
|
-| `DIMALIGNED` | DAL | Aligned dimension | β
|
-| `DIMANGULAR` | DAN | Angular dimension | β
|
-| `DIMRADIUS` | DRA | Radius dimension | β
|
-| `DIMDIAMETER` | DDI | Diameter dimension | β
|
-| `DIMORDINATE` | DOR | Ordinate dimension | β
|
-| `DIMCONTINUE` | DCO | Continue dimension | β
|
-| `DIMBASELINE` | DBA | Baseline dimension | β
|
-| `QDIM` | β | Quick dimension | β
|
-| `DIMEDIT` | DED | Edit dimension text / position | β
|
-| `DIMTEDIT` | DIMTED | Move dimension text | β
|
-| `DIMBREAK` | DBR | Break dimension line | β
|
-| `DIMSPACE` | DSPACE | Adjust spacing between dimensions | β
|
-| `DIMJOGLINE` | DJL | Jog line in dimension | β
|
-| `TOLERANCE` | TOL | Geometric tolerance | β
|
-| `LEADER` | LE | Leader line (legacy) | β
|
-| `MLEADER` | MLD | Multileader | β
|
-| `MLEADERADD` | MLA | Add leader segment | β
|
-| `MLEADERREMOVE` | MLR | Remove leader segment | β
|
-| `MLEADERALIGN` | MLAL | Align multileaders | β
|
-| `MLEADERCOLLECT` | MLC | Collect multileaders | β
|
-| `DIMJOGGED` | DJO | Jogged radius dimension | β
|
-| `DIMCENTER` | DCE | Center mark | β
|
-| `CENTERLINE` | β | Center line | β
|
-| `CENTERMARK` | β | Center mark on arc/circle | β
|
-| `QLEADER` | QL | Quick leader (legacy) | β
|
-
----
-
-## Text & Table
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `STYLE` | ST | Text style manager | β
|
-| `DDEDIT` | ED | Edit text | β
|
-| `FIND` | β | Find and replace text | β
|
-| `TABLESTYLE` | TS | Table style manager | β
|
-| `DATALINK` | β | Link table to external spreadsheet (CSV) | β
|
-| `ARCTEXT` | β | Text along an arc | β
|
-| `TORIENT` | β | Orient text for readability | β
|
-| `DATAEXTRACTION` | β | Data extraction wizard | πΆ |
-| `FIELD` | β | Auto-updating text field | β |
-| `SPELL` | SP | Spell check | β |
-
----
-
-## Layer
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `LAYER` | LA | Layer manager | β
|
-| `LAYOFF` | β | Turn layer off | β
|
-| `LAYON` | β | Turn layer on | β
|
-| `LAYFRZ` | β | Freeze layer | β
|
-| `LAYTHW` | β | Thaw layer | β
|
-| `LAYLCK` | β | Lock layer | β
|
-| `LAYULK` | β | Unlock layer | β
|
-| `LAYMCUR` | β | Make object's layer current | β
|
-| `LAYMATCH` | β | Match layer of selected object | β
|
-| `VPLAYER` | β | Viewport layer control | β
|
-| `LINETYPE` | LT | Linetype manager | β
|
-| `LTSCALE` | β | Global linetype scale | β
|
-| `LAYISO` | β | Isolate layer | β
|
-| `LAYUNISO` | β | End layer isolation | β
|
-| `LAYDEL` | β | Delete layer | β
|
-| `LAYMRG` | β | Merge layers | β
|
-| `LAYERSTATE` | LAS | Save / restore layer states | β
|
-| `LAYWALK` | β | Walk through layers | β |
-| `LAYLOCKFADECTL` | β | Locked layer fading control | β |
-
----
-
-## Block & Reference
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `BLOCK` | B | Define block | β
|
-| `INSERT` | I | Insert block | β
|
-| `MINSERT` | β | Matrix block insert | β
|
-| `WBLOCK` | W | Write block to file | β
|
-| `XATTACH` | XA | Attach external reference | β
|
-| `XREF` | XR | External reference manager | β
|
-| `XRELOAD` | β | Reload external reference | β
|
-| `XCLIP` | XC | Clip external reference | β
|
-| `REFEDIT` | β | Edit reference in-place | β
|
-| `REFCLOSE` | β | Close reference edit | β
|
-| `BEDIT` | BE | Block editor | β
|
-| `BASE` | β | Set drawing base point | β
|
-| `NCOPY` | β | Copy nested objects out of a block | β
|
-| `ATTDEF` | ATT | Define attribute | β
|
-| `ATTEDIT` | ATE | Edit attribute | β
|
-| `ATTEXT` | β | Extract attributes (legacy) | β
|
-| `ATTSYNC` | β | Synchronize attribute definitions | β
|
-| `BLOCKPALETTE` | β | Multi-view block palette (command-line list) | πΆ |
-| `ATTMAN` | `BATTMAN` | Block attribute manager (opens the attribute editor) | β
|
-| `XBIND` | XB | Bind xref elements to drawing | β |
-| `XOPEN` | β | Open xref for editing | β
|
-| `BSAVE` | β | Save block in editor | β |
-| `BCLOSE` | β | Close block editor | β |
-
----
-
-## 3D Modeling
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `BOX` | β | Box solid | β
|
-| `SPHERE` | β | Sphere solid | β
|
-| `CYLINDER` | β | Cylinder solid | β
|
-| `CONE` | β | Cone solid | β
|
-| `WEDGE` | β | Wedge solid | β
|
-| `TORUS` | β | Torus solid | β
|
-| `PYRAMID` | PYR | Pyramid solid | β
|
-| `POLYSOLID` | β | Wall-like solid | β
|
-| `EXTRUDE` | EXT | Extrude profile | β
|
-| `PRESSPULL` | β | Push / pull a closed boundary | β
|
-| `THICKEN` | β | Thicken a closed profile to a solid | β
|
-| `REVOLVE` | REV | Revolve profile around axis | β
|
-| `SWEEP` | β | Sweep profile along path | β
|
-| `LOFT` | β | Loft between profiles | β
|
-| `MASSPROP` | β | Mass properties | β
|
-| `EXPORTSTEP` | β | Export to STEP | β
|
-| `EXPORTSTL` | β | Export to STL | β
|
-| `SLICE` | SL | Slice solid with plane | β
|
-| `SUBTRACT` | SU | Subtract solids | β
|
-| `UNION` | UNI | Union solids | β
|
-| `INTERSECT` | IN | Intersect solids | β
|
-| `INTERFERE` | INF | Interference solid from overlap | β
|
-| `SECTION` | β | Cross-section outline of a solid | β
|
-| `CONVTOSOLID` | β | Convert to solid | β |
-| `CONVTOSURFACE` | β | Convert to surface | β
|
-| `SECTIONPLANE` | β | Section plane object | β |
-| `FLATSHOT` | β | 2D view from 3D | β
|
-
----
-
-## View & Navigation
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `ZOOM` | Z | Zoom | β
|
-| `LIMITS` | β | Drawing/grid limits and point checking | β
|
-| `PAN` | P | Pan | β
|
-| `ORBIT` | 3DO | 3D orbit | β
|
-| `VPORTS` | β | Viewport configuration | β
|
-| `VPJOIN` | β | Join viewports | β
|
-| `SYNCPVIEWPORTS` | VPSYNC | Sync viewport display settings | β
|
-| `MSPACE` | MS | Switch to model space | β
|
-| `PSPACE` | PS | Switch to paper space | β
|
-| `MVIEW` | MV | Model view in layout | β
|
-| `UCSICON` | β | Toggle UCS icon | β
|
-| `VIEW` | V | Named views manager | β
|
-| `PLAN` | β | Switch to plan view | β
|
-| `NAVVCUBE` | β | Toggle ViewCube | πΆ |
-| `NAVBAR` | β | Toggle navigation bar | πΆ |
-| `TOOLPALETTES` | β | Tool palettes panel | πΆ |
-| `PROPERTIES` | PR | Properties palette | πΆ |
-| `SHEETSET` | SSM | Sheet set manager | πΆ |
-| `FILETAB` | β | Toggle file tabs | πΆ |
-| `LAYOUTTAB` | β | Toggle layout tabs | πΆ |
-| `DVIEW` | DV | Dynamic view (legacy) | β |
-| `NAVSWHEEL` | β | Steering wheel | β |
-| `RENDER` | RR | Render | β |
-| `RENDERPRESETS` | β | Render presets | β |
-| `LIGHT` | β | Add scene light | β |
-| `SUNPROPERTIES` | β | Sun light settings | β |
-| `MATERIALS` | MAT | Material editor | β |
-| `VISUALSTYLES` | β | Apply a built-in visual style (no custom-style manager) | πΆ |
-| `HIDE` | HI | Hidden-line regeneration | β
|
-| `VPMAX` | β | Maximize viewport | β |
-| `VPMIN` | β | Restore viewport | β |
-
----
-
-## Inquiry
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `AREA` | β | Calculate area | β
|
-| `MASSPROP` | β | Mass properties | β
|
-| `QSELECT` | β | Quick select | β
|
-| `STATUS` | β | Drawing status | β
|
-| `COUNT` | β | Count objects | β
|
-| `DIST` | DI | Distance between two points | β
|
-| `ID` | β | Point coordinate | β
|
-| `LIST` | LI | List object data | β
|
-| `DBLIST` | β | List all objects | β
|
-| `MEASUREGEOM` | β | Measure distance / angle / area | β
|
-| `CAL` | β | Command-line calculator | β
|
-| `QUICKCALC` | QC | Quick calculator | β
|
-
----
-
-## File & Plot
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `NEW` | β | New drawing | β
|
-| `OPEN` | β | Open drawing | β
|
-| `SAVE` | β | Save | β
|
-| `SAVEAS` | β | Save as | β
|
-| `QSAVE` | β | Quick save | β
|
-| `SAVEALL` | β | Save all open drawings | β
|
-| `PLOT` | β | Print / plot | β
|
-| `EXPORT [path]` | β | Export to PDF (save dialog, or straight to `path`) | β
|
-| `EXPORTPDF [path]` | β | Export to PDF (save dialog, or straight to `path`) | β
|
-| `PAGESETUP` | β | Page setup | β
|
-| `PLOTSTYLE` | β | Plot style | β
|
-| `PURGE` | PU | Purge unused items | β
|
-| `QUIT` | β | Exit application | β
|
-| `RECOVER` | β | Recover damaged drawing | β |
-| `CLOSE` | β | Close drawing | β
|
-| `ARCHIVE` | β | Package drawing + references into a folder | β
|
-| `ETRANSMIT` | β | Transmittal package (folder) | β
|
-
----
-
-## Manage & Customize
-
-| Command | Alias | Description | Status |
-|---|---|---|---|
-| `RENAME` | β | Rename named objects | β
|
-| `LINETYPE` | LT | Linetype manager | β
|
-| `PLOTSTYLEEDITOR` | β | Plot style editor | β
|
-| `MLEADERSTYLE` | β | Multileader style manager | β
|
-| `DIMSTYLE` | D | Dimension style manager | β
|
-| `CUI` | β | Customize user interface | β
|
-| `CUIIMPORT` | β | Import customization file | β
|
-| `CUIEXPORT` | β | Export customization file | β
|
-| `ALIASEDIT` | β | Edit command aliases | β
|
-| `OPTIONS` | OP | Application settings (drafting) | β
|
-| `OBJECTSCALE` | β | Mark objects annotative | β
|
-| `SCRIPT` | SCR | Run script file | β
|
-| `AUDIT` | β | Audit drawing integrity | πΆ |
-| `OVERKILL` | β | Remove duplicate geometry | πΆ |
-| `FINDNONPURGEABLE` | β | Find non-purgeable items | πΆ |
-| `XBIND` | β | Bind xref elements | β |
-| `HYPERLINK` | β | Insert hyperlink | β
|
-| `DBCONNECT` | β | Connect to external database | β |
-| `APPLOAD` | β | Load application (LISP / ARX) | β |
-| `NETLOAD` | β | Load .NET plug-in | β |
-| `ACTRECORD` | β | Record action macro | β |
-| `ACTMANAGER` | β | Action macro manager | β |
-
----
-
-## Summary
-
-| Category | Total | β
Done | πΆ Partial | β Missing |
-|---|---|---|---|---|
-| Draw | 32 | 31 | 0 | 1 |
-| Modify | 42 | 39 | 1 | 2 |
-| Dimension | 26 | 26 | 0 | 0 |
-| Text & Table | 10 | 7 | 1 | 2 |
-| Layer | 19 | 17 | 0 | 2 |
-| Block & Reference | 23 | 18 | 2 | 3 |
-| 3D Modeling | 27 | 25 | 0 | 2 |
-| View & Navigation | 33 | 16 | 8 | 9 |
-| Inquiry | 12 | 12 | 0 | 0 |
-| File & Plot | 17 | 16 | 0 | 1 |
-| Manage & Customize | 22 | 13 | 3 | 6 |
-| **Total** | **263** | **220** | **15** | **28** |
-
-> Counts include commands listed under more than one category (e.g. `SLICE`, `HELIX`,
-> `MINSERT`, `SUBTRACT`/`UNION`/`INTERSECT` appear in both their 2D and 3D groups).
->
-> Still out of scope: `POINTCLOUDATTACH`/`RECAP` (proprietary binary point-cloud parser
-> plus a GPU point renderer) and `UNDERLAYLAYERS`/`UOSNAP` (PDF/DWF internal-layer
-> parsing) β each needs an external dependency and real test files.
diff --git a/Cargo.lock b/Cargo.lock
index d7a22fa2e..9f8404156 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -14,7 +14,7 @@ dependencies = [
[[package]]
name = "OpenCADStudio"
-version = "0.8.8"
+version = "0.8.9"
dependencies = [
"acadrust",
"bincode",
@@ -29,6 +29,7 @@ dependencies = [
"getrandom 0.3.4",
"glam 0.33.2",
"iced",
+ "iced_aw",
"image",
"inventory",
"js-sys",
@@ -2184,6 +2185,22 @@ dependencies = [
"thiserror 2.0.18",
]
+[[package]]
+name = "iced_aw"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b7f923642b024c415150b70ef84ee5c11626e3b0af73b954d817d573bcd0a4"
+dependencies = [
+ "cfg-if",
+ "chrono",
+ "iced_core",
+ "iced_fonts",
+ "iced_widget",
+ "num-format",
+ "num-traits",
+ "web-time",
+]
+
[[package]]
name = "iced_core"
version = "0.14.0"
@@ -2213,6 +2230,29 @@ dependencies = [
"log",
]
+[[package]]
+name = "iced_fonts"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "214cff7c8499e328774216690e58e315a1a5f8f6fdd1035aed6298e62ffc4c1d"
+dependencies = [
+ "iced_core",
+ "iced_fonts_macros",
+ "iced_widget",
+]
+
+[[package]]
+name = "iced_fonts_macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef5125e110cb19cd1910a28298661c98c5d9ab02eef43594968352940e8752e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "ttf-parser",
+]
+
[[package]]
name = "iced_futures"
version = "0.14.0"
@@ -3243,6 +3283,16 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "num-format"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
+dependencies = [
+ "arrayvec",
+ "itoa",
+]
+
[[package]]
name = "num-integer"
version = "0.1.46"
diff --git a/Cargo.toml b/Cargo.toml
index 1ed48982f..165613abd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "OpenCADStudio"
-version = "0.8.8"
+version = "0.8.9"
edition = "2021"
build = "build.rs"
@@ -48,6 +48,7 @@ serde = { version = "1", features = ["derive"] }
# the binary self-contained on every platform.
lzma-sys = { version = "0.1", features = ["static"], optional = true }
iced = { version = "0.14", features = ["image", "svg", "advanced", "canvas", "smol"] }
+iced_aw = { version = "0.14.1", features = ["menu"] }
bytemuck = { version = "1.25", features = ["derive"] }
glam = { version = "0.33", features = ["bytemuck"] }
truck-modeling = "0.6"
diff --git a/HEADER_AUDIT.md b/HEADER_AUDIT.md
deleted file mode 100644
index 77c405d99..000000000
--- a/HEADER_AUDIT.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# HeaderVariables Audit
-
-`acadrust::document::HeaderVariables` β 232 total.
-
-## Pass (no matching feature)
-
-- **ISOLINES** β contour line count on curved surfaces (no curved
- wireframe generator yet)
-- **surface_u_density / surface_v_density / surface_type** β defaults
- for SURFACE / REVSURF / EDGESURF commands (those commands don't
- exist yet)
-- **lens_length / camera_height / camera_display** β perspective
- camera (no perspective viewport yet)
-- **north_direction / latitude / longitude / timezone** β sun /
- geographic data (no sun-shadow renderer)
-- **LIMMIN/LIMMAX** β drawing limits (no bounded-grid mode)
-- **INSBASE** β drawing's "save as block" anchor (no WBLOCK / "save
- selection as block" flow)
-
-### Remaining (intentionally not surfaced)
-- ~70 `dim_*` defaults β render-time `DimStyle` reference is already
- authoritative; these only seed a new DimStyle when a fresh DIMSTYLE
- NEW flow is added.
-- `*_control_handle`, `*_dict_handle` β internal table-navigation
- handles; direct `document.layers` / `document.dim_styles` / etc.
- accessors bypass them. acadrust read/write preserves them.
-- AutoCAD UI prefs (`blip_mode`, `drag_mode`, `pick_style`, etc.) β
- H7CAD has its own UI prefs and ignores AutoCAD's. Round-tripped.
diff --git a/STATUSBAR.md b/STATUSBAR.md
deleted file mode 100644
index 3db384418..000000000
--- a/STATUSBAR.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Open CAD Studio β Status Bar Items
-
-Tracks the status-bar toggle pills, the customization menu, and which
-items are implemented, deferred, or out of scope.
-
-The customization menu (the `β‘` handle at the bar's far right) lists every
-pill with a check mark; toggling a row shows/hides that pill and the choice
-persists across sessions.
-
-- Pill definitions & visibility: [`src/ui/statusbar_config.rs`](src/ui/statusbar_config.rs)
-- Bar layout & pills: [`src/ui/statusbar.rs`](src/ui/statusbar.rs)
-- Customization menu: [`src/ui/statusbar_menu.rs`](src/ui/statusbar_menu.rs)
-
-## Implemented
-
-| Item | Pill | Source |
-|------|------|--------|
-| Coordinates (live cursor X,Y,Z) | `Coords` | [statusbar.rs](src/ui/statusbar.rs) `format_coords` |
-| Snap Mode | `SNAP` | pre-existing |
-| Grid | `GRID` | pre-existing |
-| Ortho Mode | `ORTHO` | pre-existing |
-| Polar Tracking | `POLAR` | pre-existing |
-| Dynamic Input | `DYN` | pre-existing |
-| Object Snap Tracking | `OTRACK` | pre-existing |
-| 2D Object Snap | `OSNAP` | pre-existing |
-| Lineweight display | `LWT` | pre-existing |
-| Model / Paper space | `Space` | pre-existing |
-| Annotation scale | `Scale` | [scale_popup.rs](src/ui/scale_popup.rs) |
-| Drawing units (INSUNITS) | `Units` | [units_popup.rs](src/ui/units_popup.rs) |
-| Transparency display | `TPY` | [uniforms.rs](src/scene/pipeline/uniforms.rs) + [wire.wgsl](src/shaders/wire.wgsl) |
-| Isolate / Hide objects | `Isolate` | [isolate_popup.rs](src/ui/isolate_popup.rs); commands `ISOLATEOBJECTS` / `HIDEOBJECTS` / `UNISOLATEOBJECTS` |
-| Quick Properties | `QP` | [properties.rs](src/ui/properties.rs) `quick_view` |
-| Selection Filtering | `FILTER` | [selection_filter_popup.rs](src/ui/selection_filter_popup.rs) |
-| Selection Cycling | `SC` | [cycle_popup.rs](src/ui/cycle_popup.rs) β pick list + hover highlight |
-| Clean Screen | `CleanScreen` | hides ribbon + side panels |
-| Viewport count (app-specific) | `Vp` | β |
-
-## Remaining β annotative engine
-
-These are not single pills; they need a real annotative-objects subsystem:
-a per-entity annotative flag, the list of scales each object supports, and a
-**separate representation (size / position) per scale**, with render-time
-selection by the current annotation scale.
-
-| Item | Needs |
-|------|-------|
-| Annotation Visibility | hide annotative objects whose scale list omits the current scale |
-| AutoScale | add the current scale's representation automatically when the scale changes |
-| Annotation Monitor | warn on annotative inconsistencies |
-
-The pieces that exist today (`annotation_scale`, dimstyle / multileader
-annotative flags) do not yet include the per-scale representation system.
-Best tackled as a separate, staged effort (start with Visibility).
-
-## Dropped β no fit in this architecture
-
-| Item | Why |
-|------|-----|
-| Lock UI | panels are separate OS windows; there is no dock layout to lock |
-| Graphics Performance | a GPU-tuning dialog with no meaningful equivalent here |
-
-## Out of scope β large standalone engines
-
-Each is a product-level subsystem, not a status pill:
-
-- Infer Constraints (parametric constraint solver)
-- Isometric Drafting (isometric drawing mode)
-- 3D Object Snap
-- Dynamic UCS (auto-align UCS to the face under the cursor)
-- Gizmo (3D move/rotate manipulator)
-- Workspace Switching (multiple ribbon workspaces)
diff --git a/assets/icons/ui/dirty_dot.svg b/assets/icons/ui/dirty_dot.svg
new file mode 100644
index 000000000..2af2700e8
--- /dev/null
+++ b/assets/icons/ui/dirty_dot.svg
@@ -0,0 +1 @@
+
diff --git a/index.html b/index.html
index bafd6c253..1bcf5c3bd 100644
--- a/index.html
+++ b/index.html
@@ -46,6 +46,7 @@
file) so CAD text renders non-Latin scripts on the web. (#141) -->
+
diff --git a/src/app/config.rs b/src/app/config.rs
index a1e3544bf..237071e88 100644
--- a/src/app/config.rs
+++ b/src/app/config.rs
@@ -18,6 +18,8 @@ use crate::ui::window::plot::PlotDialogState;
pub struct AppConfig {
/// Input modes, backup, plugin lists, viewport background colours, β¦
pub settings: UserSettings,
+ /// Iced theme selection and the six base colours used by a custom theme.
+ pub theme: UiThemeConfig,
/// Recent-files list + retained count.
pub recent: RecentConfig,
/// Last selected section on the tabbed Start page.
@@ -35,6 +37,7 @@ impl Default for AppConfig {
fn default() -> Self {
Self {
settings: UserSettings::default(),
+ theme: UiThemeConfig::default(),
recent: RecentConfig::default(),
start: StartConfig::default(),
statusbar: StatusBarConfig::default(),
@@ -44,6 +47,136 @@ impl Default for AppConfig {
}
}
+#[derive(Clone, PartialEq, Serialize, Deserialize)]
+#[serde(default)]
+pub struct UiThemeConfig {
+ pub name: String,
+ pub palette: UiThemePalette,
+}
+
+impl Default for UiThemeConfig {
+ fn default() -> Self {
+ let theme = iced::Theme::Oxocarbon;
+ Self {
+ name: theme.to_string(),
+ palette: UiThemePalette::from_iced(theme.palette()),
+ }
+ }
+}
+
+impl UiThemeConfig {
+ pub fn to_iced(&self) -> iced::Theme {
+ if self.name == "Custom" {
+ iced::Theme::custom("Custom", self.palette.to_iced())
+ } else {
+ builtin_theme(&self.name).unwrap_or(iced::Theme::Oxocarbon)
+ }
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
+#[serde(default)]
+pub struct UiThemePalette {
+ pub background: [u8; 3],
+ pub text: [u8; 3],
+ pub primary: [u8; 3],
+ pub success: [u8; 3],
+ pub warning: [u8; 3],
+ pub danger: [u8; 3],
+}
+
+impl Default for UiThemePalette {
+ fn default() -> Self {
+ Self::from_iced(iced::Theme::Oxocarbon.palette())
+ }
+}
+
+impl UiThemePalette {
+ pub fn from_iced(palette: iced::theme::Palette) -> Self {
+ Self {
+ background: color_to_rgb(palette.background),
+ text: color_to_rgb(palette.text),
+ primary: color_to_rgb(palette.primary),
+ success: color_to_rgb(palette.success),
+ warning: color_to_rgb(palette.warning),
+ danger: color_to_rgb(palette.danger),
+ }
+ }
+
+ pub fn to_iced(self) -> iced::theme::Palette {
+ iced::theme::Palette {
+ background: rgb_to_color(self.background),
+ text: rgb_to_color(self.text),
+ primary: rgb_to_color(self.primary),
+ success: rgb_to_color(self.success),
+ warning: rgb_to_color(self.warning),
+ danger: rgb_to_color(self.danger),
+ }
+ }
+
+ pub fn hex_values(self) -> [String; 6] {
+ [
+ rgb_to_hex(self.background),
+ rgb_to_hex(self.text),
+ rgb_to_hex(self.primary),
+ rgb_to_hex(self.success),
+ rgb_to_hex(self.warning),
+ rgb_to_hex(self.danger),
+ ]
+ }
+
+ pub fn set_hex(&mut self, index: usize, value: &str) -> bool {
+ let Some(rgb) = parse_hex(value) else {
+ return false;
+ };
+ match index {
+ 0 => self.background = rgb,
+ 1 => self.text = rgb,
+ 2 => self.primary = rgb,
+ 3 => self.success = rgb,
+ 4 => self.warning = rgb,
+ 5 => self.danger = rgb,
+ _ => return false,
+ }
+ true
+ }
+}
+
+pub fn builtin_theme(name: &str) -> Option
{
+ iced::Theme::ALL
+ .iter()
+ .find(|theme| theme.to_string() == name)
+ .cloned()
+}
+
+fn color_to_rgb(color: iced::Color) -> [u8; 3] {
+ [
+ (color.r * 255.0).round() as u8,
+ (color.g * 255.0).round() as u8,
+ (color.b * 255.0).round() as u8,
+ ]
+}
+
+fn rgb_to_color(rgb: [u8; 3]) -> iced::Color {
+ iced::Color::from_rgb8(rgb[0], rgb[1], rgb[2])
+}
+
+fn rgb_to_hex(rgb: [u8; 3]) -> String {
+ format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2])
+}
+
+fn parse_hex(value: &str) -> Option<[u8; 3]> {
+ let value = value.trim().strip_prefix('#').unwrap_or(value.trim());
+ if value.len() != 6 {
+ return None;
+ }
+ Some([
+ u8::from_str_radix(&value[0..2], 16).ok()?,
+ u8::from_str_radix(&value[2..4], 16).ok()?,
+ u8::from_str_radix(&value[4..6], 16).ok()?,
+ ])
+}
+
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct RecentConfig {
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 247cd32d2..7edbb0709 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -1,7 +1,7 @@
mod alias;
#[cfg(not(target_arch = "wasm32"))]
mod automation;
-mod config;
+pub(crate) mod config;
#[cfg(not(target_arch = "wasm32"))]
pub use automation::{export_headless, serve};
mod command_driver;
@@ -226,6 +226,7 @@ pub enum StartSection {
Videos,
#[default]
Welcome,
+ Discussions,
Supporters,
}
@@ -259,6 +260,10 @@ pub(super) struct OpenCADStudio {
video_thumbs: std::collections::HashMap,
/// True while the boot-time playlist fetch is still in flight.
videos_loading: bool,
+ /// GitHub Discussions shown on the Start page, with pinned entries first.
+ discussions: Vec,
+ /// True while the boot-time Discussions refresh is still in flight.
+ discussions_loading: bool,
/// Block references whose properties panel shows per-axis Scale X/Y/Z even
/// though the three factors are currently equal β the user unchecked the
/// "Uniform scale" box for them (#427). Keyed by entity handle.
@@ -266,6 +271,9 @@ pub(super) struct OpenCADStudio {
/// Which Start-page section is shown when the page is too narrow for all
/// three side by side and falls back to a tab bar.
start_section: StartSection,
+ /// Widest natural single-row width of the Start-page action buttons,
+ /// measured by `WrapFlow` so side lists collapse before those buttons wrap.
+ start_action_w: std::sync::Arc,
/// When the window is too narrow the properties panel collapses to a
/// vertical bar; this is the user's toggle to expand it back out.
props_expanded: bool,
@@ -295,6 +303,9 @@ pub(super) struct OpenCADStudio {
isolate_popup_open: bool,
/// True while the selection-filter type picker is open.
selection_filter_popup_open: bool,
+ /// Hide a status-menu tooltip after its root is clicked; reset when the
+ /// pointer leaves the root for the opened menu.
+ status_menu_tooltip_hidden: bool,
/// Clean-screen mode: hide ribbon and side panels for a full canvas.
clean_screen: bool,
/// Quick Properties: show a compact floating property panel on selection.
@@ -570,10 +581,6 @@ pub(super) struct OpenCADStudio {
mtext_editor: Option,
/// Open in-place single-line TEXT editor (plain text-entry box), if any.
text_inline: Option,
- /// Which layout tab has its context menu open (None = closed).
- layout_context_menu: Option,
- /// Which drawing tab has its context menu open (None = closed).
- doc_tab_context_menu: Option,
/// Cursor-anchored one-shot snap override menu (Shift+RMB): the canvas
/// point it opened at, or `None` when closed (#337).
snap_override_popup: Option,
@@ -689,6 +696,8 @@ pub(super) struct OpenCADStudio {
// ββ Color Scheme ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
active_theme: Theme,
+ ui_theme: config::UiThemeConfig,
+ theme_color_inputs: [String; 6],
// ββ Keyboard Shortcut Editor ββββββββββββββββββββββββββββββββββββββββββ
/// User-defined function-key overrides: "F3" β command string.
@@ -1475,6 +1484,10 @@ pub enum Message {
OptionsOpen,
/// Set the default type/version used when first saving a new drawing.
DefaultSaveFormatChanged(String),
+ /// Select one of Iced's built-in themes or the editable Custom theme.
+ OptionsThemeChanged(String),
+ /// Edit one of Custom theme's six base colours as #RRGGBB.
+ OptionsThemeColorChanged(usize, String),
ClearScene,
SetWireframe(bool),
/// Set the active tab's render mode (one of acadrust's seven visual
@@ -1512,9 +1525,6 @@ pub enum Message {
},
/// Close the given tab index.
TabClose(usize),
- /// Open/close the right-click menu for a drawing tab.
- DocTabContextMenu(usize),
- DocTabContextMenuClose,
/// Save every drawing that already has a file path.
DocTabSaveAll,
/// Close every non-Start drawing tab.
@@ -1965,10 +1975,6 @@ pub enum Message {
LayoutRenameCommit,
/// Cancel an in-progress rename (Escape).
LayoutRenameCancel,
- /// Open the right-click context menu for the given layout tab.
- LayoutContextMenu(String),
- /// Close the layout context menu.
- LayoutContextMenuClose,
// ββ Layout Manager Panel ββββββββββββββββββββββββββββββββββββββββββββ
LayoutManagerOpen,
#[allow(dead_code)]
@@ -2058,6 +2064,8 @@ pub enum Message {
PatronsFetched(Result, String>),
/// Tutorial-playlist videos fetched at boot for the Start page.
VideosFetched(Result, String>),
+ /// GitHub Discussions fetched at boot for the Start page.
+ DiscussionsFetched(Result, String>),
/// Recent-file DWG preview thumbnails decoded on a background thread.
RecentThumbsLoaded(
Vec<(std::path::PathBuf, Option)>,
@@ -2177,6 +2185,8 @@ pub enum Message {
OsWindowClosed(window::Id),
/// No-op β used as a fallback when a TabEvent has no host mapping.
Noop,
+ /// Suppress menu-root tooltips between clicking the root and leaving it.
+ StatusMenuTooltipHidden(bool),
/// GitHub releases API returned a result. `Some(version)` means a
/// newer release exists; we open the update-notice window.
UpdateCheckResult(Option),
@@ -2483,8 +2493,11 @@ impl OpenCADStudio {
videos: Vec::new(),
video_thumbs: std::collections::HashMap::new(),
videos_loading: false,
+ discussions: Vec::new(),
+ discussions_loading: false,
props_asym_scale: std::collections::HashSet::new(),
start_section: StartSection::default(),
+ start_action_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
props_expanded: false,
history_content: iced::widget::text_editor::Content::new(),
status_bar: StatusBar::new(),
@@ -2501,6 +2514,7 @@ impl OpenCADStudio {
units_popup_open: false,
isolate_popup_open: false,
selection_filter_popup_open: false,
+ status_menu_tooltip_hidden: false,
statusbar_config: crate::ui::statusbar::statusbar_config::StatusBarConfig::default(),
last_saved_config: None,
otrack_active: None,
@@ -2593,8 +2607,6 @@ impl OpenCADStudio {
cont_anchor: None,
mtext_editor: None,
text_inline: None,
- layout_context_menu: None,
- doc_tab_context_menu: None,
snap_override_popup: None,
axis_lock_dir: None,
layout_rename_state: None,
@@ -2631,8 +2643,10 @@ impl OpenCADStudio {
default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(),
// Plot style
active_plot_style: None,
- // Color scheme (default: dark CAD-style)
- active_theme: Theme::Dark,
+ // Color scheme (default: Oxocarbon)
+ active_theme: Theme::Oxocarbon,
+ ui_theme: config::UiThemeConfig::default(),
+ theme_color_inputs: config::UiThemePalette::default().hex_values(),
// Keyboard shortcuts
shortcut_overrides: rustc_hash::FxHashMap::default(),
// Command aliases (populated from ocad.pgp just after construction)
@@ -2946,6 +2960,26 @@ impl OpenCADStudio {
};
#[cfg(target_arch = "wasm32")]
let videos_fetch = Task::none();
+ // GitHub Discussions: seed from the last successful fetch, then refresh
+ // the public feed and pinned section on a background thread.
+ #[cfg(not(target_arch = "wasm32"))]
+ let discussions_fetch = {
+ s.discussions = crate::discussions::load_cached();
+ s.discussions_loading = true;
+ let (tx, rx) = iced::futures::channel::oneshot::channel();
+ std::thread::spawn(move || {
+ let _ = tx.send(crate::discussions::fetch_discussions());
+ });
+ Task::perform(
+ async move {
+ rx.await
+ .unwrap_or_else(|_| Err("discussion fetch thread died".into()))
+ },
+ Message::DiscussionsFetched,
+ )
+ };
+ #[cfg(target_arch = "wasm32")]
+ let discussions_fetch = Task::none();
// Recent-file thumbnails: decoded off-thread β parsing every recent
// DWG's preview on the boot path held the first frame back.
let thumbs_fetch = s.refresh_recent_thumbs();
@@ -2960,6 +2994,7 @@ impl OpenCADStudio {
assoc_prompt,
patrons_fetch,
videos_fetch,
+ discussions_fetch,
thumbs_fetch,
]),
)
@@ -2979,7 +3014,12 @@ impl OpenCADStudio {
crate::patreon::fetch_patrons_web(),
Message::PatronsFetched,
);
- (s, Task::batch([focus, patrons]))
+ s.discussions_loading = true;
+ let discussions = Task::perform(
+ crate::discussions::fetch_discussions_web(),
+ Message::DiscussionsFetched,
+ );
+ (s, Task::batch([focus, patrons, discussions]))
}
}
diff --git a/src/app/update/command.rs b/src/app/update/command.rs
index 176214e1b..4376ad2ae 100644
--- a/src/app/update/command.rs
+++ b/src/app/update/command.rs
@@ -561,7 +561,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task {
self.refresh_properties();
return Task::none();
}
- // Cancel layout rename / context menus first, then fall through.
+ // Cancel layout rename first, then fall through.
let i_e = self.active_tab;
if self.qselect.take().is_some() {
return Task::none();
@@ -573,10 +573,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task {
return Task::none();
}
}
- if self.layout_rename_state.take().is_some()
- || self.layout_context_menu.take().is_some()
- || self.doc_tab_context_menu.take().is_some()
- {
+ if self.layout_rename_state.take().is_some() {
return Task::none();
}
// Typed text on the command line cancels first β one
diff --git a/src/app/update/file.rs b/src/app/update/file.rs
index 29102e900..ebe9e0b87 100644
--- a/src/app/update/file.rs
+++ b/src/app/update/file.rs
@@ -320,6 +320,7 @@ impl OpenCADStudio {
pub(in crate::app) fn current_config(&self) -> crate::app::config::AppConfig {
crate::app::config::AppConfig {
settings: self.current_settings(),
+ theme: self.ui_theme.clone(),
recent: crate::app::config::RecentConfig {
files: self
.recent_files
@@ -342,6 +343,9 @@ impl OpenCADStudio {
/// Distribute a loaded config into live app state (called once at startup).
pub(in crate::app) fn apply_config(&mut self, cfg: crate::app::config::AppConfig) {
self.apply_settings(&cfg.settings);
+ self.ui_theme = cfg.theme.clone();
+ self.active_theme = self.ui_theme.to_iced();
+ self.theme_color_inputs = self.ui_theme.palette.hex_values();
self.recent_files = cfg
.recent
.files
diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs
index 9a8e832ca..ed11ad25b 100644
--- a/src/app/update/mod.rs
+++ b/src/app/update/mod.rs
@@ -930,9 +930,7 @@ impl OpenCADStudio {
}
Message::TabSwitch(idx) => {
- self.doc_tab_context_menu = None;
self.layout_list_open = false;
- self.layout_context_menu = None;
self.layout_rename_state = None;
if idx < self.tabs.len() {
if idx != self.active_tab {
@@ -991,35 +989,14 @@ impl OpenCADStudio {
if let Some(index) = self.tabs.iter().position(|tab| tab.id == active_id) {
self.active_tab = index;
}
- self.doc_tab_context_menu = None;
Task::none()
}
- Message::TabClose(idx) => {
- self.doc_tab_context_menu = None;
- self.on_tab_close(idx)
- }
+ Message::TabClose(idx) => self.on_tab_close(idx),
- Message::DocTabContextMenu(idx) => {
- if self.tabs.get(idx).is_some_and(|tab| !tab.is_start) {
- self.layout_context_menu = None;
- self.doc_tab_context_menu = Some(idx);
- }
- Task::none()
- }
-
- Message::DocTabContextMenuClose => {
- self.doc_tab_context_menu = None;
- Task::none()
- }
-
- Message::DocTabSaveAll => {
- self.doc_tab_context_menu = None;
- self.dispatch_command("SAVEALL")
- }
+ Message::DocTabSaveAll => self.dispatch_command("SAVEALL"),
Message::DocTabCloseAll => {
- self.doc_tab_context_menu = None;
let ids = self
.tabs
.iter()
@@ -1030,7 +1007,6 @@ impl OpenCADStudio {
}
Message::DocTabCloseOthers(idx) => {
- self.doc_tab_context_menu = None;
let Some(keep_id) = self.tabs.get(idx).filter(|tab| !tab.is_start).map(|t| t.id)
else {
return Task::none();
@@ -1046,7 +1022,6 @@ impl OpenCADStudio {
}
Message::DocTabCopyFullPath(idx) => {
- self.doc_tab_context_menu = None;
#[cfg(not(target_arch = "wasm32"))]
{
let Some(path) = self.tabs.get(idx).and_then(|tab| tab.current_path.clone())
@@ -1078,7 +1053,6 @@ impl OpenCADStudio {
}
Message::DocTabOpenFileLocation(idx) => {
- self.doc_tab_context_menu = None;
#[cfg(not(target_arch = "wasm32"))]
{
let Some(path) = self.tabs.get(idx).and_then(|tab| tab.current_path.clone())
@@ -2098,12 +2072,9 @@ impl OpenCADStudio {
Task::none()
}
Message::TogglePolarPopup => {
- self.polar_popup_open ^= true;
- if self.polar_popup_open {
- // Start the custom field empty each time; picking a preset or
- // typing a value is what actually enables polar tracking.
- self.polar_custom_input.clear();
- }
+ // MenuBar owns its open state. Reset only the transient field
+ // whenever the caret starts a fresh interaction.
+ self.polar_custom_input.clear();
Task::none()
}
Message::ClosePolarPopup => {
@@ -3386,7 +3357,6 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "LAYOUT REORDER");
self.tabs[i].scene.set_layout_tab_order(&paper);
self.tabs[i].dirty = true;
- self.layout_context_menu = None;
Task::none()
}
@@ -3396,7 +3366,6 @@ impl OpenCADStudio {
let i = self.active_tab;
self.push_undo_snapshot(i, "LAYOUT DEL");
if self.tabs[i].scene.delete_layout(&name) {
- self.layout_context_menu = None;
self.layout_rename_state = None;
self.command_line
.push_output(&format!("Layout \"{name}\" silindi"));
@@ -3408,7 +3377,6 @@ impl OpenCADStudio {
Message::LayoutRenameStart(name) => {
if name != "Model" {
self.layout_rename_state = Some((name.clone(), name));
- self.layout_context_menu = None;
// Focus the inline field so the user types into it
// directly instead of the command line (issue #86).
return iced::widget::operation::focus(iced::widget::Id::new(
@@ -3433,26 +3401,6 @@ impl OpenCADStudio {
Task::none()
}
- Message::LayoutContextMenu(name) => {
- // No menu on the transient BEDIT block tab: Rename/Delete are
- // layout operations and don't apply to it.
- let is_block_tab = self.tabs[self.active_tab]
- .block_edit
- .as_ref()
- .map(|be| be.block_name == name)
- .unwrap_or(false);
- if name != "Model" && !is_block_tab {
- self.doc_tab_context_menu = None;
- self.layout_context_menu = Some(name);
- }
- Task::none()
- }
-
- Message::LayoutContextMenuClose => {
- self.layout_context_menu = None;
- Task::none()
- }
-
// ββ Layout Manager Panel ββββββββββββββββββββββββββββββββββββββββββ
Message::LayoutManagerOpen => {
let i = self.active_tab;
@@ -3603,7 +3551,12 @@ impl OpenCADStudio {
}
Message::SetTheme(theme) => {
+ self.ui_theme.name = theme.to_string();
+ self.ui_theme.palette =
+ crate::app::config::UiThemePalette::from_iced(theme.palette());
+ self.theme_color_inputs = self.ui_theme.palette.hex_values();
self.active_theme = theme;
+ self.persist_settings_if_changed();
Task::none()
}
@@ -3672,6 +3625,37 @@ impl OpenCADStudio {
Message::DefaultSaveFormatChanged(format) => {
self.default_save_format =
crate::io::canonical_save_format(&format).to_string();
+ self.persist_settings_if_changed();
+ Task::none()
+ }
+
+ Message::OptionsThemeChanged(name) => {
+ self.ui_theme.name = name;
+ if let Some(theme) =
+ crate::app::config::builtin_theme(&self.ui_theme.name)
+ {
+ self.ui_theme.palette =
+ crate::app::config::UiThemePalette::from_iced(theme.palette());
+ self.theme_color_inputs = self.ui_theme.palette.hex_values();
+ self.active_theme = theme;
+ } else {
+ self.ui_theme.name = "Custom".to_string();
+ self.active_theme = self.ui_theme.to_iced();
+ }
+ self.persist_settings_if_changed();
+ Task::none()
+ }
+
+ Message::OptionsThemeColorChanged(index, value) => {
+ if index >= self.theme_color_inputs.len() {
+ return Task::none();
+ }
+ self.theme_color_inputs[index] = value.clone();
+ if self.ui_theme.palette.set_hex(index, &value) {
+ self.ui_theme.name = "Custom".to_string();
+ self.active_theme = self.ui_theme.to_iced();
+ self.persist_settings_if_changed();
+ }
Task::none()
}
@@ -3975,6 +3959,16 @@ impl OpenCADStudio {
self.videos_loading = false;
Task::none()
}
+ Message::DiscussionsFetched(Ok(discussions)) => {
+ self.discussions_loading = false;
+ self.discussions = discussions;
+ Task::none()
+ }
+ // Offline: keep the native cache (web leaves the panel empty).
+ Message::DiscussionsFetched(Err(_)) => {
+ self.discussions_loading = false;
+ Task::none()
+ }
Message::RecentThumbsLoaded(thumbs) => {
for (path, handle) in thumbs {
self.recent_thumbs.insert(path, handle);
@@ -4169,6 +4163,13 @@ impl OpenCADStudio {
}
Message::Noop => Task::none(),
+ Message::StatusMenuTooltipHidden(hidden) => {
+ self.status_menu_tooltip_hidden = hidden;
+ if hidden {
+ self.polar_custom_input.clear();
+ }
+ Task::none()
+ }
// ββ Unsaved-changes dialog ββββββββββββββββββββββββββββββββββββ
Message::UnsavedDialogCancel => {
diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs
index 102d0faaf..ff2ae6c88 100644
--- a/src/app/update/viewport.rs
+++ b/src/app/update/viewport.rs
@@ -3702,9 +3702,8 @@ impl OpenCADStudio {
self.tabs[i].scene.sync_camera_to_document();
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
let sync_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
- // Cancel any pending rename/context-menu and active viewport when switching.
+ // Cancel any pending rename and active viewport when switching.
self.layout_rename_state = None;
- self.layout_context_menu = None;
self.tabs[i].scene.active_viewport = None;
let perf_phase = Instant::now();
self.tabs[i].scene.set_current_layout(name.clone());
diff --git a/src/app/view/controls.rs b/src/app/view/controls.rs
index 3aec2fbf0..43af88c64 100644
--- a/src/app/view/controls.rs
+++ b/src/app/view/controls.rs
@@ -4,7 +4,7 @@ use super::super::Message;
use iced::widget::{
button, container, mouse_area, row,
};
-use iced::{Background, Border, Color, Element, Theme};
+use iced::{Background, Border, Element, Theme};
pub(super) fn viewport_controls<'a>(
render_mode: acadrust::entities::ViewportRenderMode,
@@ -23,64 +23,58 @@ pub(super) fn viewport_controls<'a>(
RenderModeChoice(M::FlatShadedWithEdges),
RenderModeChoice(M::GouraudShadedWithEdges),
];
- let light = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 };
- let accent = Color { r: 0.45, g: 0.70, b: 1.0, a: 1.0 };
- let green = Color { r: 0.36, g: 0.80, b: 0.45, a: 1.0 };
- let red = Color { r: 0.92, g: 0.38, b: 0.38, a: 1.0 };
-
- // Fixed-colour icon button (close = red); colour stays on hover.
- let tinted_btn = move |bytes: &'static [u8], color: Color, msg: Message| {
- button(crate::ui::icons::tinted(bytes, 15.0, color))
+ let danger_btn = move |bytes: &'static [u8], msg: Message| {
+ button(crate::ui::icons::themed_danger(bytes, 15.0))
.on_press(msg)
.padding([4, 6])
- .style(move |_: &Theme, status| iced::widget::button::Style {
- background: Some(Background::Color(match status {
+ .style(move |theme: &Theme, status| iced::widget::button::Style {
+ background: matches!(
+ status,
iced::widget::button::Status::Hovered
- | iced::widget::button::Status::Pressed => Color {
- r: 0.25,
- g: 0.25,
- b: 0.25,
- a: 0.9,
- },
- _ => Color::TRANSPARENT,
- })),
+ | iced::widget::button::Status::Pressed
+ )
+ .then_some(Background::Color(
+ theme.extended_palette().danger.weak.color
+ )),
border: Border {
radius: 3.0.into(),
..Default::default()
},
- text_color: color,
+ text_color: theme.extended_palette().danger.base.color,
..Default::default()
})
};
// Borderless icon button; an `active` toggle gets an accent tint + fill.
let icon_btn = move |bytes: &'static [u8], active: bool, msg: Message| {
- let tint = if active { accent } else { light };
- button(crate::ui::icons::tinted(bytes, 15.0, tint))
+ let icon = if active {
+ crate::ui::icons::themed_primary(bytes, 15.0)
+ } else {
+ crate::ui::icons::themed(bytes, 15.0)
+ };
+ button(icon)
.on_press(msg)
.padding([4, 6])
- .style(move |_: &Theme, status| iced::widget::button::Style {
- background: Some(Background::Color(match (active, status) {
- (_, iced::widget::button::Status::Hovered) => Color {
- r: 0.25,
- g: 0.25,
- b: 0.25,
- a: 0.9,
- },
- (true, _) => Color {
- r: 0.16,
- g: 0.22,
- b: 0.32,
- a: 0.9,
- },
- (false, _) => Color::TRANSPARENT,
- })),
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match (active, status) {
+ (_, iced::widget::button::Status::Hovered) => {
+ Some(palette.background.strong)
+ }
+ (true, _) => Some(palette.primary.weak),
+ (false, _) => None,
+ };
+ iced::widget::button::Style {
+ background: pair.map(|p| Background::Color(p.color)),
border: Border {
radius: 3.0.into(),
..Default::default()
},
- text_color: tint,
+ text_color: pair
+ .map(|p| p.text)
+ .unwrap_or(palette.background.base.text),
..Default::default()
+ }
})
};
@@ -92,27 +86,27 @@ pub(super) fn viewport_controls<'a>(
)
.text_size(11)
.padding([4, 6])
- .style(move |_: &Theme, _| iced::widget::pick_list::Style {
- background: Background::Color(Color::TRANSPARENT),
+ .style(move |theme: &Theme, _| {
+ let text = theme.extended_palette().background.base.text;
+ iced::widget::pick_list::Style {
+ background: Background::Color(iced::Color::TRANSPARENT),
border: Border {
radius: 3.0.into(),
..Default::default()
},
- text_color: light,
- placeholder_color: light,
- handle_color: light,
+ text_color: text,
+ placeholder_color: text.scale_alpha(0.68),
+ handle_color: text,
+ }
});
// Thin vertical divider between control groups.
let sep = || {
- container(iced::widget::Space::new().width(1.0).height(16.0)).style(|_: &Theme| {
+ container(iced::widget::Space::new().width(1.0).height(16.0)).style(|theme: &Theme| {
iced::widget::container::Style {
- background: Some(Background::Color(Color {
- r: 0.45,
- g: 0.45,
- b: 0.45,
- a: 0.7,
- })),
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color.scale_alpha(0.7)
+ )),
..Default::default()
}
})
@@ -139,7 +133,7 @@ pub(super) fn viewport_controls<'a>(
// would only fire on release). Placed just left of Close.
if tile_count > 1 {
let drag = mouse_area(
- container(crate::ui::icons::tinted(crate::ui::icons::MOVE, 15.0, green))
+ container(crate::ui::icons::themed_success(crate::ui::icons::MOVE, 15.0))
.padding([4, 6])
.style(|_: &Theme| iced::widget::container::Style {
border: Border {
@@ -155,30 +149,25 @@ pub(super) fn viewport_controls<'a>(
.push(sep())
.push(drag)
.push(sep())
- .push(tinted_btn(crate::ui::icons::CLOSE, red, Message::CloseModelViewport));
+ .push(danger_btn(crate::ui::icons::CLOSE, Message::CloseModelViewport));
}
}
container(bar)
.padding(2)
- .style(|_: &Theme| iced::widget::container::Style {
- background: Some(Background::Color(Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 0.75,
- })),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ iced::widget::container::Style {
+ background: Some(Background::Color(
+ palette.background.weak.color.scale_alpha(0.92)
+ )),
border: Border {
- color: Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
- },
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
+ }
})
.into()
}
diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs
index 66673a065..59e3d7c16 100644
--- a/src/app/view/mod.rs
+++ b/src/app/view/mod.rs
@@ -13,6 +13,7 @@ use iced::widget::{
};
use iced::window;
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
+use iced_aw::ContextMenu;
mod controls;
mod modal;
@@ -21,8 +22,8 @@ mod viewcube;
use controls::{dyn_component_value, viewport_controls};
use overlay::{
- doc_tab_context_menu_overlay, layout_context_menu_overlay, mtext_editor_overlay,
- position_canvas_overlay, qselect_overlay, text_inline_overlay, viewport_context_menu_overlay,
+ mtext_editor_overlay, position_canvas_overlay, qselect_overlay, text_inline_overlay,
+ viewport_context_menu_overlay,
};
use viewcube::{viewcube_nav_controls, viewcube_ucs_picker, UCS_PICKER_W};
@@ -80,6 +81,13 @@ impl OpenCADStudio {
pub fn view_main(&self) -> Element<'_, Message> {
let i = self.active_tab;
let tab = &self.tabs[i];
+ let theme_text = self.active_theme.extended_palette().background.base.text;
+ let viewcube_text_color = [
+ theme_text.r,
+ theme_text.g,
+ theme_text.b,
+ theme_text.a,
+ ];
let is_paper = tab.scene.current_layout != "Model";
// Adaptive corner widgets: the ViewCube shows only while the active
// viewport is wide enough to hold it *beside* the render-mode bar, whose
@@ -120,11 +128,14 @@ impl OpenCADStudio {
&self.videos,
self.videos_loading,
&self.video_thumbs,
+ &self.discussions,
+ self.discussions_loading,
&self.recent_files,
&self.recent_thumbs,
self.recent_limit,
&self.recent_limit_input,
size.width,
+ self.start_action_w.clone(),
self.start_section,
)
})
@@ -134,6 +145,7 @@ impl OpenCADStudio {
&tab.scene,
viewcube_visible,
tab.render_mode,
+ viewcube_text_color,
))
.width(Fill)
.height(Fill)
@@ -169,6 +181,7 @@ impl OpenCADStudio {
show_viewcube,
render_mode,
idx,
+ viewcube_text_color,
))
.width(Fill)
.height(Fill),
@@ -742,18 +755,12 @@ impl OpenCADStudio {
])
.report_width0(self.render_bar_w.clone())
.into();
- // 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.
- let bar_layer = column![
- Space::new().height(iced::Length::Fixed(rect.y.max(0.0))),
- row![
- Space::new().width(iced::Length::Fixed(rect.x.max(0.0))),
- container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))),
- ],
- ]
- .width(Fill)
- .height(Fill);
+ // Pin the bar to the active model tile's top-left corner so it
+ // follows the active panel in a tiled layout.
+ let bar_layer = crate::ui::pin_at(
+ iced::Point::new(rect.x, rect.y),
+ container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))),
+ );
viewport_stack = viewport_stack.push(bar_layer);
}
@@ -802,12 +809,7 @@ impl OpenCADStudio {
},
..Default::default()
});
- let border_layer = column![
- Space::new().height(iced::Length::Fixed(y)),
- row![Space::new().width(iced::Length::Fixed(x)), border_frame,],
- ]
- .width(Fill)
- .height(Fill);
+ let border_layer = crate::ui::pin_at(iced::Point::new(x, y), border_frame);
viewport_stack = viewport_stack.push(border_layer);
let vp_mode = tab
@@ -833,15 +835,10 @@ impl OpenCADStudio {
])
.report_width0(self.render_bar_w.clone())
.into();
- let picker_layer = column![
- Space::new().height(iced::Length::Fixed(y + 4.0)),
- row![
- Space::new().width(iced::Length::Fixed(x + 4.0)),
- container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))),
- ],
- ]
- .width(Fill)
- .height(Fill);
+ let picker_layer = crate::ui::pin_at(
+ iced::Point::new(x + 4.0, y + 4.0),
+ container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))),
+ );
viewport_stack = viewport_stack.push(picker_layer);
// Hide the ViewCube first β before the render bar β when they collide.
@@ -849,15 +846,10 @@ impl OpenCADStudio {
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
- let controls = column![
- Space::new().height(iced::Length::Fixed(cube_y)),
- row![
- Space::new().width(iced::Length::Fixed(cube_x)),
- viewcube_nav_controls(),
- ],
- ]
- .width(Fill)
- .height(Fill);
+ let controls = crate::ui::pin_at(
+ iced::Point::new(cube_x, cube_y),
+ viewcube_nav_controls(),
+ );
viewport_stack = viewport_stack.push(controls);
let ucs_current = tab
@@ -873,16 +865,13 @@ impl OpenCADStudio {
.map(|u| u.name.clone())
.filter(|n| !n.is_empty())
.collect();
- let picker = column![
- Space::new().height(iced::Length::Fixed(cube_y + VIEWCUBE_HIT_SIZE + 6.0)),
- row![
- Space::new()
- .width(iced::Length::Fixed(cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5)),
- iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)),
- ],
- ]
- .width(Fill)
- .height(Fill);
+ let picker = crate::ui::pin_at(
+ iced::Point::new(
+ cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5,
+ cube_y + VIEWCUBE_HIT_SIZE + 6.0,
+ ),
+ iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)),
+ );
viewport_stack = viewport_stack.push(picker);
}
}
@@ -897,15 +886,10 @@ impl OpenCADStudio {
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
// Cube hit area + nav controls (home / roll / nudge) as one layer.
- let controls = column![
- Space::new().height(iced::Length::Fixed(cube_y)),
- row![
- Space::new().width(iced::Length::Fixed(cube_x)),
- viewcube_nav_controls(),
- ],
- ]
- .width(Fill)
- .height(Fill);
+ let controls = crate::ui::pin_at(
+ iced::Point::new(cube_x, cube_y),
+ viewcube_nav_controls(),
+ );
viewport_stack = viewport_stack.push(controls);
// WCS / named-UCS selector under the cube.
@@ -922,15 +906,13 @@ impl OpenCADStudio {
.map(|u| u.name.clone())
.filter(|n| !n.is_empty())
.collect();
- let picker = column![
- Space::new().height(iced::Length::Fixed(cube_y + VIEWCUBE_HIT_SIZE + 6.0)),
- row![
- Space::new().width(iced::Length::Fixed(cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5)),
- iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)),
- ],
- ]
- .width(Fill)
- .height(Fill);
+ let picker = crate::ui::pin_at(
+ iced::Point::new(
+ cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5,
+ cube_y + VIEWCUBE_HIT_SIZE + 6.0,
+ ),
+ iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)),
+ );
viewport_stack = viewport_stack.push(picker);
}
@@ -960,56 +942,47 @@ impl OpenCADStudio {
for (idx, item) in popup.items.iter().enumerate() {
let is_sel = idx == popup.selected;
let label = item.label;
- let btn = button(text(label).size(12).color(Color::WHITE))
+ let btn = button(text(label).size(12))
.on_press(Message::GripMenuPick(idx))
.padding([3, 10])
.width(Fill)
- .style(move |_: &Theme, status| iced::widget::button::Style {
- background: Some(Background::Color(match (is_sel, status) {
- (true, _) => Color {
- r: 0.20,
- g: 0.45,
- b: 0.95,
- a: 1.0,
- },
- (_, iced::widget::button::Status::Hovered) => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match (is_sel, status) {
+ (true, _) => Some(palette.primary.strong),
+ (_, iced::widget::button::Status::Hovered) => {
+ Some(palette.background.strong)
+ }
+ _ => None,
+ };
+ iced::widget::button::Style {
+ background: pair.map(|p| Background::Color(p.color)),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
- text_color: Color::WHITE,
+ text_color: pair
+ .map(|p| p.text)
+ .unwrap_or(palette.background.base.text),
..Default::default()
+ }
});
col = col.push(btn);
}
let menu_panel = container(col)
.padding(2)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 0.95,
- })),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: Color {
- r: 0.40,
- g: 0.40,
- b: 0.40,
- a: 1.0,
- },
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
+ }
});
// Offset the menu by 12 px so the cursor doesn't land on
// the first item immediately, matching the right-click
@@ -1035,14 +1008,14 @@ impl OpenCADStudio {
for (idx, name) in popup.items.iter().enumerate() {
let is_cur = popup.current == Some(idx);
let mark: Element<'_, Message> = if is_cur {
- crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, Color::WHITE)
+ crate::ui::icons::themed_check_cell(true)
} else {
Space::new().width(11).into()
};
let btn = button(
row![
container(mark).width(16),
- text(name).size(12).color(Color::WHITE),
+ text(name).size(12),
]
.spacing(2)
.align_y(iced::Center),
@@ -1050,47 +1023,39 @@ impl OpenCADStudio {
.on_press(Message::VisibilityPick(idx))
.padding([3, 10])
.width(Fill)
- .style(move |_: &Theme, status| iced::widget::button::Style {
- background: Some(Background::Color(match status {
- iced::widget::button::Status::Hovered => Color {
- r: 0.20,
- g: 0.45,
- b: 0.95,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ iced::widget::button::Style {
+ background: matches!(
+ status,
+ iced::widget::button::Status::Hovered
+ )
+ .then_some(Background::Color(palette.primary.weak.color)),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
- text_color: Color::WHITE,
+ text_color: palette.background.base.text,
..Default::default()
+ }
});
col = col.push(btn);
}
let panel = container(iced::widget::scrollable(col).height(iced::Length::Shrink))
.max_height(360.0)
.padding(2)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 0.95,
- })),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: Color {
- r: 0.40,
- g: 0.40,
- b: 0.40,
- a: 1.0,
- },
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
+ }
});
let anchor = iced::Point::new(popup.anchor.x + 12.0, popup.anchor.y + 12.0);
viewport_stack =
@@ -1157,42 +1122,27 @@ impl OpenCADStudio {
} else {
trace
};
- let perf_button_style = |_: &Theme, status: button::Status| button::Style {
- background: Some(Background::Color(if matches!(status, button::Status::Hovered) {
- Color {
- r: 0.24,
- g: 0.24,
- b: 0.24,
- a: 1.0,
- }
+ let perf_button_style = |theme: &Theme, status: button::Status| {
+ let palette = theme.extended_palette();
+ let pair = if matches!(status, button::Status::Hovered) {
+ palette.background.strong
} else {
- Color {
- r: 0.14,
- g: 0.14,
- b: 0.14,
- a: 1.0,
- }
- })),
- text_color: Color::WHITE,
+ palette.background.weak
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
- },
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
+ }
};
let copy_btn = button(
row![
- crate::ui::icons::tinted(
- crate::ui::icons::COPY,
- 11.0,
- Color::from_rgb(0.7, 0.85, 1.0),
- ),
+ crate::ui::icons::themed_primary(crate::ui::icons::COPY, 11.0),
text("Copy").size(11),
]
.spacing(4)
@@ -1203,11 +1153,7 @@ impl OpenCADStudio {
.padding([2, 6]);
let clear_btn = button(
row![
- crate::ui::icons::tinted(
- crate::ui::icons::TRASH,
- 11.0,
- Color::from_rgb(1.0, 0.55, 0.55),
- ),
+ crate::ui::icons::themed_danger(crate::ui::icons::TRASH, 11.0),
text("Clear").size(11),
]
.spacing(4)
@@ -1217,20 +1163,24 @@ impl OpenCADStudio {
.style(perf_button_style)
.padding([2, 6]);
let header = row![
- text("PERF").size(12).color(Color::from_rgb(0.6, 1.0, 0.6)),
+ text("PERF").size(12).style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(theme.extended_palette().success.base.color),
+ }),
Space::new().width(iced::Length::Fill),
copy_btn,
clear_btn,
]
.spacing(6)
.align_y(iced::Center);
- let log = scrollable(text(trace).size(11).color(Color::from_rgb(0.8, 0.9, 0.8)))
+ let log = scrollable(text(trace).size(11))
.height(iced::Length::Fixed(220.0))
.width(iced::Length::Fill);
let panel = container(
column![
header,
- text(summary).size(11).color(Color::from_rgb(0.6, 1.0, 0.6)),
+ text(summary).size(11).style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(theme.extended_palette().success.base.color),
+ }),
log,
]
.spacing(5),
@@ -1429,13 +1379,26 @@ impl OpenCADStudio {
if let Some(be) = &tab.block_edit {
displayed_layouts.push(be.block_name.clone());
}
+ let status_menu_data = crate::ui::statusbar::StatusMenuData {
+ layout_names: layout_names.clone(),
+ polar_custom_input: &self.polar_custom_input,
+ scale_is_model: is_model,
+ scale_list: tab.scene.scale_list(),
+ has_selection: !tab.scene.selected.is_empty(),
+ selection_types: tab
+ .scene
+ .entity_type_names_in_layout()
+ .into_iter()
+ .map(|name| name.to_string())
+ .collect(),
+ selection_filter: &tab.scene.selection_filter,
+ tooltip_hidden: self.status_menu_tooltip_hidden,
+ };
self.status_bar.view(
&self.snapper,
- self.snap_popup_open,
self.ortho_mode,
self.polar_mode,
self.polar_increment_deg,
- self.polar_popup_open,
self.dyn_input,
self.snapper.otrack_enabled,
displayed_layouts,
@@ -1451,7 +1414,6 @@ impl OpenCADStudio {
tab.scene.active_viewport.is_some(),
self.show_layout_tabs,
tab.scene.annotation_scale,
- self.scale_popup_open,
scale_pill_enabled,
tab.scene.document.header.lineweight_display,
cursor_coord,
@@ -1460,127 +1422,27 @@ impl OpenCADStudio {
picking,
self.clean_screen,
tab.scene.document.header.insertion_units,
- self.units_popup_open,
tab.scene.is_isolation_active(),
tab.scene.transparency_display,
self.quick_properties,
tab.scene.selection_filter_active(),
self.selection_cycling,
&self.statusbar_config,
+ status_menu_data,
)
})
.width(Fill)
.height(Fill)
})
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.11,
- g: 0.11,
- b: 0.11,
- a: 1.0,
- })),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.width(Fill)
.height(Fill);
- let win = self.win_size;
- let sb_pill = crate::ui::wrap_bar::dropdown_bounds;
-
- let snap_layer: Element<'_, Message> = if self.snap_popup_open {
- crate::ui::popup::snap_popup::snap_popup_overlay(
- &self.snapper,
- sb_pill(crate::ui::statusbar::SB_OSNAP_ID),
- win,
- )
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
- let scale_layer: Element<'_, Message> = if self.scale_popup_open {
- let is_model = tab.scene.current_layout == "Model";
- crate::ui::popup::scale_popup::scale_popup_overlay(
- is_model,
- tab.scene.annotation_scale,
- tab.scene.first_viewport_scale(),
- tab.scene.scale_list(),
- sb_pill(crate::ui::statusbar::SB_SCALE_ID),
- win,
- )
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
- let polar_layer: Element<'_, Message> = if self.polar_popup_open {
- crate::ui::popup::polar_popup::polar_popup_overlay(
- self.polar_increment_deg,
- &self.polar_custom_input,
- sb_pill(crate::ui::statusbar::SB_POLAR_ID),
- win,
- )
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
- let statusbar_menu_layer: Element<'_, Message> = if self.statusbar_menu_open {
- crate::ui::statusbar::statusbar_menu::statusbar_menu_overlay(
- &self.statusbar_config,
- sb_pill(crate::ui::statusbar::SB_MENU_ID),
- win,
- )
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
- let layout_list_layer: Element<'_, Message> = if self.layout_list_open && !tab.is_start {
- crate::ui::statusbar::statusbar_menu::layout_list_overlay(
- &tab.scene.layout_names(),
- &tab.scene.current_layout,
- sb_pill(crate::ui::statusbar::SB_LAYOUTLIST_ID),
- win,
- )
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
- let units_layer: Element<'_, Message> = if self.units_popup_open {
- crate::ui::popup::units_popup::units_popup_overlay(
- tab.scene.document.header.insertion_units,
- sb_pill(crate::ui::statusbar::SB_UNITS_ID),
- win,
- )
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
- let isolate_layer: Element<'_, Message> = if self.isolate_popup_open {
- crate::ui::popup::isolate_popup::isolate_popup_overlay(
- !tab.scene.selected.is_empty(),
- tab.scene.is_isolation_active(),
- sb_pill(crate::ui::statusbar::SB_ISOLATE_ID),
- win,
- )
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
- let sel_filter_layer: Element<'_, Message> = if self.selection_filter_popup_open {
- let types: Vec = tab
- .scene
- .entity_type_names_in_layout()
- .into_iter()
- .map(|s| s.to_string())
- .collect();
- crate::ui::popup::selection_filter_popup::selection_filter_popup_overlay(
- types,
- &tab.scene.selection_filter,
- sb_pill(crate::ui::statusbar::SB_FILTER_ID),
- win,
- )
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
let dropdown_layer: Element<'_, Message> = self
.ribbon
.dropdown_overlay(
@@ -1590,33 +1452,6 @@ impl OpenCADStudio {
)
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into());
- let layout_ctx_layer: Element<'_, Message> = if let Some(name) = &self.layout_context_menu {
- layout_context_menu_overlay(name, win)
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
- let doc_tab_ctx_layer: Element<'_, Message> =
- if let Some(idx) = self.doc_tab_context_menu {
- if let Some(context_tab) = self.tabs.get(idx) {
- let has_other_drawings = self
- .tabs
- .iter()
- .enumerate()
- .any(|(other_idx, tab)| other_idx != idx && !tab.is_start);
- doc_tab_context_menu_overlay(
- idx,
- context_tab.current_path.as_deref(),
- has_other_drawings,
- win,
- )
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- }
- } else {
- iced::widget::Space::new().width(0).height(0).into()
- };
-
let snap_override_layer: Element<'_, Message> =
if let Some(pos) = self.snap_override_popup {
overlay::snap_override_overlay(pos)
@@ -1640,17 +1475,7 @@ impl OpenCADStudio {
let composed = stack![
main_ui,
- snap_layer,
- scale_layer,
- polar_layer,
- statusbar_menu_layer,
- layout_list_layer,
- units_layer,
- isolate_layer,
- sel_filter_layer,
dropdown_layer,
- layout_ctx_layer,
- doc_tab_ctx_layer,
qselect_layer,
snap_override_layer,
open_progress_layer,
@@ -2041,57 +1866,54 @@ impl OpenCADStudio {
// ββ Document tab bar βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
- const BAR_BG: Color = Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 1.0,
- };
- const TAB_ACTIVE: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- };
- const TAB_HOVER: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.18,
- a: 1.0,
- };
- const TAB_INACTIVE: Color = Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 1.0,
- };
- const ACCENT: Color = Color {
- r: 0.20,
- g: 0.55,
- b: 0.90,
- a: 1.0,
- };
- const TEXT_ACTIVE: Color = Color::WHITE;
- const TEXT_INACTIVE: Color = Color {
- r: 0.60,
- g: 0.60,
- b: 0.60,
- a: 1.0,
- };
- const CLOSE_HOVER: Color = Color {
- r: 0.70,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- };
- const BORDER_COLOR: Color = Color {
- r: 0.25,
- g: 0.25,
- b: 0.25,
- a: 1.0,
+/// Right-click actions for a drawing tab. `ContextMenu` owns opening,
+/// cursor-relative placement, boundary clamping, and dismissal.
+fn doc_tab_context_menu(
+ tab_idx: usize,
+ has_current_path: bool,
+ has_other_drawings: bool,
+) -> Element<'static, Message> {
+ const MENU_W: f32 = 210.0;
+
+ let item = |label: &'static str, msg: Option| {
+ let mut item = button(text(label).size(12))
+ .style(button::subtle)
+ .padding([4, 12])
+ .width(Fill);
+ if let Some(msg) = msg {
+ item = item.on_press(msg);
+ }
+ item
};
+ let native_path_actions = cfg!(not(target_arch = "wasm32")) && has_current_path;
+ container(
+ column![
+ item("Save All", Some(Message::DocTabSaveAll)),
+ item("Close All", Some(Message::DocTabCloseAll)),
+ item(
+ "Close All Other Drawings",
+ has_other_drawings.then_some(Message::DocTabCloseOthers(tab_idx)),
+ ),
+ item(
+ "Copy Full File Path",
+ native_path_actions.then_some(Message::DocTabCopyFullPath(tab_idx)),
+ ),
+ item(
+ "Open File Location",
+ native_path_actions.then_some(Message::DocTabOpenFileLocation(tab_idx)),
+ ),
+ ]
+ .spacing(0)
+ .width(MENU_W),
+ )
+ .style(container::bordered_box)
+ .padding([4, 0])
+ .width(iced::Length::Fixed(MENU_W))
+ .into()
+}
+
+pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
// Document tabs live in a flex-wrap flow so they spill onto lower rows when
// there are more tabs than the width can hold on one line.
let mut items: Vec> = Vec::new();
@@ -2107,16 +1929,7 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
let name = crate::ui::text_util::elide(&tab.tab_display_name(), 24);
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,
- },
- ),
+ crate::ui::icons::themed_warning(crate::ui::icons::DIRTY_DOT, 14.0),
text(name).size(12),
]
.spacing(5)
@@ -2128,29 +1941,27 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
let title_btn = button(title_inner)
.on_press(Message::TabSwitch(idx))
- .padding([5, 12])
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (is_active, status) {
- (true, _) => TAB_ACTIVE,
- (false, button::Status::Hovered) => TAB_HOVER,
- _ => TAB_INACTIVE,
- })),
- text_color: if is_active {
- TEXT_ACTIVE
- } else {
- TEXT_INACTIVE
- },
- border: Border {
- color: if is_active {
- ACCENT
+ .height(Fill)
+ .padding([4, 12])
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let background = match (is_active, status) {
+ (false, button::Status::Hovered) => {
+ Some(Background::Color(palette.background.weak.color))
+ }
+ _ => None,
+ };
+ button::Style {
+ background,
+ text_color: if is_active {
+ palette.primary.weak.text
} else {
- Color::TRANSPARENT
+ palette.background.base.text.scale_alpha(0.72)
},
- width: if is_active { 1.0 } else { 0.0 },
- radius: 0.0.into(),
- },
- shadow: iced::Shadow::default(),
- snap: false,
+ border: Border::default(),
+ shadow: iced::Shadow::default(),
+ snap: false,
+ }
});
let title_btn: Element<'_, Message> = if tab.is_start {
title_btn.into()
@@ -2167,90 +1978,83 @@ 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(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 {
- background: Some(Background::Color(match status {
- button::Status::Hovered => CLOSE_HOVER,
- _ => {
- if is_active {
- TAB_ACTIVE
- } else {
- TAB_INACTIVE
- }
- }
- })),
- border: Border {
- radius: 3.0.into(),
- ..Default::default()
- },
- ..Default::default()
- });
- row![title_btn, close_btn].spacing(0).align_y(iced::Center)
+ let close_btn = button(text("Γ").size(12))
+ .on_press(Message::TabClose(idx))
+ .height(Fill)
+ .padding([4, 8])
+ .style(button::subtle);
+ row![title_btn, close_btn]
+ .spacing(0)
+ .height(Fill)
+ .align_y(iced::Center)
};
- let tab_container = container(row_inner).style(move |_: &Theme| container::Style {
- border: Border {
- color: if is_active {
- BORDER_COLOR
- } else {
- Color::TRANSPARENT
- },
- width: if is_active { 1.0 } else { 0.0 },
- radius: 0.0.into(),
- },
- ..Default::default()
- });
+ let tab_container = container(row_inner)
+ .height(iced::Length::Fixed(23.0))
+ .style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(if is_active {
+ palette.primary.weak.color
+ } else {
+ palette.background.base.color
+ })),
+ border: Border {
+ color: if is_active {
+ palette.primary.base.color
+ } else {
+ Color::TRANSPARENT
+ },
+ width: if is_active { 1.0 } else { 0.0 },
+ radius: 0.0.into(),
+ },
+ ..Default::default()
+ }
+ });
let tab_element: Element<'_, Message> = if tab.is_start {
tab_container.into()
} else {
+ let has_current_path = tab.current_path.is_some();
+ let has_other_drawings = drag_targets.len() > 1;
+ let tab_target: Element<'_, Message> = if let Some(path) = &tab.current_path {
+ iced::widget::tooltip(
+ tab_container,
+ container(text(path.to_string_lossy().into_owned()).size(11))
+ .style(container::bordered_box)
+ .padding([4, 8]),
+ iced::widget::tooltip::Position::Bottom,
+ )
+ .gap(4)
+ .into()
+ } else {
+ tab_container.into()
+ };
crate::ui::wrap_bar::PosReport::owned(
format!("DOC_TAB:{idx}"),
- mouse_area(tab_container).on_right_press(Message::DocTabContextMenu(idx)),
+ ContextMenu::new(tab_target, move || {
+ doc_tab_context_menu(idx, has_current_path, has_other_drawings)
+ }),
)
.into()
};
items.push(tab_element);
}
- let new_btn = button(text("+").size(14).color(Color {
- r: 0.65,
- g: 0.65,
- b: 0.65,
- a: 1.0,
- }))
- .on_press(Message::TabNew)
- .padding([4, 10])
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => TAB_HOVER,
- _ => Color::TRANSPARENT,
- })),
- border: Border {
- radius: 0.0.into(),
- ..Default::default()
- },
- ..Default::default()
- });
+ let new_btn = button(text("+").size(14))
+ .on_press(Message::TabNew)
+ .padding([4, 10])
+ .style(button::subtle);
items.push(new_btn.into());
container(WrapFlow::new(items).spacing_x(0.0).row_h(30.0))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BAR_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
border: Border {
- color: BORDER_COLOR,
+ color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@@ -2279,21 +2083,17 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
// fixed Start tab (`DocumentTab::is_start`). English-only by design β this
// is the public welcome screen and stays consistent across locales.
//
-// The page picks up the application icon's red-brown (#B03020) as a tint so
-// it visually belongs to OpenCADStudio without overpowering the dark workspace.
+fn start_muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
-const BRAND: Color = Color {
- r: 0.690,
- g: 0.188,
- b: 0.125,
- a: 1.0,
-}; // #B03020
-const BRAND_DARK: Color = Color {
- r: 0.45,
- g: 0.12,
- b: 0.08,
- a: 1.0,
-};
+fn start_primary_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
+ }
+}
/// Transparent input layer for one Model pane: a `mouse_area` filling the pane
/// that emits pane-tagged viewport events (`idx` = the pane's tile index). The
@@ -2315,7 +2115,6 @@ fn pane_mouse_area<'a>(idx: usize) -> Element<'a, Message> {
/// Canvas that draws a label rotated 90Β° (for a collapsed panel's bar).
struct VBarLabel {
text: String,
- color: Color,
}
impl canvas::Program for VBarLabel {
@@ -2325,7 +2124,7 @@ impl canvas::Program for VBarLabel {
&self,
_state: &(),
renderer: &iced::Renderer,
- _theme: &Theme,
+ theme: &Theme,
bounds: iced::Rectangle,
_cursor: iced::advanced::mouse::Cursor,
) -> Vec {
@@ -2336,7 +2135,7 @@ impl canvas::Program for VBarLabel {
frame.fill_text(canvas::Text {
content: self.text.clone(),
position: iced::Point::ORIGIN,
- color: self.color,
+ color: theme.extended_palette().background.base.text.scale_alpha(0.72),
size: iced::Pixels(13.0),
align_x: iced::advanced::text::Alignment::Center,
align_y: iced::alignment::Vertical::Center,
@@ -2351,28 +2150,8 @@ impl canvas::Program for VBarLabel {
/// A collapsed panel rendered as a tall narrow bar with its name written along
/// it, rotated 90Β°. Pressing it emits `on_press`.
pub(super) fn collapse_bar<'a>(name: &str, on_press: Message) -> Element<'a, Message> {
- const LABEL: Color = Color {
- r: 0.72,
- g: 0.72,
- b: 0.74,
- a: 1.0,
- };
- const BAR_BG: Color = Color {
- r: 0.13,
- g: 0.13,
- b: 0.14,
- a: 1.0,
- };
- const BAR_BORDER: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.24,
- a: 1.0,
- };
-
let label = canvas(VBarLabel {
text: name.to_string(),
- color: LABEL,
})
.width(Fill)
.height(Fill);
@@ -2381,10 +2160,12 @@ pub(super) fn collapse_bar<'a>(name: &str, on_press: Message) -> Element<'a, Mes
container(label)
.width(iced::Length::Fixed(26.0))
.height(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BAR_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
border: Border {
- color: BAR_BORDER,
+ color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@@ -2401,6 +2182,8 @@ pub(super) fn start_page_view<'a>(
videos: &'a [crate::videos::VideoEntry],
videos_loading: bool,
video_thumbs: &'a std::collections::HashMap,
+ discussions: &'a [crate::discussions::DiscussionEntry],
+ discussions_loading: bool,
recents: &'a [std::path::PathBuf],
thumbs: &'a std::collections::HashMap<
std::path::PathBuf,
@@ -2409,70 +2192,41 @@ pub(super) fn start_page_view<'a>(
recent_limit: usize,
recent_limit_input: &'a str,
avail_w: f32,
+ action_width_out: std::sync::Arc,
active: super::StartSection,
) -> Element<'a, Message> {
- const TEXT: Color = Color {
- r: 0.94,
- g: 0.93,
- b: 0.92,
- a: 1.0,
- };
- const CARD_BG: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.13,
- a: 1.0,
- };
- const CARD_BORDER: Color = Color {
- r: 0.20,
- g: 0.20,
- b: 0.22,
- a: 1.0,
- };
-
- let headline = text("Open CAD Studio").size(40).color(BRAND);
+ let headline = text("Open CAD Studio").size(40).style(start_primary_style);
// Plain outlined button (Open / New / Help / Contribute).
let outline_btn = |label: &'static str, msg: Message| {
- button(text(label).size(14).color(TEXT))
+ button(text(label).size(14))
.on_press(msg)
.padding([10, 22])
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => Color {
- r: 0.18,
- g: 0.18,
- b: 0.20,
- a: 1.0,
- },
- _ => Color {
- r: 0.13,
- g: 0.13,
- b: 0.15,
- a: 1.0,
- },
- })),
- text_color: TEXT,
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match status {
+ button::Status::Hovered => palette.background.strong,
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: Color {
- r: 0.30,
- g: 0.30,
- b: 0.33,
- a: 1.0,
- },
+ color: palette.background.neutral.color,
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
+ }
})
};
- // Donate β the prominent call-to-action. Solid brand fill, white text.
+ // Donate β the prominent call-to-action, using the theme's danger role.
let donate_btn = {
button(
row![
- crate::ui::icons::tinted(crate::ui::icons::HEART, 14.0, Color::WHITE),
- text("Donate").size(14).color(Color::WHITE),
+ crate::ui::icons::themed_danger_text(crate::ui::icons::HEART, 14.0),
+ text("Donate").size(14),
]
.spacing(5)
.align_y(iced::Center),
@@ -2482,29 +2236,7 @@ pub(super) fn start_page_view<'a>(
event: crate::modules::ModuleEvent::Command("DONATE".to_string()),
})
.padding([12, 28])
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => BRAND_DARK,
- _ => BRAND,
- })),
- text_color: Color::WHITE,
- border: Border {
- color: BRAND_DARK,
- width: 1.0,
- radius: 6.0.into(),
- },
- shadow: iced::Shadow {
- color: Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.4,
- },
- offset: iced::Vector::new(0.0, 2.0),
- blur_radius: 6.0,
- },
- ..Default::default()
- })
+ .style(button::danger)
};
let primary_row = WrapFlow::new(vec![
@@ -2513,7 +2245,8 @@ pub(super) fn start_page_view<'a>(
donate_btn.into(),
])
.spacing_x(12.0)
- .row_h(48.0);
+ .row_h(48.0)
+ .report_natural_width(action_width_out.clone());
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut secondary_items: Vec> = vec![
@@ -2532,80 +2265,28 @@ pub(super) fn start_page_view<'a>(
// link to the web version.
#[cfg(not(target_arch = "wasm32"))]
{
- // Bright ribbon blue (matches the active-tool accent), filled.
+ // Filled with the active theme's primary colour.
secondary_items.push(
- button(text("OCS Web").size(14).color(Color::WHITE))
+ button(text("OCS Web").size(14))
.on_press(Message::RibbonToolClick {
tool_id: "WEBVERSION".to_string(),
event: crate::modules::ModuleEvent::Command("WEBVERSION".to_string()),
})
.padding([10, 22])
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => Color {
- r: 0.15,
- g: 0.45,
- b: 0.78,
- a: 1.0,
- },
- _ => Color {
- r: 0.20,
- g: 0.55,
- b: 0.90,
- a: 1.0,
- },
- })),
- text_color: Color::WHITE,
- border: Border {
- color: Color {
- r: 0.20,
- g: 0.55,
- b: 0.90,
- a: 1.0,
- },
- width: 1.0,
- radius: 6.0.into(),
- },
- ..Default::default()
- })
+ .style(button::primary)
.into(),
);
}
let secondary_row = WrapFlow::new(secondary_items)
.spacing_x(12.0)
- .row_h(44.0);
-
- // Buttons sit on a transparent container with a large, brand-tinted
- // ambient shadow (offset = 0, big blur) β produces a soft halo behind
- // the action row, matching the Thunderbird coloured-glow look against
- // the dark page.
- let primary_glow = container(primary_row)
- .padding(iced::Padding {
- top: 4.0,
- right: 8.0,
- bottom: 4.0,
- left: 8.0,
- })
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color::TRANSPARENT)),
- shadow: iced::Shadow {
- color: Color {
- r: BRAND.r,
- g: BRAND.g,
- b: BRAND.b,
- a: 0.45,
- },
- offset: iced::Vector::ZERO,
- blur_radius: 80.0,
- },
- ..Default::default()
- });
+ .row_h(44.0)
+ .report_natural_width(action_width_out.clone());
let content = column![
Space::new().height(iced::Length::Fixed(28.0)),
container(headline).center_x(Fill),
Space::new().height(iced::Length::Fixed(22.0)),
- container(primary_glow).center_x(Fill),
+ container(primary_row).center_x(Fill),
Space::new().height(iced::Length::Fixed(10.0)),
container(secondary_row).center_x(Fill),
Space::new().height(Fill),
@@ -2614,34 +2295,27 @@ pub(super) fn start_page_view<'a>(
.width(Fill)
.height(Fill);
- // Page background reverts to plain dark β the glow alone provides the
- // brand colour cue, the rest of the page stays neutral so it reads as
- // "workspace area" not "advertising banner".
- const PAGE_BG: Color = Color {
- r: 0.08,
- g: 0.08,
- b: 0.085,
- a: 1.0,
- };
// Collapse side panels one at a time as width shrinks: Tutorials first,
- // then Supporters, and Recent Documents last. The previous all-or-nothing
- // threshold reserved a videos-sized empty margin on both sides of the
- // centred Welcome block and hid every panel around 1664 px.
+ // then Discussions, Supporters, and Recent Documents last.
#[derive(Clone, Copy, PartialEq, Eq)]
enum StartLayout {
AllPanels,
WithoutVideos,
+ WithoutVideosAndDiscussions,
RecentAndWelcome,
Compact,
}
- let recent_w = 280.0f32;
- let videos_w = 300.0f32;
- let sup_w = 240.0f32;
- let welcome_wide_min = 360.0f32;
+ let panel_w = 280.0f32;
+ const VIDEO_PANEL_PADDING: f32 = 16.0;
+ const VIDEO_SCROLL_GUTTER: f32 = 14.0;
+ let measured_action_w = f32::from_bits(
+ action_width_out.load(std::sync::atomic::Ordering::Relaxed)
+ );
+ let welcome_wide_min = measured_action_w.max(360.0);
let avail = (avail_w - 16.0).max(0.0); // minus the page's l/r padding
- let panel_widths = [recent_w, videos_w, sup_w];
- let mut panel_visible = [true, true, true];
- let required_width = |visible: &[bool; 3]| {
+ let panel_widths = [panel_w; 4];
+ let mut panel_visible = [true, true, true, true];
+ let required_width = |visible: &[bool; 4]| {
let visible_panels = visible.iter().filter(|&&shown| shown).count();
welcome_wide_min
+ panel_widths
@@ -2653,16 +2327,17 @@ pub(super) fn start_page_view<'a>(
};
// Re-measure after every collapse. There are no independent breakpoints:
// the available width and the panels' preferred widths decide the state.
- for panel in [1usize, 2, 0] {
+ for panel in [1usize, 2, 3, 0] {
if required_width(&panel_visible) <= avail {
break;
}
panel_visible[panel] = false;
}
let start_layout = match panel_visible {
- [true, true, true] => StartLayout::AllPanels,
- [true, false, true] => StartLayout::WithoutVideos,
- [true, false, false] => StartLayout::RecentAndWelcome,
+ [true, true, true, true] => StartLayout::AllPanels,
+ [true, false, true, true] => StartLayout::WithoutVideos,
+ [true, false, false, true] => StartLayout::WithoutVideosAndDiscussions,
+ [true, false, false, false] => StartLayout::RecentAndWelcome,
_ => StartLayout::Compact,
};
@@ -2674,7 +2349,8 @@ pub(super) fn start_page_view<'a>(
match start_layout {
StartLayout::AllPanels
| StartLayout::WithoutVideos
- | StartLayout::RecentAndWelcome => iced::Length::Fixed(recent_w),
+ | StartLayout::WithoutVideosAndDiscussions
+ | StartLayout::RecentAndWelcome => iced::Length::Fixed(panel_w),
StartLayout::Compact => iced::Length::Fill,
},
);
@@ -2683,20 +2359,16 @@ pub(super) fn start_page_view<'a>(
// Tutorial-videos rail: the official playlist, fetched at boot (cached on
// disk) β thumbnail card + title per video, click opens the browser.
let videos_panel: Element<'a, Message> = {
- const NAME_COLOR: Color = Color {
- r: 0.80,
- g: 0.80,
- b: 0.82,
- a: 1.0,
- };
- // Inner width = panel 300 β padding 2Γ16 β scrollbar gutter 14.
- const THUMB_H: f32 = (300.0 - 32.0 - 14.0) * 9.0 / 16.0;
- let mut list = column![text("Tutorials").size(15).color(TEXT)]
+ // Derive the 16:9 cover box from the actual shared list width so the
+ // whole thumbnail remains visible when that width changes.
+ let thumb_h =
+ (panel_w - VIDEO_PANEL_PADDING * 2.0 - VIDEO_SCROLL_GUTTER) * 9.0 / 16.0;
+ let mut list = column![text("Tutorials").size(15)]
.spacing(10)
.width(Fill)
// Keep the scrollbar off the thumbnails.
.padding(iced::Padding {
- right: 14.0,
+ right: VIDEO_SCROLL_GUTTER,
..iced::Padding::ZERO
});
for v in videos {
@@ -2706,14 +2378,14 @@ pub(super) fn start_page_view<'a>(
container(
iced::widget::image(handle.clone())
.width(Fill)
- .height(iced::Length::Fixed(THUMB_H))
- .content_fit(iced::ContentFit::Cover),
+ .height(iced::Length::Fixed(thumb_h))
+ .content_fit(iced::ContentFit::Contain),
)
.width(Fill)
- .height(iced::Length::Fixed(THUMB_H))
- .style(|_: &Theme| container::Style {
+ .height(iced::Length::Fixed(thumb_h))
+ .style(|theme: &Theme| container::Style {
border: Border {
- color: CARD_BORDER,
+ color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 6.0.into(),
},
@@ -2722,7 +2394,7 @@ pub(super) fn start_page_view<'a>(
.clip(true),
);
}
- card = card.push(text(v.title.clone()).size(12).color(NAME_COLOR));
+ card = card.push(text(v.title.clone()).size(12).style(start_muted_style));
list = list.push(
mouse_area(card)
.interaction(iced::mouse::Interaction::Pointer)
@@ -2735,28 +2407,25 @@ pub(super) fn start_page_view<'a>(
} else {
"Videos load from the internet."
};
- list = list.push(text(note).size(12).color(NAME_COLOR));
+ list = list.push(text(note).size(12).style(start_muted_style));
}
let playlist_btn = mouse_area(
- container(
- text("Open playlist on YouTube").size(12).color(Color::WHITE),
- )
+ container(text("Open playlist on YouTube").size(12))
.padding([6, 10])
.width(Fill)
.center_x(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.85,
- g: 0.15,
- b: 0.15,
- a: 1.0,
- })),
+ .style(|theme: &Theme| {
+ let pair = theme.extended_palette().danger.base;
+ container::Style {
+ background: Some(Background::Color(pair.color)),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 6.0.into(),
},
+ text_color: Some(pair.text),
..Default::default()
+ }
}),
)
.interaction(iced::mouse::Interaction::Pointer)
@@ -2767,21 +2436,144 @@ pub(super) fn start_page_view<'a>(
playlist_btn,
])
.width(match start_layout {
- StartLayout::AllPanels => iced::Length::Fixed(videos_w),
+ StartLayout::AllPanels => iced::Length::Fixed(panel_w),
StartLayout::WithoutVideos
+ | StartLayout::WithoutVideosAndDiscussions
+ | StartLayout::RecentAndWelcome
+ | StartLayout::Compact => iced::Length::Fill,
+ })
+ .height(Fill)
+ .padding(VIDEO_PANEL_PADDING)
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
+ border: Border {
+ color: palette.background.neutral.color,
+ width: 1.0,
+ radius: 8.0.into(),
+ },
+ ..Default::default()
+ }
+ })
+ .into()
+ };
+
+ // GitHub Discussions rail. Native builds refresh from GitHub's public feed;
+ // web builds read the CI-generated snapshot. Both sources mark pinned
+ // discussions and sort them before the rest of the list.
+ let discussions_panel: Element<'a, Message> = {
+ let mut list = column![text("Discussions").size(15)]
+ .spacing(8)
+ .width(Fill);
+ for discussion in discussions {
+ let mut meta = iced::widget::row![
+ text(format!("#{}", discussion.number))
+ .size(10)
+ .style(start_muted_style),
+ ]
+ .spacing(6)
+ .align_y(iced::Center);
+ if discussion.pinned {
+ meta = meta.push(
+ text("Pinned")
+ .size(10)
+ .style(start_primary_style),
+ );
+ }
+ if !discussion.author.is_empty() {
+ meta = meta.push(
+ text(format!("@{}", discussion.author))
+ .size(10)
+ .style(start_muted_style),
+ );
+ }
+ let card = container(
+ column![
+ text(discussion.title.clone()).size(12),
+ meta,
+ ]
+ .spacing(4),
+ )
+ .padding([8, 10])
+ .width(Fill)
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(
+ palette.background.base.color.scale_alpha(0.42),
+ )),
+ border: Border {
+ color: palette.background.neutral.color,
+ width: 1.0,
+ radius: 6.0.into(),
+ },
+ ..Default::default()
+ }
+ });
+ list = list.push(
+ mouse_area(card)
+ .interaction(iced::mouse::Interaction::Pointer)
+ .on_press(Message::OpenUrl(discussion.url.clone())),
+ );
+ }
+ if discussions.is_empty() {
+ let note = if discussions_loading {
+ "Loading discussionsβ¦"
+ } else {
+ "Discussions load from GitHub."
+ };
+ list = list.push(text(note).size(12).style(start_muted_style));
+ }
+ let open_btn = mouse_area(
+ container(text("Open Discussions on GitHub").size(12))
+ .padding([6, 10])
+ .width(Fill)
+ .center_x(Fill)
+ .style(|theme: &Theme| {
+ let pair = theme.extended_palette().primary.base;
+ container::Style {
+ background: Some(Background::Color(pair.color)),
+ border: Border {
+ color: Color::TRANSPARENT,
+ width: 0.0,
+ radius: 6.0.into(),
+ },
+ text_color: Some(pair.text),
+ ..Default::default()
+ }
+ }),
+ )
+ .interaction(iced::mouse::Interaction::Pointer)
+ .on_press(Message::OpenUrl(
+ crate::discussions::DISCUSSIONS_URL.to_string(),
+ ));
+ container(column![
+ iced::widget::scrollable(list).height(Fill),
+ Space::new().height(iced::Length::Fixed(12.0)),
+ open_btn,
+ ])
+ .width(match start_layout {
+ StartLayout::AllPanels | StartLayout::WithoutVideos => {
+ iced::Length::Fixed(panel_w)
+ }
+ StartLayout::WithoutVideosAndDiscussions
| StartLayout::RecentAndWelcome
| StartLayout::Compact => iced::Length::Fill,
})
.height(Fill)
.padding(16)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(CARD_BG)),
- border: Border {
- color: CARD_BORDER,
- width: 1.0,
- radius: 8.0.into(),
- },
- ..Default::default()
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
+ border: Border {
+ color: palette.background.neutral.color,
+ width: 1.0,
+ radius: 8.0.into(),
+ },
+ ..Default::default()
+ }
})
.into()
};
@@ -2790,14 +2582,8 @@ pub(super) fn start_page_view<'a>(
// (no token configured / offline) only the "Support on Patreon" button
// shows, so the rail always invites support.
let supporters: Element<'a, Message> = {
- const NAME_COLOR: Color = Color {
- r: 0.78,
- g: 0.78,
- b: 0.80,
- a: 1.0,
- };
let mut list = column![
- text("Supporters").size(15).color(TEXT),
+ text("Supporters").size(15),
Space::new().height(iced::Length::Fixed(12.0)),
]
.spacing(6)
@@ -2808,8 +2594,8 @@ pub(super) fn start_page_view<'a>(
let amount = format!("${:.2}", *cents as f64 / 100.0);
list = list.push(
iced::widget::row![
- text(name).size(12).color(NAME_COLOR).width(Fill),
- text(amount).size(12).color(NAME_COLOR),
+ text(name).size(12).style(start_muted_style).width(Fill),
+ text(amount).size(12).style(start_muted_style),
]
.spacing(6),
);
@@ -2817,8 +2603,8 @@ pub(super) fn start_page_view<'a>(
let support_btn = mouse_area(
container(
iced::widget::row![
- crate::ui::icons::tinted(crate::ui::icons::HEART, 13.0, Color::WHITE),
- text("Support on Patreon").size(12).color(Color::WHITE),
+ crate::ui::icons::themed_danger_text(crate::ui::icons::HEART, 13.0),
+ text("Support on Patreon").size(12),
]
.spacing(6)
.align_y(iced::Center),
@@ -2826,19 +2612,18 @@ pub(super) fn start_page_view<'a>(
.padding([6, 10])
.width(Fill)
.center_x(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.90,
- g: 0.28,
- b: 0.30,
- a: 1.0,
- })),
+ .style(|theme: &Theme| {
+ let pair = theme.extended_palette().danger.base;
+ container::Style {
+ background: Some(Background::Color(pair.color)),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 6.0.into(),
},
+ text_color: Some(pair.text),
..Default::default()
+ }
}),
)
.interaction(iced::mouse::Interaction::Pointer)
@@ -2851,22 +2636,27 @@ pub(super) fn start_page_view<'a>(
support_btn,
])
.width(match start_layout {
- StartLayout::AllPanels | StartLayout::WithoutVideos => {
- iced::Length::Fixed(sup_w)
+ StartLayout::AllPanels
+ | StartLayout::WithoutVideos
+ | StartLayout::WithoutVideosAndDiscussions => {
+ iced::Length::Fixed(panel_w)
}
StartLayout::RecentAndWelcome
| StartLayout::Compact => iced::Length::Fill,
})
.height(Fill)
.padding(20)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(CARD_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: CARD_BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 8.0.into(),
},
..Default::default()
+ }
})
.into()
};
@@ -2876,12 +2666,19 @@ pub(super) fn start_page_view<'a>(
recent,
videos_panel,
welcome,
+ discussions_panel,
supporters,
]
.spacing(16)
.height(Fill)
.into(),
StartLayout::WithoutVideos => {
+ iced::widget::row![recent, welcome, discussions_panel, supporters]
+ .spacing(16)
+ .height(Fill)
+ .into()
+ }
+ StartLayout::WithoutVideosAndDiscussions => {
iced::widget::row![recent, welcome, supporters]
.spacing(16)
.height(Fill)
@@ -2897,44 +2694,38 @@ pub(super) fn start_page_view<'a>(
button(text(label).size(14))
.on_press(Message::StartSectionSelect(section))
.padding([8, 18])
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (is_active, status) {
- (true, _) => Color {
- r: 0.18,
- g: 0.18,
- b: 0.20,
- a: 1.0,
- },
- (false, button::Status::Hovered) => Color {
- r: 0.14,
- g: 0.14,
- b: 0.16,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- text_color: if is_active {
- TEXT
- } else {
- Color {
- r: 0.62,
- g: 0.62,
- b: 0.62,
- a: 1.0,
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match (is_active, status) {
+ (true, _) => Some(palette.primary.weak),
+ (false, button::Status::Hovered) => {
+ Some(palette.background.strong)
}
- },
+ _ => None,
+ };
+ button::Style {
+ background: pair.map(|p| Background::Color(p.color)),
+ text_color: pair
+ .map(|p| p.text)
+ .unwrap_or(palette.background.base.text.scale_alpha(0.68)),
border: Border {
- color: if is_active { BRAND } else { Color::TRANSPARENT },
+ color: if is_active {
+ palette.primary.base.color
+ } else {
+ Color::TRANSPARENT
+ },
width: if is_active { 1.0 } else { 0.0 },
radius: 6.0.into(),
},
..Default::default()
+ }
})
};
let tab_bar = WrapFlow::new(vec![
tab_btn("Recent Files", super::StartSection::Recent).into(),
tab_btn("Videos", super::StartSection::Videos).into(),
tab_btn("Welcome", super::StartSection::Welcome).into(),
+ tab_btn("Discussions", super::StartSection::Discussions).into(),
tab_btn("Supporters", super::StartSection::Supporters).into(),
])
.spacing_x(6.0)
@@ -2951,6 +2742,11 @@ pub(super) fn start_page_view<'a>(
.center_x(Fill)
.into(),
super::StartSection::Welcome => welcome.into(),
+ super::StartSection::Discussions => container(discussions_panel)
+ .width(Fill)
+ .height(Fill)
+ .center_x(Fill)
+ .into(),
super::StartSection::Supporters => container(supporters)
.width(Fill)
.height(Fill)
@@ -2969,8 +2765,10 @@ pub(super) fn start_page_view<'a>(
};
container(body)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PAGE_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.padding(iced::Padding {
@@ -3000,44 +2798,16 @@ pub(super) fn recent_files_panel<'a>(
limit_input: &'a str,
width: iced::Length,
) -> Element<'a, Message> {
- // Card chrome matches the Supporters rail (the canonical start-page card).
- const PANEL_BG: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.13,
- a: 1.0,
- };
- const PANEL_BORDER: Color = Color {
- r: 0.20,
- g: 0.20,
- b: 0.22,
- a: 1.0,
- };
- const ITEM_HOVER: Color = Color {
- r: 0.16,
- g: 0.16,
- b: 0.18,
- a: 1.0,
- };
- const TEXT: Color = Color {
- r: 0.94,
- g: 0.93,
- b: 0.92,
- a: 1.0,
- };
- const MUTED: Color = Color {
- r: 0.60,
- g: 0.60,
- b: 0.62,
- a: 1.0,
- };
-
// Title mirrors the Supporters rail: size 15 in the bright text colour,
// followed by a 12px gap before the content.
- let title = text("Recent Documents").size(15).color(TEXT);
+ let title = text("Recent Documents").size(15);
let body: Element<'a, Message> = if recents.is_empty() {
- container(text("Files you open will show up here.").size(12).color(MUTED))
+ container(
+ text("Files you open will show up here.")
+ .size(12)
+ .style(start_muted_style)
+ )
.height(Fill)
.into()
} else {
@@ -3078,11 +2848,10 @@ pub(super) fn recent_files_panel<'a>(
thumb,
column![
text(crate::ui::text_util::elide(&name, 28))
- .size(12)
- .color(TEXT),
+ .size(12),
text(crate::ui::text_util::elide(&dir, 38))
.size(10)
- .color(MUTED),
+ .style(start_muted_style),
]
.spacing(2),
]
@@ -3092,41 +2861,42 @@ pub(super) fn recent_files_panel<'a>(
.on_press(Message::OpenRecent(path_for_open))
.padding([6, 12])
.width(Fill)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ITEM_HOVER,
- _ => Color::TRANSPARENT,
- })),
- text_color: TEXT,
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ button::Style {
+ background: matches!(status, button::Status::Hovered).then_some(
+ Background::Color(palette.background.strong.color)
+ ),
+ text_color: palette.background.base.text,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
..Default::default()
+ }
});
let path_for_remove = path.clone();
- let remove_btn = button(crate::ui::icons::tinted(crate::ui::icons::CLOSE, 11.0, MUTED))
+ let remove_btn = button(crate::ui::icons::themed_secondary(
+ crate::ui::icons::CLOSE,
+ 11.0,
+ ))
.on_press(Message::RecentRemove(path_for_remove))
.padding([4, 8])
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => Color {
- r: 0.45,
- g: 0.15,
- b: 0.15,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- text_color: MUTED,
+ .style(|theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ button::Style {
+ background: matches!(status, button::Status::Hovered)
+ .then_some(Background::Color(palette.danger.weak.color)),
+ text_color: palette.background.base.text.scale_alpha(0.68),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
},
..Default::default()
+ }
});
col = col.push(row![open_btn, remove_btn].spacing(0).align_y(iced::Center));
@@ -3139,18 +2909,20 @@ pub(super) fn recent_files_panel<'a>(
// update handler clamps to [MIN, MAX] and persists (see `set_recent_limit`),
// so an over-max entry snaps to the max.
const STEP: usize = 5;
- let step_style = |_: &Theme, status: button::Status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ITEM_HOVER,
- _ => Color::TRANSPARENT,
- })),
- text_color: TEXT,
+ let step_style = |theme: &Theme, status: button::Status| {
+ let palette = theme.extended_palette();
+ button::Style {
+ background: matches!(status, button::Status::Hovered).then_some(
+ Background::Color(palette.background.strong.color)
+ ),
+ text_color: palette.background.base.text,
border: Border {
- color: PANEL_BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
+ }
};
// +/- step from whatever is currently shown in the box (mid-edit included).
let shown = limit_input.parse::().unwrap_or(limit);
@@ -3161,19 +2933,19 @@ pub(super) fn recent_files_panel<'a>(
.padding([2, 6])
.width(iced::Length::Fixed(46.0));
let limit_row = row![
- text("Keep recent files").size(11).color(MUTED).width(Fill),
- button(crate::ui::icons::tinted(crate::ui::icons::MINUS, 11.0, TEXT))
+ text("Keep recent files").size(11).style(start_muted_style).width(Fill),
+ button(crate::ui::icons::themed(crate::ui::icons::MINUS, 11.0))
.on_press(Message::SetRecentLimit(shown.saturating_sub(STEP)))
.padding([3, 6])
.style(step_style),
count_box,
- button(crate::ui::icons::tinted(crate::ui::icons::PLUS, 11.0, TEXT))
+ button(crate::ui::icons::themed(crate::ui::icons::PLUS, 11.0))
.on_press(Message::SetRecentLimit(shown + STEP))
.padding([3, 6])
.style(step_style),
text(format!("/ {}", super::recent::RECENT_MAX))
.size(11)
- .color(MUTED),
+ .style(start_muted_style),
]
.spacing(6)
.align_y(iced::Center);
@@ -3191,14 +2963,17 @@ pub(super) fn recent_files_panel<'a>(
.width(width)
.height(Fill)
.padding(20)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: PANEL_BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 8.0.into(),
},
..Default::default()
+ }
})
.into()
}
diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs
index 45a2eea02..9e4f6b049 100644
--- a/src/app/view/modal.rs
+++ b/src/app/view/modal.rs
@@ -1,6 +1,6 @@
use super::super::{Message, OpenCADStudio};
use iced::widget::{button, column, container, pick_list, row, text, Space};
-use iced::{Background, Border, Color, Element, Fill, Theme};
+use iced::{Background, Element, Fill, Theme};
impl OpenCADStudio {
/// Title shown in the active modal's title bar, left of the move/close
@@ -63,9 +63,13 @@ impl OpenCADStudio {
sized(crate::ui::window::alias_editor::view_window(&self.alias_editor_rows), 480, 520)
}
super::super::ModalKind::Options => sized(
- crate::ui::window::options::view_window(&self.default_save_format),
- 480,
- 190,
+ crate::ui::window::options::view_window(
+ &self.default_save_format,
+ &self.ui_theme,
+ &self.theme_color_inputs,
+ ),
+ 520,
+ 500,
),
super::super::ModalKind::PluginManager => sized(
crate::ui::window::plugin_manager::view_window(
@@ -712,125 +716,56 @@ impl OpenCADStudio {
}
}
+fn dialog_button(
+ label: &'static str,
+ message: Message,
+ style: fn(&Theme, button::Status) -> button::Style,
+) -> Element<'static, Message> {
+ button(text(label).size(13))
+ .on_press(message)
+ .style(style)
+ .padding([6, 18])
+ .into()
+}
+
+fn dialog_body_style(theme: &Theme) -> container::Style {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
+ text_color: Some(palette.background.base.text),
+ ..Default::default()
+ }
+}
+
+fn dialog_muted_text_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
/// Compact Save-As options dialog: pick the format/version and a default file
/// name. The destination folder and overwrite confirmation come from the
/// native OS save dialog (native) or the browser download (web) that follows.
fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a, Message> {
- const BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.17,
- a: 1.0,
- };
- const BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.36,
- a: 1.0,
- };
- const TEXT: Color = Color {
- r: 0.90,
- g: 0.90,
- b: 0.90,
- a: 1.0,
- };
- const DIM: Color = Color {
- r: 0.58,
- g: 0.58,
- b: 0.62,
- a: 1.0,
- };
- const BTN_OK: Color = Color {
- r: 0.20,
- g: 0.46,
- b: 0.80,
- a: 1.0,
- };
- const BTN_HOV: Color = Color {
- r: 0.26,
- g: 0.55,
- b: 0.92,
- a: 1.0,
- };
- const BTN_GREY: Color = Color {
- r: 0.26,
- g: 0.26,
- b: 0.29,
- a: 1.0,
- };
- const BTN_GHOV: Color = Color {
- r: 0.34,
- g: 0.34,
- b: 0.38,
- a: 1.0,
- };
-
- let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
- button(text(lbl).size(12).color(TEXT))
- .on_press(msg)
- .style(move |_: &Theme, st| button::Style {
- background: Some(Background::Color(
- if matches!(st, button::Status::Hovered | button::Status::Pressed) {
- hov
- } else {
- base
- },
- )),
- text_color: TEXT,
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- })
- .padding([4, 12])
- };
-
let sel_fmt = crate::io::SAVE_FORMAT_OPTIONS
.iter()
.copied()
.find(|&s| s == format);
- let label = |s: &'static str| text(s).size(11).color(DIM);
+ let label = |s: &'static str| text(s).size(11).style(dialog_muted_text_style);
let mut items: Vec> = Vec::new();
- items.push(text("Save Drawing As").size(14).color(TEXT).into());
+ items.push(text("Save Drawing As").size(14).into());
items.push(Space::new().height(12).into());
// Web has no native file dialog, so the file name is typed here. On native
// the OS save dialog collects the name, so this field is omitted.
#[cfg(target_arch = "wasm32")]
{
- const INPUT_BG: Color = Color {
- r: 0.10,
- g: 0.10,
- b: 0.12,
- a: 1.0,
- };
- let input_sty =
- |_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style {
- background: Background::Color(INPUT_BG),
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- icon: TEXT,
- placeholder: DIM,
- value: TEXT,
- selection: Color {
- r: 0.20,
- g: 0.46,
- b: 0.80,
- a: 0.45,
- },
- };
items.push(
row![
label("File name:").width(70),
iced::widget::text_input("drawing.dwg", filename)
.on_input(Message::SaveDialogFilenameChanged)
- .style(input_sty)
.size(13)
.padding([5, 8])
.width(Fill),
@@ -860,9 +795,9 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a,
items.push(
row![
Space::new().width(Fill),
- btn("Save as...", Message::SaveDialogConfirm, BTN_OK, BTN_HOV),
+ dialog_button("Save as...", Message::SaveDialogConfirm, button::primary),
Space::new().width(8),
- btn("Cancel", Message::SaveDialogCancel, BTN_GREY, BTN_GHOV),
+ dialog_button("Cancel", Message::SaveDialogCancel, button::secondary),
]
.into(),
);
@@ -870,10 +805,7 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a,
let body = column(items).spacing(0);
container(body)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
- ..Default::default()
- })
+ .style(dialog_body_style)
.padding([14, 16])
.width(Fill)
.height(Fill)
@@ -881,89 +813,23 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a,
}
fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
- const BG: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.20,
- a: 1.0,
- };
- const BORDER_COL: Color = Color {
- r: 0.38,
- g: 0.38,
- b: 0.42,
- a: 1.0,
- };
- const TEXT_COL: Color = Color {
- r: 0.90,
- g: 0.90,
- b: 0.90,
- a: 1.0,
- };
- const BTN_SAVE: Color = Color {
- r: 0.20,
- g: 0.46,
- b: 0.80,
- a: 1.0,
- };
- const BTN_HOVER: Color = Color {
- r: 0.26,
- g: 0.55,
- b: 0.92,
- a: 1.0,
- };
- const BTN_DISC: Color = Color {
- r: 0.28,
- g: 0.28,
- b: 0.30,
- a: 1.0,
- };
- const BTN_DHOV: Color = Color {
- r: 0.36,
- g: 0.36,
- b: 0.40,
- a: 1.0,
- };
-
let body_text = format!("Do you want to save changes to \"{}\"?", name);
- let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
- button(text(label).size(13).color(TEXT_COL))
- .on_press(msg)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => hov,
- _ => base,
- })),
- text_color: TEXT_COL,
- border: Border {
- color: BORDER_COL,
- width: 1.0,
- radius: 4.0.into(),
- },
- shadow: iced::Shadow::default(),
- snap: false,
- })
- .padding([6, 18])
- };
-
container(
column![
- text(body_text).size(13).color(TEXT_COL),
+ text(body_text).size(13),
iced::widget::Space::new().height(20),
row![
- btn("Save", Message::UnsavedDialogSave, BTN_SAVE, BTN_HOVER),
+ dialog_button("Save", Message::UnsavedDialogSave, button::primary),
iced::widget::Space::new().width(8),
- btn("Discard", Message::UnsavedDialogDiscard, BTN_DISC, BTN_DHOV),
+ dialog_button("Discard", Message::UnsavedDialogDiscard, button::danger),
iced::widget::Space::new().width(8),
- btn("Cancel", Message::UnsavedDialogCancel, BTN_DISC, BTN_DHOV),
+ dialog_button("Cancel", Message::UnsavedDialogCancel, button::secondary),
],
]
.spacing(0),
)
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
- ..Default::default()
- })
+ .style(dialog_body_style)
.center(Fill)
.padding([24, 28])
.into()
@@ -971,55 +837,6 @@ fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
#[cfg(not(target_arch = "wasm32"))]
fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Message> {
- const BG: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.20,
- a: 1.0,
- };
- const BORDER_COL: Color = Color {
- r: 0.38,
- g: 0.38,
- b: 0.42,
- a: 1.0,
- };
- const TEXT_COL: Color = Color {
- r: 0.90,
- g: 0.90,
- b: 0.90,
- a: 1.0,
- };
- const DIM_COL: Color = Color {
- r: 0.62,
- g: 0.62,
- b: 0.66,
- a: 1.0,
- };
- const BTN_PRIMARY: Color = Color {
- r: 0.20,
- g: 0.46,
- b: 0.80,
- a: 1.0,
- };
- const BTN_PRIMARY_HOVER: Color = Color {
- r: 0.26,
- g: 0.55,
- b: 0.92,
- a: 1.0,
- };
- const BTN_SECONDARY: Color = Color {
- r: 0.28,
- g: 0.28,
- b: 0.30,
- a: 1.0,
- };
- const BTN_SECONDARY_HOVER: Color = Color {
- r: 0.36,
- g: 0.36,
- b: 0.40,
- a: 1.0,
- };
-
let file_name = std::path::Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
@@ -1027,70 +844,45 @@ fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Messag
let heading = format!("\"{file_name}\" could not be saved.");
let path_line = format!("Path: {path}");
let details = format!("Details: {error}");
- let btn = |label: &'static str, msg: Message, base: Color, hover: Color| {
- button(text(label).size(13).color(TEXT_COL))
- .on_press(msg)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => hover,
- _ => base,
- })),
- text_color: TEXT_COL,
- border: Border {
- color: BORDER_COL,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- })
- .padding([6, 18])
- };
container(
column![
- text(heading).size(14).color(TEXT_COL),
+ text(heading).size(14),
Space::new().height(8),
text(
"The file is open or being used by another application. \
Close it there and retry, or save this drawing under a different name."
)
.size(13)
- .color(TEXT_COL)
.width(Fill),
Space::new().height(12),
- text(path_line).size(11).color(DIM_COL).width(Fill),
+ text(path_line).size(11).style(dialog_muted_text_style).width(Fill),
Space::new().height(4),
- text(details).size(11).color(DIM_COL).width(Fill),
+ text(details).size(11).style(dialog_muted_text_style).width(Fill),
Space::new().height(18),
row![
- btn(
+ dialog_button(
"Retry",
Message::SaveFileInUseRetry,
- BTN_PRIMARY,
- BTN_PRIMARY_HOVER
+ button::primary
),
Space::new().width(8),
- btn(
+ dialog_button(
"Save As",
Message::SaveFileInUseSaveAs,
- BTN_SECONDARY,
- BTN_SECONDARY_HOVER
+ button::secondary
),
Space::new().width(8),
- btn(
+ dialog_button(
"Cancel",
Message::SaveFileInUseCancel,
- BTN_SECONDARY,
- BTN_SECONDARY_HOVER
+ button::secondary
),
],
]
.spacing(0),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
- ..Default::default()
- })
+ .style(dialog_body_style)
.padding([18, 20])
.width(Fill)
.height(Fill)
@@ -1099,142 +891,55 @@ fn file_in_use_dialog_window(path: &str, error: &str) -> Element<'static, Messag
#[cfg(not(target_arch = "wasm32"))]
fn external_change_dialog_window(path: &str) -> Element<'static, Message> {
- const BG: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.20,
- a: 1.0,
- };
- const BORDER_COL: Color = Color {
- r: 0.38,
- g: 0.38,
- b: 0.42,
- a: 1.0,
- };
- const TEXT_COL: Color = Color {
- r: 0.90,
- g: 0.90,
- b: 0.90,
- a: 1.0,
- };
- const DIM_COL: Color = Color {
- r: 0.62,
- g: 0.62,
- b: 0.66,
- a: 1.0,
- };
- const BTN_PRIMARY: Color = Color {
- r: 0.20,
- g: 0.46,
- b: 0.80,
- a: 1.0,
- };
- const BTN_PRIMARY_HOVER: Color = Color {
- r: 0.26,
- g: 0.55,
- b: 0.92,
- a: 1.0,
- };
- const BTN_SECONDARY: Color = Color {
- r: 0.28,
- g: 0.28,
- b: 0.30,
- a: 1.0,
- };
- const BTN_SECONDARY_HOVER: Color = Color {
- r: 0.36,
- g: 0.36,
- b: 0.40,
- a: 1.0,
- };
- const BTN_DANGER: Color = Color {
- r: 0.62,
- g: 0.31,
- b: 0.14,
- a: 1.0,
- };
- const BTN_DANGER_HOVER: Color = Color {
- r: 0.78,
- g: 0.39,
- b: 0.17,
- a: 1.0,
- };
-
let file_name = std::path::Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "Drawing".to_string());
let heading = format!("\"{file_name}\" was changed by another application.");
let path_line = format!("Path: {path}");
- let btn = |label: &'static str, msg: Message, base: Color, hover: Color| {
- button(text(label).size(13).color(TEXT_COL))
- .on_press(msg)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => hover,
- _ => base,
- })),
- text_color: TEXT_COL,
- border: Border {
- color: BORDER_COL,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- })
- .padding([6, 14])
- };
container(
column![
- text(heading).size(14).color(TEXT_COL),
+ text(heading).size(14),
Space::new().height(8),
text(
"Saving now could destroy those external changes. Reload the disk copy, \
save your local work elsewhere, or explicitly overwrite it."
)
.size(13)
- .color(TEXT_COL)
.width(Fill),
Space::new().height(12),
- text(path_line).size(11).color(DIM_COL).width(Fill),
+ text(path_line).size(11).style(dialog_muted_text_style).width(Fill),
Space::new().height(18),
row![
- btn(
+ dialog_button(
"Reload from Disk",
Message::ExternalChangeReload,
- BTN_PRIMARY,
- BTN_PRIMARY_HOVER
+ button::primary
),
Space::new().width(8),
- btn(
+ dialog_button(
"Save As",
Message::ExternalChangeSaveAs,
- BTN_SECONDARY,
- BTN_SECONDARY_HOVER
+ button::secondary
),
Space::new().width(8),
- btn(
+ dialog_button(
"Overwrite",
Message::ExternalChangeOverwrite,
- BTN_DANGER,
- BTN_DANGER_HOVER
+ button::danger
),
Space::new().width(8),
- btn(
+ dialog_button(
"Cancel",
Message::ExternalChangeCancel,
- BTN_SECONDARY,
- BTN_SECONDARY_HOVER
+ button::secondary
),
],
]
.spacing(0),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
- ..Default::default()
- })
+ .style(dialog_body_style)
.padding([18, 20])
.width(Fill)
.height(Fill)
@@ -1246,58 +951,31 @@ fn external_change_dialog_window(path: &str) -> Element<'static, Message> {
/// bytes, so saving to a different version or to DXF would drop them. Offers to
/// save in the source version (keep them) or proceed (drop them).
fn aec_drop_dialog_window(count: usize, target: &str, src_version: &str) -> Element<'static, Message> {
- const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
- const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 };
- const TEXT_COL: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
- const BTN_SAVE: Color = Color { r: 0.20, g: 0.46, b: 0.80, a: 1.0 };
- const BTN_HOVER: Color = Color { r: 0.26, g: 0.55, b: 0.92, a: 1.0 };
- const BTN_DISC: Color = Color { r: 0.28, g: 0.28, b: 0.30, a: 1.0 };
- const BTN_DHOV: Color = Color { r: 0.36, g: 0.36, b: 0.40, a: 1.0 };
-
let body_text = format!(
"This drawing contains {count} AEC/Civil objects that \"{target}\" \
cannot store, so they will not be saved.\n\n\
To keep them, save in the source version ({src_version})."
);
- let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
- button(text(label).size(13).color(TEXT_COL))
- .on_press(msg)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => hov,
- _ => base,
- })),
- text_color: TEXT_COL,
- border: Border {
- color: BORDER_COL,
- width: 1.0,
- radius: 4.0.into(),
- },
- shadow: iced::Shadow::default(),
- snap: false,
- })
- .padding([6, 14])
- };
-
container(
column![
- text(body_text).size(13).color(TEXT_COL),
+ text(body_text).size(13),
iced::widget::Space::new().height(20),
row![
- btn("Save in source version", Message::AecDropSameVersion, BTN_SAVE, BTN_HOVER),
+ dialog_button(
+ "Save in source version",
+ Message::AecDropSameVersion,
+ button::primary
+ ),
iced::widget::Space::new().width(8),
- btn("Save anyway", Message::AecDropProceed, BTN_DISC, BTN_DHOV),
+ dialog_button("Save anyway", Message::AecDropProceed, button::warning),
iced::widget::Space::new().width(8),
- btn("Back", Message::AecDropBack, BTN_DISC, BTN_DHOV),
+ dialog_button("Back", Message::AecDropBack, button::secondary),
],
]
.spacing(0),
)
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
- ..Default::default()
- })
+ .style(dialog_body_style)
.center(Fill)
.padding([24, 28])
.into()
@@ -1308,14 +986,6 @@ fn aec_drop_dialog_window(count: usize, target: &str, src_version: &str) -> Elem
/// Confirm deleting layer(s) that still have objects on them. "Delete Objects"
/// erases them and removes the layers; "Cancel" leaves everything.
fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'static, Message> {
- const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
- const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 };
- const TEXT_COL: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
- const BTN_DEL: Color = Color { r: 0.72, g: 0.26, b: 0.24, a: 1.0 };
- const BTN_DEL_HOV: Color = Color { r: 0.84, g: 0.32, b: 0.30, a: 1.0 };
- const BTN_CANCEL: Color = Color { r: 0.28, g: 0.28, b: 0.30, a: 1.0 };
- const BTN_CANCEL_HOV: Color = Color { r: 0.36, g: 0.36, b: 0.40, a: 1.0 };
-
let obj = if count == 1 { "object" } else { "objects" };
let subject = if names.len() == 1 {
format!("Layer \"{}\"", names[0])
@@ -1328,38 +998,23 @@ fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'stati
if count == 1 { "that object" } else { "those objects" }
);
- let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
- button(text(label).size(13).color(TEXT_COL))
- .on_press(msg)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => hov,
- _ => base,
- })),
- text_color: TEXT_COL,
- border: Border { color: BORDER_COL, width: 1.0, radius: 4.0.into() },
- shadow: iced::Shadow::default(),
- snap: false,
- })
- .padding([6, 18])
- };
-
container(
column![
- text(body_text).size(13).color(TEXT_COL),
+ text(body_text).size(13),
iced::widget::Space::new().height(20),
row![
- btn("Delete Objects", Message::LayerDeleteConfirm, BTN_DEL, BTN_DEL_HOV),
+ dialog_button(
+ "Delete Objects",
+ Message::LayerDeleteConfirm,
+ button::danger
+ ),
iced::widget::Space::new().width(8),
- btn("Cancel", Message::CloseModal, BTN_CANCEL, BTN_CANCEL_HOV),
+ dialog_button("Cancel", Message::CloseModal, button::secondary),
],
]
.spacing(0),
)
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
- ..Default::default()
- })
+ .style(dialog_body_style)
.center(Fill)
.padding([24, 28])
.into()
@@ -1370,99 +1025,30 @@ fn layer_delete_warning_window(names: &[String], count: usize) -> Element<'stati
/// just dismisses. Either answer flips the persisted `default_assoc_prompted`
/// flag so the dialog never reappears.
fn default_assoc_dialog_window() -> Element<'static, Message> {
- const BG: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.20,
- a: 1.0,
- };
- const BORDER_COL: Color = Color {
- r: 0.38,
- g: 0.38,
- b: 0.42,
- a: 1.0,
- };
- const TEXT_COL: Color = Color {
- r: 0.90,
- g: 0.90,
- b: 0.90,
- a: 1.0,
- };
- const DIM_COL: Color = Color {
- r: 0.62,
- g: 0.62,
- b: 0.66,
- a: 1.0,
- };
- const BTN_YES: Color = Color {
- r: 0.20,
- g: 0.46,
- b: 0.80,
- a: 1.0,
- };
- const BTN_YHOV: Color = Color {
- r: 0.26,
- g: 0.55,
- b: 0.92,
- a: 1.0,
- };
- const BTN_NO: Color = Color {
- r: 0.28,
- g: 0.28,
- b: 0.30,
- a: 1.0,
- };
- const BTN_NHOV: Color = Color {
- r: 0.36,
- g: 0.36,
- b: 0.40,
- a: 1.0,
- };
-
- let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
- button(text(label).size(13).color(TEXT_COL))
- .on_press(msg)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => hov,
- _ => base,
- })),
- text_color: TEXT_COL,
- border: Border {
- color: BORDER_COL,
- width: 1.0,
- radius: 4.0.into(),
- },
- shadow: iced::Shadow::default(),
- snap: false,
- })
- .padding([6, 18])
- };
-
container(
column![
text("Make Open CAD Studio your default CAD app?")
- .size(15)
- .color(TEXT_COL),
+ .size(15),
iced::widget::Space::new().height(10),
text("Open .dwg and .dxf drawings in Open CAD Studio by default. You can change this later in your system settings.")
.size(12)
- .color(DIM_COL),
+ .style(dialog_muted_text_style),
iced::widget::Space::new().height(22),
row![
iced::widget::Space::new().width(Fill),
- btn("Not now", Message::AssocPromptNo, BTN_NO, BTN_NHOV),
+ dialog_button("Not now", Message::AssocPromptNo, button::secondary),
iced::widget::Space::new().width(8),
- btn("Yes, set as default", Message::AssocPromptYes, BTN_YES, BTN_YHOV),
+ dialog_button(
+ "Yes, set as default",
+ Message::AssocPromptYes,
+ button::primary
+ ),
]
.align_y(iced::Center),
]
.spacing(0),
)
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
- ..Default::default()
- })
+ .style(dialog_body_style)
.center(Fill)
.padding([24, 28])
.into()
diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs
index c96449016..a9ba0d98b 100644
--- a/src/app/view/overlay.rs
+++ b/src/app/view/overlay.rs
@@ -9,18 +9,7 @@ pub(super) fn position_canvas_overlay<'a>(
anchor: iced::Point,
panel: Element<'a, Message>,
) -> Element<'a, Message> {
- let ax = anchor.x.max(0.0);
- let ay = anchor.y.max(0.0);
- column![
- Space::new().height(iced::Length::Fixed(ay)),
- row![
- Space::new().width(iced::Length::Fixed(ax)),
- iced::widget::opaque(panel),
- ],
- ]
- .width(Fill)
- .height(Fill)
- .into()
+ crate::ui::pin_at(anchor, iced::widget::opaque(panel))
}
// ββ In-place MText editor overlay βββββββββββββββββββββββββββββββββββββββββββ
@@ -37,19 +26,6 @@ pub(super) fn text_inline_overlay(
ed: &super::super::text_inline::TextInlineState,
canvas: (f32, f32),
) -> Element<'_, Message> {
- const PANEL_BG: Color = Color {
- r: 0.16,
- g: 0.16,
- b: 0.16,
- a: 0.98,
- };
- const BORDER: Color = Color {
- r: 0.40,
- g: 0.40,
- b: 0.40,
- a: 1.0,
- };
-
let field = text_input("Text", &ed.value)
.id(iced::widget::Id::new(TEXT_INLINE_ID))
.on_input(Message::TextInlineInput)
@@ -59,14 +35,17 @@ pub(super) fn text_inline_overlay(
.width(iced::Length::Fixed(240.0));
let panel = container(field)
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ .style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 5.0.into(),
},
..Default::default()
+ }
})
.padding(4);
@@ -206,7 +185,7 @@ impl iced::widget::canvas::Program for MTextPreview {
&self,
_state: &MTextPreviewState,
renderer: &iced::Renderer,
- _theme: &Theme,
+ theme: &Theme,
bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor,
) -> Vec {
@@ -232,12 +211,7 @@ impl iced::widget::canvas::Program for MTextPreview {
);
frame.fill(
&rect,
- Color {
- r: 0.20,
- g: 0.42,
- b: 0.72,
- a: 0.45,
- },
+ theme.extended_palette().primary.base.color.scale_alpha(0.45),
);
}
}
@@ -273,12 +247,7 @@ impl iced::widget::canvas::Program for MTextPreview {
frame.stroke(
&path,
Stroke::default()
- .with_color(Color {
- r: 0.95,
- g: 0.95,
- b: 0.55,
- a: 1.0,
- })
+ .with_color(theme.extended_palette().warning.base.color)
.with_width(1.5),
);
} else if collapsed {
@@ -302,12 +271,7 @@ impl iced::widget::canvas::Program for MTextPreview {
frame.stroke(
&path,
Stroke::default()
- .with_color(Color {
- r: 0.95,
- g: 0.95,
- b: 0.55,
- a: 1.0,
- })
+ .with_color(theme.extended_palette().warning.base.color)
.with_width(1.5),
);
}
@@ -355,59 +319,34 @@ pub(super) fn mtext_editor_overlay<'a>(
modal_resize: iced::Vector,
) -> Element<'a, Message> {
use super::super::mtext_editor::{JustifyChoice, MTextFmt, ParaAlign};
- use iced::widget::{canvas, svg};
+ use iced::widget::canvas;
- const BORDER: Color = Color {
- r: 0.40,
- g: 0.40,
- b: 0.40,
- a: 1.0,
- };
- const TEXT_COL: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
- };
- const FIELD_BG: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
- };
-
- let btn_style = |_: &Theme, status: button::Status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.28,
- g: 0.40,
- b: 0.55,
- a: 1.0,
- },
- _ => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- },
- })),
- text_color: TEXT_COL,
+ let btn_style = |theme: &Theme, status: button::Status| {
+ let palette = theme.extended_palette();
+ let pair = match status {
+ button::Status::Hovered | button::Status::Pressed => palette.background.strong,
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
shadow: iced::Shadow::default(),
snap: false,
+ }
};
let icon_btn = move |bytes: &'static [u8], msg: Message| -> Element<'static, Message> {
- button(svg(svg::Handle::from_memory(bytes)).width(18).height(18))
+ button(crate::ui::icons::themed(bytes, 18.0))
.on_press(msg)
.padding(3)
.style(btn_style)
.into()
};
- let lbl = |s: &'static str| text(s).size(11).color(TEXT_COL);
+ let lbl = |s: &'static str| text(s).size(11);
let small_input = |placeholder: &'static str,
val: &str,
on: fn(String) -> Message,
@@ -651,14 +590,17 @@ pub(super) fn mtext_editor_overlay<'a>(
.width(Fill)
.height(Fill),
)
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(FIELD_BG)),
+ .style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
+ }
})
.padding(2)
.width(Fill)
@@ -676,8 +618,10 @@ pub(super) fn mtext_editor_overlay<'a>(
]
.align_y(iced::Alignment::Center),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(crate::ui::style::style_manager::TB)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color
+ )),
..Default::default()
})
.width(Fill)
@@ -711,132 +655,6 @@ pub(super) fn mtext_editor_overlay<'a>(
)
}
-// ββ Drawing-tab right-click context menu βββββββββββββββββββββββββββββββββββ
-
-/// Right-click menu for a drawing tab. The scope deliberately contains only
-/// the actions approved in issue #493.
-pub(super) fn doc_tab_context_menu_overlay(
- tab_idx: usize,
- current_path: Option<&std::path::Path>,
- has_other_drawings: bool,
- win: (f32, f32),
-) -> Element<'_, Message> {
- const MENU_W: f32 = 210.0;
- const MENU_H: f32 = 136.0;
- const MENU_BG: Color = Color {
- r: 0.17,
- g: 0.17,
- b: 0.17,
- a: 1.0,
- };
- const MENU_BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
- };
- const ITEM_HOVER: Color = Color {
- r: 0.25,
- g: 0.45,
- b: 0.70,
- a: 1.0,
- };
- const TEXT_COLOR: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
- };
- const DISABLED_TEXT: Color = Color {
- r: 0.43,
- g: 0.43,
- b: 0.43,
- a: 1.0,
- };
-
- let item = |label: &'static str, msg: Option| {
- let enabled = msg.is_some();
- let mut item = button(text(label).size(12).color(if enabled {
- TEXT_COLOR
- } else {
- DISABLED_TEXT
- }))
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(if enabled {
- match status {
- button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
- _ => Color::TRANSPARENT,
- }
- } else {
- Color::TRANSPARENT
- })),
- text_color: if enabled { TEXT_COLOR } else { DISABLED_TEXT },
- border: Border::default(),
- shadow: iced::Shadow::default(),
- snap: false,
- })
- .padding([4, 12])
- .width(Fill);
- if let Some(msg) = msg {
- item = item.on_press(msg);
- }
- item
- };
-
- let native_path_actions = cfg!(not(target_arch = "wasm32")) && current_path.is_some();
- let menu = container(
- column![
- item("Save All", Some(Message::DocTabSaveAll)),
- item("Close All", Some(Message::DocTabCloseAll)),
- item(
- "Close All Other Drawings",
- has_other_drawings.then_some(Message::DocTabCloseOthers(tab_idx)),
- ),
- item(
- "Copy Full File Path",
- native_path_actions.then_some(Message::DocTabCopyFullPath(tab_idx)),
- ),
- item(
- "Open File Location",
- native_path_actions.then_some(Message::DocTabOpenFileLocation(tab_idx)),
- ),
- ]
- .spacing(0)
- .width(MENU_W),
- )
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(MENU_BG)),
- border: Border {
- color: MENU_BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- })
- .padding([4, 0])
- .width(iced::Length::Fixed(MENU_W));
-
- let catcher = mouse_area(
- container(Space::new().width(Fill).height(Fill))
- .width(Fill)
- .height(Fill),
- )
- .on_press(Message::DocTabContextMenuClose)
- .on_right_press(Message::DocTabContextMenuClose);
-
- let bounds = crate::ui::wrap_bar::dropdown_bounds(&format!("DOC_TAB:{tab_idx}"));
- let pos = bounds
- .map(|b| {
- iced::Point::new(
- b.x.clamp(0.0, (win.0 - MENU_W).max(0.0)),
- (b.y + b.height).clamp(0.0, (win.1 - MENU_H).max(0.0)),
- )
- })
- .unwrap_or(iced::Point::new(4.0, 30.0));
-
- stack![catcher, position_canvas_overlay(pos, menu.into())].into()
-}
-
// ββ Viewport right-click context menu ββββββββββββββββββββββββββββββββββββββ
pub(super) fn viewport_context_menu_overlay(
@@ -847,50 +665,10 @@ pub(super) fn viewport_context_menu_overlay(
last_cmds: Vec,
draworder_open: bool,
) -> Element<'static, Message> {
- const MENU_BG: Color = Color {
- r: 0.17,
- g: 0.17,
- b: 0.17,
- a: 1.0,
- };
- const MENU_BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
- };
- const ITEM_HOVER: Color = Color {
- r: 0.25,
- g: 0.45,
- b: 0.70,
- a: 1.0,
- };
- const TEXT_COL: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
- };
- const SEP_COL: Color = Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
- };
-
let item = |label: String, msg: Message| -> Element<'static, Message> {
- button(text(label).size(12).color(TEXT_COL))
+ button(text(label).size(12))
.on_press(msg)
- .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,
- })
+ .style(button::subtle)
.padding([4, 12])
.width(Fill)
.into()
@@ -898,8 +676,10 @@ pub(super) fn viewport_context_menu_overlay(
let sep = || -> Element<'static, Message> {
container(iced::widget::Space::new().width(Fill).height(1))
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(SEP_COL)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color,
+ )),
..Default::default()
})
.width(Fill)
@@ -910,18 +690,9 @@ pub(super) fn viewport_context_menu_overlay(
// Indented variant for sub-menu rows (e.g. Draw Order children).
let subitem = |label: String, msg: Message| -> Element<'static, Message> {
- button(text(label).size(12).color(TEXT_COL))
+ button(text(label).size(12))
.on_press(msg)
- .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,
- })
+ .style(button::subtle)
.padding(iced::Padding {
top: 4.0,
right: 12.0,
@@ -964,30 +735,21 @@ pub(super) fn viewport_context_menu_overlay(
));
items.push(sep());
let do_caret = if draworder_open {
- crate::ui::icons::arrow_down(9.0, TEXT_COL)
+ crate::ui::icons::themed_arrow_down(9.0)
} else {
- crate::ui::icons::arrow_right(9.0, TEXT_COL)
+ crate::ui::icons::themed_arrow_right(9.0)
};
items.push(
button(
row![
- text("Draw Order").size(12).color(TEXT_COL),
+ text("Draw Order").size(12),
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,
- })
+ .style(button::subtle)
.padding([4, 12])
.width(Fill)
.into(),
@@ -1046,123 +808,23 @@ pub(super) fn viewport_context_menu_overlay(
let menu_col = column(items).spacing(0).width(iced::Length::Fixed(180.0));
let menu = container(menu_col)
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(MENU_BG)),
- border: Border {
- color: MENU_BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- })
+ .style(container::bordered_box)
.padding([4, 0])
.width(iced::Length::Fixed(180.0));
position_canvas_overlay(pos, menu.into())
}
-/// A small right-click context menu rendered above the status bar.
-/// The `name` is the layout tab that was right-clicked.
-pub(super) fn layout_context_menu_overlay(name: &str, win: (f32, f32)) -> Element<'_, Message> {
- const MENU_BG: Color = Color {
- r: 0.17,
- g: 0.17,
- b: 0.17,
- a: 1.0,
- };
- const MENU_BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
- };
- const ITEM_HOVER: Color = Color {
- r: 0.25,
- g: 0.45,
- b: 0.70,
- a: 1.0,
- };
- const TEXT_COLOR: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
- };
-
- let item = |label: &'static str, msg: Message| {
- button(text(label).size(12).color(TEXT_COLOR))
- .on_press(msg)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
- _ => Color::TRANSPARENT,
- })),
- text_color: TEXT_COLOR,
- border: Border::default(),
- shadow: iced::Shadow::default(),
- snap: false,
- })
- .padding([4, 12])
- .width(Fill)
- };
-
- let rename_name = name.to_string();
- let delete_name = name.to_string();
-
- let menu = container(
- column![
- item("Rename", Message::LayoutRenameStart(rename_name)),
- item("Delete", Message::LayoutDelete(delete_name)),
- ]
- .spacing(0)
- .width(160),
- )
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(MENU_BG)),
- border: Border {
- color: MENU_BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- })
- .padding([4, 0]);
-
- // Click-catcher fills the whole screen to close the menu when clicking outside.
- let catcher = mouse_area(
- container(iced::widget::Space::new().width(Fill).height(Fill))
- .width(Fill)
- .height(Fill),
- )
- .on_press(Message::LayoutContextMenuClose)
- .on_right_press(Message::LayoutContextMenuClose);
-
- // Anchor the menu above the status bar next to the right-clicked tab β
- // its bounds were recorded by the tab's PosReport wrapper (#428). A
- // missing report (shouldn't happen) falls back to the left edge.
- let pill = crate::ui::wrap_bar::dropdown_bounds(&format!("SB_LAYOUT_TAB:{name}"));
- let positioned =
- crate::ui::popup::position_statusbar_popup(menu.into(), pill, win, 160.0, false);
-
- stack![catcher, positioned].into()
-}
-
/// One-shot snap override menu (Shift+RMB, #337): a cursor-anchored grid of
/// snap ICONS only β the names show as hover tooltips. Picking one applies
/// that snap to just the next point pick.
pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Message> {
- const PANEL_BG: Color = Color { r: 0.16, g: 0.16, b: 0.16, a: 0.98 };
- const PANEL_BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
- const ICON_COLOR: Color = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 };
- const HOVER: Color = Color { r: 0.25, g: 0.45, b: 0.70, a: 1.0 };
- const TIP_BG: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 0.98 };
const COLS: usize = 4;
let cell = |snap_type: crate::snap::SnapType, label: &'static str| -> Element<'static, Message> {
- let icon = container(crate::ui::icons::tinted::(
+ let icon = container(crate::ui::icons::themed::(
crate::ui::icons::osnap(snap_type),
16.0,
- ICON_COLOR,
))
.width(26)
.height(26)
@@ -1170,26 +832,34 @@ pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Messag
.align_y(iced::Center);
let btn = button(icon)
.on_press(Message::SnapOverridePick(snap_type))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => HOVER,
- _ => Color::TRANSPARENT,
- })),
+ .style(|theme: &Theme, status| button::Style {
+ background: matches!(
+ status,
+ button::Status::Hovered | button::Status::Pressed
+ )
+ .then_some(Background::Color(
+ theme.extended_palette().primary.weak.color
+ )),
border: Border::default(),
+ text_color: theme.extended_palette().background.base.text,
..Default::default()
})
.padding(2);
iced::widget::tooltip(
btn,
- container(text(label).size(11).color(Color::WHITE))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TIP_BG)),
+ container(text(label).size(11))
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.strong.color)),
border: Border {
- color: PANEL_BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 2.0.into(),
},
+ text_color: Some(palette.background.strong.text),
..Default::default()
+ }
})
.padding([2, 6]),
iced::widget::tooltip::Position::Bottom,
@@ -1207,14 +877,17 @@ pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Messag
}
let panel = container(grid)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: PANEL_BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
+ }
})
.padding(4);
@@ -1247,49 +920,6 @@ pub(super) fn qselect_overlay<'a>(
properties: &[(String, String)],
) -> Element<'a, Message> {
use iced::widget::{checkbox, pick_list};
- const BG: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 0.98,
- };
- const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
- };
- const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
- };
- const BTN_OK: Color = Color {
- r: 0.22,
- g: 0.42,
- b: 0.68,
- a: 1.0,
- };
- const BTN_OK_HOV: Color = Color {
- r: 0.30,
- g: 0.52,
- b: 0.80,
- a: 1.0,
- };
- const BTN_BG: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- };
- const BTN_HOV: Color = Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
- };
-
let mut type_options: Vec = vec![QSELECT_ANY_TYPE.to_string()];
type_options.extend(types.iter().cloned());
@@ -1334,28 +964,33 @@ pub(super) fn qselect_overlay<'a>(
let label = |s: &'static str| {
text(s)
.size(12)
- .color(TEXT)
.width(iced::Length::Fixed(90.0))
};
- let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
- button(text(lbl).size(12).color(TEXT))
+ let btn = |lbl: &'static str, msg: Message, primary: bool| {
+ button(text(lbl).size(12))
.on_press(msg)
- .style(move |_: &Theme, st| button::Style {
- background: Some(Background::Color(
- if matches!(st, button::Status::Hovered | button::Status::Pressed) {
- hov
- } else {
- base
- },
- )),
- text_color: TEXT,
+ .style(move |theme: &Theme, st| {
+ let palette = theme.extended_palette();
+ let pair = match (
+ primary,
+ matches!(st, button::Status::Hovered | button::Status::Pressed),
+ ) {
+ (true, true) => palette.primary.strong,
+ (true, false) => palette.primary.base,
+ (false, true) => palette.background.strong,
+ (false, false) => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
+ }
})
.padding([4, 14])
};
@@ -1366,7 +1001,7 @@ pub(super) fn qselect_overlay<'a>(
}
let panel_body = column![
- text("Quick Select").size(14).color(TEXT),
+ text("Quick Select").size(14),
Space::new().height(10),
row![
label("Object type:"),
@@ -1421,15 +1056,15 @@ pub(super) fn qselect_overlay<'a>(
.on_toggle(Message::QSelectSetAppend)
.size(14),
Space::new().width(6),
- text("Append to current selection").size(12).color(TEXT),
+ text("Append to current selection").size(12),
]
.align_y(iced::Alignment::Center),
Space::new().height(14),
row![
Space::new().width(Fill),
- btn("Cancel", Message::QSelectClose, BTN_BG, BTN_HOV),
+ btn("Cancel", Message::QSelectClose, false),
Space::new().width(8),
- btn("Apply", Message::QSelectApply, BTN_OK, BTN_OK_HOV),
+ btn("Apply", Message::QSelectApply, true),
]
.align_y(iced::Alignment::Center),
]
@@ -1438,14 +1073,17 @@ pub(super) fn qselect_overlay<'a>(
let panel = container(panel_body)
.padding(16)
.width(iced::Length::Fixed(400.0))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
+ }
});
// Outside-click catcher β fills the whole screen, sits below the
diff --git a/src/app/view/viewcube.rs b/src/app/view/viewcube.rs
index 495519fd5..25d0640a7 100644
--- a/src/app/view/viewcube.rs
+++ b/src/app/view/viewcube.rs
@@ -1,10 +1,9 @@
use super::super::Message;
use crate::scene::{VIEWCUBE_PX, VIEWCUBE_REGION_PX};
use iced::widget::{
- button, column, container, mouse_area, pick_list, row, stack,
- Space,
+ button, container, mouse_area, pick_list, stack, Space,
};
-use iced::{Background, Border, Color, Element, Fill, Theme};
+use iced::{Background, Border, Element, Theme};
// ββ Render-mode picker ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -15,15 +14,9 @@ use iced::{Background, Border, Color, Element, Fill, Theme};
/// state and emit `ToggleGrid` / `ToggleGridSnap`.
// ββ ViewCube navigation controls (home / roll / nudge / UCS) βββββββββββββββ
-/// Place `el` at pixel offset (x, y) inside a Fill layer (top-left origin).
+/// Place `el` at pixel offset (x, y) inside a fill layer (top-left origin).
fn vc_place<'a>(x: f32, y: f32, el: Element<'a, Message>) -> Element<'a, Message> {
- column![
- Space::new().height(iced::Length::Fixed(y.max(0.0))),
- row![Space::new().width(iced::Length::Fixed(x.max(0.0))), el],
- ]
- .width(Fill)
- .height(Fill)
- .into()
+ crate::ui::pin_at(iced::Point::new(x, y), el)
}
/// Borderless square icon button used by the ViewCube nav controls.
@@ -37,20 +30,19 @@ fn vc_btn<'a>(content: Element<'a, Message>, size: f32, msg: Message) -> Element
)
.padding(0)
.on_press(msg)
- .style(|_: &Theme, status| iced::widget::button::Style {
- background: Some(Background::Color(match status {
- iced::widget::button::Status::Hovered | iced::widget::button::Status::Pressed => Color {
- r: 0.45,
- g: 0.62,
- b: 0.95,
- a: 0.30,
- },
- _ => Color::TRANSPARENT,
- })),
+ .style(|theme: &Theme, status| iced::widget::button::Style {
+ background: matches!(
+ status,
+ iced::widget::button::Status::Hovered | iced::widget::button::Status::Pressed
+ )
+ .then_some(Background::Color(
+ theme.extended_palette().primary.weak.color
+ )),
border: Border {
radius: 3.0.into(),
..Default::default()
},
+ text_color: theme.extended_palette().background.base.text,
..Default::default()
})
.into()
@@ -61,12 +53,6 @@ fn vc_btn<'a>(content: Element<'a, Message>, size: f32, msg: Message) -> Element
pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
use crate::scene::NudgeDir;
use crate::ui::icons;
- let tint = Color {
- r: 0.86,
- g: 0.89,
- b: 0.96,
- a: 1.0,
- };
let r = VIEWCUBE_REGION_PX;
let c = r * 0.5;
let cube_half = VIEWCUBE_PX as f32 * 0.36; // VIEWCUBE_PX * VIEWCUBE_SCALE
@@ -101,23 +87,23 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
vc_place(
3.0,
3.0,
- vc_btn(icons::home(13.0, tint), BTN, Message::ViewCubeHome)
+ vc_btn(icons::themed_home(13.0), BTN, Message::ViewCubeHome)
),
vc_place(
rax,
ray,
- vc_btn(icons::undo(12.0, tint), BTN, Message::ViewCubeRoll(false))
+ vc_btn(icons::themed_undo(12.0, true), BTN, Message::ViewCubeRoll(false))
),
vc_place(
rbx,
rby,
- vc_btn(icons::redo(12.0, tint), BTN, Message::ViewCubeRoll(true))
+ vc_btn(icons::themed_redo(12.0, true), BTN, Message::ViewCubeRoll(true))
),
vc_place(
tux,
tuy,
vc_btn(
- icons::arrow_down(8.0, tint),
+ icons::themed_arrow_down(8.0),
TRI,
Message::ViewCubeNudge(NudgeDir::Up)
)
@@ -126,7 +112,7 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
tdx,
tdy,
vc_btn(
- icons::arrow_up(8.0, tint),
+ icons::themed_arrow_up(8.0),
TRI,
Message::ViewCubeNudge(NudgeDir::Down)
)
@@ -135,7 +121,7 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
tlx,
tly,
vc_btn(
- icons::arrow_right(8.0, tint),
+ icons::themed_arrow_right(8.0),
TRI,
Message::ViewCubeNudge(NudgeDir::Left)
)
@@ -144,7 +130,7 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
trx,
try_,
vc_btn(
- icons::arrow_left(8.0, tint),
+ icons::themed_arrow_left(8.0),
TRI,
Message::ViewCubeNudge(NudgeDir::Right)
)
@@ -162,12 +148,6 @@ pub(super) const UCS_PICKER_W: f32 = 84.0;
/// The WCS / named-UCS selector shown under the cube.
pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec) -> Element<'a, Message> {
- let light = Color {
- r: 0.85,
- g: 0.87,
- b: 0.93,
- a: 1.0,
- };
let mut options = vec!["WCS".to_string()];
options.extend(names);
let selected = if current.is_empty() {
@@ -181,21 +161,20 @@ pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec) -> El
// Fixed width so the caller can centre it under the cube centre with a
// simple half-width offset (content-sized width would drift off-centre).
.width(iced::Length::Fixed(UCS_PICKER_W))
- .style(move |_: &Theme, _| iced::widget::pick_list::Style {
- background: Background::Color(Color {
- r: 0.16,
- g: 0.17,
- b: 0.20,
- a: 0.92,
- }),
+ .style(move |theme: &Theme, _| {
+ let palette = theme.extended_palette();
+ iced::widget::pick_list::Style {
+ background: Background::Color(palette.background.weak.color),
border: Border {
radius: 3.0.into(),
+ color: palette.background.neutral.color,
+ width: 1.0,
..Default::default()
},
- text_color: light,
- placeholder_color: light,
- handle_color: light,
+ text_color: palette.background.base.text,
+ placeholder_color: palette.background.base.text.scale_alpha(0.68),
+ handle_color: palette.background.base.text,
+ }
})
.into()
}
-
diff --git a/src/discussions.rs b/src/discussions.rs
new file mode 100644
index 000000000..43b733122
--- /dev/null
+++ b/src/discussions.rs
@@ -0,0 +1,240 @@
+//! GitHub Discussions shown on the Start page.
+//!
+//! Native builds read GitHub's public Atom feed and the public Discussions
+//! page, which exposes the repository's pinned-discussion section without an
+//! access token. The last successful result is cached for offline launches.
+//! Web builds read `discussions.json`, generated by the Pages workflow through
+//! GitHub's authenticated GraphQL API.
+
+pub const DISCUSSIONS_URL: &str =
+ "https://github.com/HakanSeven12/OpenCADStudio/discussions";
+
+#[cfg(not(target_arch = "wasm32"))]
+const FEED_URL: &str =
+ "https://github.com/HakanSeven12/OpenCADStudio/discussions.atom";
+
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
+pub struct DiscussionEntry {
+ pub number: u64,
+ pub title: String,
+ pub url: String,
+ pub author: String,
+ pub updated_at: String,
+ #[serde(default)]
+ pub pinned: bool,
+}
+
+fn sort_entries(entries: &mut [DiscussionEntry]) {
+ entries.sort_by(|a, b| {
+ b.pinned
+ .cmp(&a.pinned)
+ .then_with(|| b.updated_at.cmp(&a.updated_at))
+ .then_with(|| b.number.cmp(&a.number))
+ });
+}
+
+fn parse_json(body: &str) -> Result, String> {
+ let mut entries: Vec =
+ serde_json::from_str(body).map_err(|e| e.to_string())?;
+ entries.retain(|entry| {
+ entry.number > 0
+ && !entry.title.trim().is_empty()
+ && entry.url.starts_with("https://github.com/")
+ });
+ sort_entries(&mut entries);
+ Ok(entries)
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn cache_path() -> Option {
+ crate::config::config_dir().map(|dir| dir.join("discussions.json"))
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn load_cached() -> Vec {
+ cache_path()
+ .and_then(|path| std::fs::read_to_string(path).ok())
+ .and_then(|body| parse_json(&body).ok())
+ .or_else(|| parse_json(include_str!("../web/discussions.json")).ok())
+ .unwrap_or_default()
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn fetch_discussions() -> Result, String> {
+ let agent: ureq::Agent = ureq::Agent::config_builder()
+ .timeout_global(Some(std::time::Duration::from_secs(15)))
+ .build()
+ .into();
+ let feed = get_text(&agent, FEED_URL)?;
+ let page = get_text(&agent, DISCUSSIONS_URL)?;
+ let pinned = pinned_numbers(&page);
+ let mut entries = parse_atom(&feed);
+ if entries.is_empty() {
+ return Err("GitHub Discussions feed returned no entries".into());
+ }
+ for entry in &mut entries {
+ entry.pinned = pinned.contains(&entry.number);
+ }
+ sort_entries(&mut entries);
+
+ if let Some(path) = cache_path() {
+ if let Some(parent) = path.parent() {
+ let _ = std::fs::create_dir_all(parent);
+ }
+ if let Ok(body) = serde_json::to_string(&entries) {
+ let _ = std::fs::write(path, body);
+ }
+ }
+ Ok(entries)
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn get_text(agent: &ureq::Agent, url: &str) -> Result {
+ agent
+ .get(url)
+ .header(
+ "User-Agent",
+ concat!("OpenCADStudio/", env!("CARGO_PKG_VERSION")),
+ )
+ .call()
+ .map_err(|e| e.to_string())?
+ .body_mut()
+ .with_config()
+ .limit(4 * 1024 * 1024)
+ .read_to_string()
+ .map_err(|e| e.to_string())
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn parse_atom(feed: &str) -> Vec {
+ feed.split("")
+ .skip(1)
+ .filter_map(|tail| {
+ let entry = tail.split_once("")?.0;
+ let url_start = entry.find(
+ "href=\"https://github.com/HakanSeven12/OpenCADStudio/discussions/",
+ )? + "href=\"".len();
+ let url_tail = &entry[url_start..];
+ let url = url_tail.split_once('"')?.0.to_string();
+ let number = url.rsplit('/').next()?.parse().ok()?;
+ let title = xml_text(entry, "title")?;
+ let author_block = entry.split_once("")?.1.split_once("")?.0;
+ let author = xml_text(author_block, "name").unwrap_or_default();
+ let updated_at = xml_text(entry, "updated").unwrap_or_default();
+ Some(DiscussionEntry {
+ number,
+ title,
+ url,
+ author,
+ updated_at,
+ pinned: false,
+ })
+ })
+ .collect()
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn xml_text(block: &str, tag: &str) -> Option {
+ let open = format!("<{tag}>");
+ let close = format!("{tag}>");
+ let value = block.split_once(&open)?.1.split_once(&close)?.0;
+ let compact = value.split_whitespace().collect::>().join(" ");
+ if compact.is_empty() {
+ None
+ } else {
+ Some(
+ compact
+ .replace(""", "\"")
+ .replace("'", "'")
+ .replace("'", "'")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("&", "&"),
+ )
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn pinned_numbers(page: &str) -> std::collections::HashSet {
+ let mut numbers = std::collections::HashSet::new();
+ let Some(after_heading) = page.split_once("id=\"pinned-discussions\"").map(|(_, rest)| rest)
+ else {
+ return numbers;
+ };
+ let section = after_heading
+ .split_once("")
+ .map(|(section, _)| section)
+ .unwrap_or(after_heading);
+ let marker = "/HakanSeven12/OpenCADStudio/discussions/";
+ let mut rest = section;
+ while let Some(pos) = rest.find(marker) {
+ let tail = &rest[pos + marker.len()..];
+ let digits = tail
+ .chars()
+ .take_while(|ch| ch.is_ascii_digit())
+ .collect::();
+ if let Ok(number) = digits.parse() {
+ numbers.insert(number);
+ }
+ rest = tail;
+ }
+ numbers
+}
+
+#[cfg(target_arch = "wasm32")]
+pub async fn fetch_discussions_web() -> Result, String> {
+ use wasm_bindgen::JsCast;
+ use wasm_bindgen_futures::JsFuture;
+
+ let window = web_sys::window().ok_or("no window")?;
+ let response = JsFuture::from(window.fetch_with_str("discussions.json"))
+ .await
+ .map_err(|_| "fetch failed")?;
+ let response: web_sys::Response =
+ response.dyn_into().map_err(|_| "not a Response")?;
+ if !response.ok() {
+ return Err(format!("HTTP {}", response.status()));
+ }
+ let text = JsFuture::from(response.text().map_err(|_| "text() unavailable")?)
+ .await
+ .map_err(|_| "body read failed")?;
+ let body = text.as_string().ok_or("body is not a string")?;
+ parse_json(&body)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[cfg(not(target_arch = "wasm32"))]
+ #[test]
+ fn pinned_discussions_are_sorted_first() {
+ let feed = r#"
+
+
+ New & recent
+ 2026-07-28T10:00:00+00:00
+ alice
+
+
+
+ Pinned
+ 2026-07-20T10:00:00+00:00
+ bob
+
+ "#;
+ let page = r#"
+ Pinned Discussions
+
+ "#;
+ let pinned = pinned_numbers(page);
+ let mut entries = parse_atom(feed);
+ for entry in &mut entries {
+ entry.pinned = pinned.contains(&entry.number);
+ }
+ sort_entries(&mut entries);
+
+ assert_eq!(entries[0].number, 1);
+ assert_eq!(entries[1].title, "New & recent");
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 64369e432..ea1711c07 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,6 +9,7 @@ pub mod entities;
pub mod io;
pub mod modules;
pub mod patreon;
+pub mod discussions;
pub mod videos;
pub mod plugin;
pub mod perf;
diff --git a/src/main.rs b/src/main.rs
index 783e5109b..c92a56c8f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,6 +13,7 @@ mod entities;
mod io;
mod modules;
mod patreon;
+mod discussions;
mod videos;
mod plugin;
mod perf;
diff --git a/src/scene/pipeline/viewcube.rs b/src/scene/pipeline/viewcube.rs
index 2105ea23f..74f3d0b83 100644
--- a/src/scene/pipeline/viewcube.rs
+++ b/src/scene/pipeline/viewcube.rs
@@ -576,6 +576,7 @@ impl ViewCubeText {
vp_w: u32,
vp_h: u32,
cube_px: u32,
+ text_color: [f32; 4],
) {
let (vw, vh) = (vp_w as f32, vp_h as f32);
let cube_half = cube_px as f32 * VIEWCUBE_SCALE;
@@ -637,7 +638,12 @@ impl ViewCubeText {
continue;
}
let alpha = ((dot - 0.12) / 0.88).clamp(0.0, 1.0);
- let color = [1.0, 1.0, 1.0, alpha];
+ let color = [
+ text_color[0],
+ text_color[1],
+ text_color[2],
+ text_color[3] * alpha,
+ ];
let u = Vec3::from(FACE_U[fi]);
let v = Vec3::from(FACE_V[fi]);
let center = face_n; // unit normal = face surface centre (distance E)
@@ -714,7 +720,12 @@ impl ViewCubeText {
} else {
0.5
};
- let color = [1.0, 1.0, 1.0, alpha];
+ let color = [
+ text_color[0],
+ text_color[1],
+ text_color[2],
+ text_color[3] * alpha,
+ ];
let (u0, v0, u1, v1) = glyph_uv(gi, self.atlas_w, self.atlas_h);
let corner = |lx: f32, ly: f32| center + Vec3::X * lx + Vec3::Y * ly;
let tl = project_world(corner(-CARD_GW * 0.5, CARD_GH * 0.5));
@@ -1232,6 +1243,7 @@ impl ViewCubePipeline {
vp_w: u32,
vp_h: u32,
hover: Option,
+ text_color: [f32; 4],
) {
queue.write_buffer(
&self.uniform_buffer,
@@ -1245,7 +1257,15 @@ impl ViewCubePipeline {
)),
);
self.text
- .update(queue, cam_rotation, compass_rotation, vp_w, vp_h, self.cube_px);
+ .update(
+ queue,
+ cam_rotation,
+ compass_rotation,
+ vp_w,
+ vp_h,
+ self.cube_px,
+ text_color,
+ );
}
pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size) {
diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs
index f1266acae..af93772bf 100644
--- a/src/scene/view/render.rs
+++ b/src/scene/view/render.rs
@@ -152,6 +152,8 @@ pub struct Primitive {
pub(in crate::scene) viewports: Vec,
/// Background color used to clear each viewport's MSAA buffer.
pub(in crate::scene) bg_color: [f32; 4],
+ /// Active Iced theme text colour for GPU-rendered ViewCube labels.
+ pub(in crate::scene) viewcube_text_color: [f32; 4],
/// One input-to-render sample, carried only when PERF tracing is enabled.
pub(in crate::scene) nav_perf: Option,
}
@@ -327,6 +329,7 @@ impl shader::Primitive for Primitive {
(vp.screen_rect.width * bounds.width) as u32,
(vp.screen_rect.height * bounds.height) as u32,
vp.hover_region,
+ self.viewcube_text_color,
);
}
continue;
@@ -874,6 +877,7 @@ impl shader::Primitive for Primitive {
(vp.screen_rect.width * bounds.width) as u32,
(vp.screen_rect.height * bounds.height) as u32,
vp.hover_region,
+ self.viewcube_text_color,
);
}
}
@@ -1689,6 +1693,7 @@ impl Scene {
model_render_mode: acadrust::entities::ViewportRenderMode,
_hover_region: Option,
show_viewcube: bool,
+ viewcube_text_color: [f32; 4],
) -> Primitive {
let nav_build_started = iced::time::Instant::now();
let perf_nav = self.take_nav_perf();
@@ -1720,6 +1725,7 @@ impl Scene {
Primitive {
viewports,
bg_color,
+ viewcube_text_color,
nav_perf: perf_nav,
}
}
@@ -1736,6 +1742,7 @@ impl Scene {
tile_idx: usize,
model_render_mode: acadrust::entities::ViewportRenderMode,
show_viewcube: bool,
+ viewcube_text_color: [f32; 4],
) -> Primitive {
let hover_region = self.viewcube_hover.get();
let canvas = (bounds.width.max(1.0), bounds.height.max(1.0));
@@ -1745,6 +1752,7 @@ impl Scene {
return Primitive {
viewports: vec![],
bg_color,
+ viewcube_text_color,
nav_perf: None,
};
};
@@ -1792,6 +1800,7 @@ impl Scene {
Primitive {
viewports,
bg_color,
+ viewcube_text_color,
nav_perf: perf_nav,
}
}
diff --git a/src/scene/view/viewport_pane.rs b/src/scene/view/viewport_pane.rs
index a3cf1a044..07d87b868 100644
--- a/src/scene/view/viewport_pane.rs
+++ b/src/scene/view/viewport_pane.rs
@@ -23,6 +23,9 @@ pub struct ViewportPane<'a> {
/// per `pane_grid` pane, filling its own bounds). `None` β the unified
/// full-canvas path (paper layout, or the whole-canvas Model fallback).
pub pane: Option,
+ /// Active Iced theme text colour, forwarded to the GPU-rendered ViewCube
+ /// face and compass labels.
+ pub viewcube_text_color: [f32; 4],
}
impl<'a> ViewportPane<'a> {
@@ -30,12 +33,14 @@ impl<'a> ViewportPane<'a> {
scene: &'a Scene,
show_viewcube: bool,
render_mode: acadrust::entities::ViewportRenderMode,
+ viewcube_text_color: [f32; 4],
) -> Self {
Self {
scene,
show_viewcube,
render_mode,
pane: None,
+ viewcube_text_color,
}
}
@@ -46,12 +51,14 @@ impl<'a> ViewportPane<'a> {
show_viewcube: bool,
render_mode: acadrust::entities::ViewportRenderMode,
tile_idx: usize,
+ viewcube_text_color: [f32; 4],
) -> Self {
Self {
scene,
show_viewcube,
render_mode,
pane: Some(tile_idx),
+ viewcube_text_color,
}
}
}
@@ -74,12 +81,14 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program for ViewportPane<'a>
idx,
self.render_mode,
self.show_viewcube,
+ self.viewcube_text_color,
),
None => self.scene.build_viewports(
bounds,
self.render_mode,
state.hover_region,
self.show_viewcube,
+ self.viewcube_text_color,
),
}
}
diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs
index 08edb5c60..bb2af84be 100644
--- a/src/ui/color_select.rs
+++ b/src/ui/color_select.rs
@@ -20,25 +20,6 @@ pub struct ColorExtras {
pub by_block: bool,
}
-const PICKER_BG: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
-const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
-};
-
/// Encode a colour as the ACI integer string the style editors store
/// (ByBlock=0, ByLayer=256, indexed 1-255; RGB has no ACI slot β ByLayer).
pub fn color_to_aci_string(c: AcadColor) -> String {
@@ -85,15 +66,10 @@ pub fn color_display_name(c: AcadColor) -> String {
/// A small colour square.
fn swatch<'a>(bg: Color) -> Element<'a, Message> {
container(text("").width(13).height(13))
- .style(move |_: &Theme| container::Style {
+ .style(move |theme: &Theme| container::Style {
background: Some(Background::Color(bg)),
border: Border {
- color: Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.5,
- },
+ color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 2.0.into(),
},
@@ -126,8 +102,8 @@ pub fn color_selector<'a>(
let head = button(
row![
swatch(cur_bg),
- text(cur_name).size(11).color(TEXT),
- crate::ui::icons::arrow_toggle(open, 9.0, TEXT),
+ text(cur_name).size(11),
+ crate::ui::icons::themed_arrow_toggle(open, 9.0),
]
.spacing(5)
.align_y(iced::Center),
@@ -141,14 +117,17 @@ pub fn color_selector<'a>(
}
let popup = container(color_list(extras, on_select, on_more))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PICKER_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
+ }
})
.padding(5)
.width(220);
@@ -161,6 +140,21 @@ pub fn color_selector<'a>(
})
}
+fn list_row_style(theme: &Theme, status: button::Status) -> button::Style {
+ let palette = theme.extended_palette();
+ let hovered = matches!(status, button::Status::Hovered);
+ let text_color = if hovered {
+ palette.background.strong.text
+ } else {
+ palette.background.base.text
+ };
+ button::Style {
+ background: hovered.then_some(Background::Color(palette.background.strong.color)),
+ text_color,
+ ..Default::default()
+ }
+}
+
/// The colour list shown inside a picker popup: named ACI colours (with
/// swatches) plus a "Moreβ¦" entry that opens the full palette window. Shared by
/// `color_selector` and the ribbon's colour overlay.
@@ -172,21 +166,12 @@ pub fn color_list<'a>(
let named_row = |color: AcadColor| -> Element<'a, Message> {
let (bg, name) = acad_color_display(color);
button(
- row![swatch(bg), text(name).size(11).color(TEXT)]
+ row![swatch(bg), text(name).size(11)]
.spacing(5)
.align_y(iced::Center),
)
.on_press(on_select(color))
- .style(|_: &Theme, status| button::Style {
- background: matches!(status, button::Status::Hovered)
- .then_some(Background::Color(Color {
- r: 0.25,
- g: 0.25,
- b: 0.30,
- a: 1.0,
- })),
- ..Default::default()
- })
+ .style(list_row_style)
.padding([2, 4])
.width(Length::Fill)
.into()
@@ -203,18 +188,9 @@ pub fn color_list<'a>(
list = list.push(named_row(AcadColor::Index(i)));
}
list = list.push(
- button(text("Moreβ¦").size(11).color(TEXT))
+ button(text("Moreβ¦").size(11))
.on_press(on_more)
- .style(|_: &Theme, status| button::Style {
- background: matches!(status, button::Status::Hovered)
- .then_some(Background::Color(Color {
- r: 0.25,
- g: 0.25,
- b: 0.30,
- a: 1.0,
- })),
- ..Default::default()
- })
+ .style(list_row_style)
.padding([2, 4])
.width(Length::Fill),
);
@@ -227,7 +203,7 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta
let chip = |color: AcadColor, label: &'static str| -> Element<'static, Message> {
let (bg, _) = acad_color_display(color);
button(
- row![swatch(bg), text(label).size(11).color(TEXT)]
+ row![swatch(bg), text(label).size(11)]
.spacing(5)
.align_y(iced::Center),
)
@@ -250,18 +226,13 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta
r = r.push(
button(text("").width(18).height(18))
.on_press(on_pick(AcadColor::Index(ci)))
- .style(move |_: &Theme, status| button::Style {
+ .style(move |theme: &Theme, status| button::Style {
background: Some(Background::Color(bg)),
border: Border {
color: if matches!(status, button::Status::Hovered) {
- Color::WHITE
+ theme.extended_palette().primary.base.color
} else {
- Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.4,
- }
+ theme.extended_palette().background.neutral.color
},
width: if matches!(status, button::Status::Hovered) {
1.5
@@ -270,6 +241,7 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta
},
radius: 1.0.into(),
},
+ text_color: theme.extended_palette().background.base.text,
..Default::default()
})
.padding(0),
@@ -281,14 +253,16 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta
container(
column![
- text("Select Color").size(13).color(TEXT),
+ text("Select Color").size(13),
row![chip(AcadColor::ByLayer, "ByLayer"), chip(AcadColor::ByBlock, "ByBlock")].spacing(6),
scrollable(grid).height(Length::Fill),
]
.spacing(8),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PICKER_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color
+ )),
..Default::default()
})
.padding(10)
diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs
index 24a0b05e7..085d33de6 100644
--- a/src/ui/command_line.rs
+++ b/src/ui/command_line.rs
@@ -370,11 +370,14 @@ impl CommandLine {
let history_rows = visible[start..]
.iter()
.fold(column![].spacing(0), |col, entry| {
- let color = match entry.kind {
- EntryKind::Command => CMD_COLOR,
- EntryKind::Output => OUT_COLOR,
- EntryKind::Error => ERR_COLOR,
- EntryKind::Info => INFO_COLOR,
+ let kind = entry.kind.clone();
+ let entry_text = |value: String| {
+ text(value).size(11).style({
+ let kind = kind.clone();
+ move |theme: &Theme| iced::widget::text::Style {
+ color: Some(history_color(theme, &kind)),
+ }
+ })
};
// The current step's prompt is the single pinned line. When the
// step offers options, render them as clickable buttons inline
@@ -384,31 +387,28 @@ impl CommandLine {
// listing is dropped here (the history log keeps the full text).
if entry.pinned && !self.step_options.is_empty() {
let shown = strip_option_listing(&entry.text);
- let mut r = row![text(shown).size(11).color(color)]
+ let mut r = row![entry_text(shown)]
.spacing(6)
.align_y(iced::Center);
for opt in &self.step_options {
- let btn = button(
- text(opt.label.to_uppercase()).size(11).color(CMD_COLOR),
- )
+ let btn = button(text(opt.label.to_uppercase()).size(11))
.on_press(Message::CommandOptionPick(opt.keyword.clone()))
.padding([1, 6])
- .style(|_: &Theme, status| {
- let bg = if matches!(status, button::Status::Hovered) {
- Color {
- r: 0.28,
- g: 0.40,
- b: 0.56,
- a: 1.0,
- }
+ .style(|theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = if matches!(
+ status,
+ button::Status::Hovered | button::Status::Pressed
+ ) {
+ palette.primary.weak
} else {
- INPUT_ROW_BG
+ palette.background.weakest
};
button::Style {
- background: Some(Background::Color(bg)),
- text_color: Color::WHITE,
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
@@ -419,46 +419,41 @@ impl CommandLine {
}
col.push(container(r).padding([1, 8]))
} else {
- col.push(container(text(&entry.text).size(11).color(color)).padding([1, 8]))
+ col.push(container(entry_text(entry.text.clone())).padding([1, 8]))
}
});
- let prompt = container(text("Command:").size(11).color(PROMPT_COLOR)).padding([5, 8]);
+ let prompt = container(
+ text("Command:").size(11).style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(theme.extended_palette().success.base.color),
+ }),
+ )
+ .padding([5, 8]);
// Literal-space toggle: while active, every line behaves as if it
// started with `>` β Space stays in the line instead of submitting, so
// arguments with spaces (text strings, paths, `UCS Z 90` as one line)
// can be typed. Persists until toggled off; a hand-typed leading `>`
// lights the button too (same mode, one line only).
let literal_active = self.literal_spaces || self.input.starts_with('>');
- let literal_btn = button(text(">").size(11).color(if literal_active {
- Color::WHITE
- } else {
- PROMPT_COLOR
- }))
+ let literal_btn = button(text(">").size(11))
.on_press(Message::CommandLiteralToggle)
.padding([2, 6])
- .style(move |_: &Theme, status| {
- let bg = if literal_active {
- Color {
- r: 0.28,
- g: 0.40,
- b: 0.56,
- a: 1.0,
- }
- } else if matches!(status, button::Status::Hovered) {
- Color {
- r: 0.22,
- g: 0.30,
- b: 0.42,
- a: 1.0,
- }
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = if literal_active {
+ palette.primary.weak
+ } else if matches!(
+ status,
+ button::Status::Hovered | button::Status::Pressed
+ ) {
+ palette.background.weak
} else {
- INPUT_ROW_BG
+ palette.background.weakest
};
button::Style {
- background: Some(Background::Color(bg)),
- text_color: Color::WHITE,
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
@@ -467,19 +462,10 @@ impl CommandLine {
});
let literal_tip = container(
text("Literal spaces: Space stays in the line instead of running the command (same as typing a leading '>'). Stays on until toggled off.")
- .size(11)
- .color(Color::WHITE),
+ .size(11),
)
.padding([3, 6])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: BORDER_COLOR,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- });
+ .style(container::bordered_box);
let literal_btn = tooltip(literal_btn, literal_tip, tooltip::Position::Top).gap(4);
// While dynamic input is capturing keystrokes, the command-line
// text field is left without an `on_input` handler so it can't
@@ -492,33 +478,6 @@ impl CommandLine {
.on_submit(Message::CommandSubmit);
}
let input = input
- .style(|_: &Theme, _| text_input::Style {
- background: Background::Color(INPUT_BG),
- border: Border {
- color: Color {
- r: 0.40,
- g: 0.60,
- b: 0.90,
- a: 1.0,
- },
- width: 1.0,
- radius: 2.0.into(),
- },
- icon: Color::WHITE,
- placeholder: Color {
- r: 0.4,
- g: 0.4,
- b: 0.4,
- a: 1.0,
- },
- value: Color::WHITE,
- selection: Color {
- r: 0.20,
- g: 0.44,
- b: 0.72,
- a: 0.5,
- },
- })
.size(11)
.padding([4, 6]);
// Autocomplete suggestions panel, shown above the input row
@@ -537,31 +496,25 @@ impl CommandLine {
let mut col = column![].spacing(0).width(Length::Fill);
for (idx, cmd) in matches.iter().enumerate() {
let is_selected = cursor == idx;
- let row = button(text(cmd.clone()).size(11).color(CMD_COLOR))
+ let row = button(text(cmd.clone()).size(11))
.on_press(Message::CommandSuggestionPick(cmd.clone()))
.width(Length::Fill)
.padding([2, 8])
- .style(move |_: &Theme, status| {
- let bg = if is_selected {
- Color {
- r: 0.28,
- g: 0.40,
- b: 0.56,
- a: 1.0,
- }
- } else if matches!(status, button::Status::Hovered) {
- Color {
- r: 0.22,
- g: 0.30,
- b: 0.42,
- a: 1.0,
- }
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = if is_selected {
+ palette.primary.weak
+ } else if matches!(
+ status,
+ button::Status::Hovered | button::Status::Pressed
+ ) {
+ palette.background.weak
} else {
- PANEL_BG
+ palette.background.base
};
button::Style {
- background: Some(Background::Color(bg)),
- text_color: Color::WHITE,
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border::default(),
..Default::default()
}
@@ -569,15 +522,7 @@ impl CommandLine {
col = col.push(row);
}
container(col)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: BORDER_COLOR,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- })
+ .style(container::bordered_box)
.width(Length::Fill)
.into()
}
@@ -590,22 +535,13 @@ impl CommandLine {
// user can recover anything that has already faded off the
// overlay.
let dropdown_icon = if self.history_open {
- crate::ui::icons::arrow_down(11.0, PROMPT_COLOR)
+ crate::ui::icons::themed_arrow_down(11.0)
} else {
- crate::ui::icons::arrow_right(11.0, PROMPT_COLOR)
+ crate::ui::icons::themed_arrow_right(11.0)
};
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,
- border: Border {
- color: BORDER_COLOR,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(button::subtle)
.padding([2, 6]);
let input_row = row![prompt, literal_btn, input, dropdown_btn]
.spacing(4)
@@ -625,23 +561,21 @@ impl CommandLine {
.size(11)
.padding([2, 8])
.max_height(180.0)
- .style(|_: &Theme, _status| text_editor::Style {
- background: Background::Color(PANEL_BG),
- border: Border::default(),
- placeholder: OUT_COLOR,
- value: CMD_COLOR,
- selection: Color {
- r: 0.20,
- g: 0.44,
- b: 0.72,
- a: 0.5,
- },
+ .style(|theme: &Theme, _status| {
+ let palette = theme.extended_palette();
+ text_editor::Style {
+ background: Background::Color(palette.background.base.color),
+ border: Border::default(),
+ placeholder: palette.background.base.text.scale_alpha(0.72),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
+ }
});
// Header strip: a Copy-all and a Clear button pinned above the log.
let copy_btn = button(
row![
- crate::ui::icons::tinted(crate::ui::icons::COPY, 11.0, PROMPT_COLOR),
- text("Copy").size(11).color(CMD_COLOR),
+ crate::ui::icons::themed_success(crate::ui::icons::COPY, 11.0),
+ text("Copy").size(11),
]
.spacing(4)
.align_y(iced::Center),
@@ -651,8 +585,8 @@ impl CommandLine {
.padding([2, 6]);
let clear_btn = button(
row![
- crate::ui::icons::tinted(crate::ui::icons::TRASH, 11.0, ERR_COLOR),
- text("Clear").size(11).color(CMD_COLOR),
+ crate::ui::icons::themed_warning(crate::ui::icons::TRASH, 11.0),
+ text("Clear").size(11),
]
.spacing(4)
.align_y(iced::Center),
@@ -668,15 +602,7 @@ impl CommandLine {
.width(Length::Fill)
.padding([2, 6]);
let panel = container(column![header, log])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: BORDER_COLOR,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- })
+ .style(container::bordered_box)
.width(Length::Fill)
.padding([4, 0]);
opaque(panel).into()
@@ -688,35 +614,43 @@ impl CommandLine {
autocomplete,
dropdown,
container(history_rows)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(HISTORY_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.width(Length::Fill)
.padding([2, 0]),
container(input_row)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(INPUT_ROW_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weakest.color)),
border: Border {
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
+ }
})
.width(Length::Fill)
// Match the drawing tab bar / status bar height, and vertically
// centre the prompt/input/dropdown within it (issue #216).
.center_y(Length::Fixed(30.0)),
])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
+ }
})
.width(Length::Fixed(720.0))
.into()
@@ -780,22 +714,18 @@ pub fn ranked_matches(
/// Flat button style for the history dropdown's Copy / Clear strip: a subtle
/// filled pill that brightens on hover.
-fn header_btn_style(_: &Theme, status: button::Status) -> button::Style {
- let bg = if matches!(status, button::Status::Hovered) {
- Color {
- r: 0.24,
- g: 0.24,
- b: 0.24,
- a: 1.0,
- }
+fn header_btn_style(theme: &Theme, status: button::Status) -> button::Style {
+ let palette = theme.extended_palette();
+ let pair = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
+ palette.background.weak
} else {
- INPUT_ROW_BG
+ palette.background.weakest
};
button::Style {
- background: Some(Background::Color(bg)),
- text_color: Color::WHITE,
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
@@ -803,66 +733,15 @@ fn header_btn_style(_: &Theme, status: button::Status) -> button::Style {
}
}
-const PANEL_BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const HISTORY_BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const INPUT_ROW_BG: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.18,
- a: 1.0,
-};
-const INPUT_BG: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
-const BORDER_COLOR: Color = Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
-};
-const PROMPT_COLOR: Color = Color {
- r: 0.55,
- g: 0.78,
- b: 0.55,
- a: 1.0,
-};
-const CMD_COLOR: Color = Color {
- r: 0.80,
- g: 0.80,
- b: 0.80,
- a: 1.0,
-};
-const OUT_COLOR: Color = Color {
- r: 0.65,
- g: 0.65,
- b: 0.65,
- a: 1.0,
-};
-const ERR_COLOR: Color = Color {
- r: 0.90,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const INFO_COLOR: Color = Color {
- r: 0.50,
- g: 0.70,
- b: 0.90,
- a: 1.0,
-};
+fn history_color(theme: &Theme, kind: &EntryKind) -> Color {
+ let palette = theme.extended_palette();
+ match kind {
+ EntryKind::Command => palette.background.base.text,
+ EntryKind::Output => palette.background.base.text.scale_alpha(0.72),
+ EntryKind::Error => palette.danger.base.color,
+ EntryKind::Info => palette.primary.base.color,
+ }
+}
#[cfg(test)]
mod tests {
diff --git a/src/ui/icons.rs b/src/ui/icons.rs
index 61c1b7ed0..50aa23db6 100644
--- a/src/ui/icons.rs
+++ b/src/ui/icons.rs
@@ -1,4 +1,4 @@
-//! Shared monochrome UI-chrome icons rendered from bundled SVGs.
+//! Shared SVG rendering for monochrome UI chrome and multi-colour tool icons.
//!
//! Dropdown carets and the undo/redo controls used to be drawn as Unicode
//! glyphs (`βΎ`, `β²`, `βΆ`, `β·`). Those depend on the active text font carrying
@@ -6,8 +6,18 @@
//! 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 std::cell::RefCell;
+
+use iced::advanced::layout::{self, Layout};
+use iced::advanced::renderer;
+use iced::advanced::svg::{self as core_svg, Renderer as _};
+use iced::advanced::widget::{Tree, Widget};
use iced::widget::{container, svg, Space};
-use iced::{Color, Element, Length, Theme};
+use iced::{
+ Color, ContentFit, Element, Length, Point, Radians, Rectangle, Renderer,
+ Size, Theme,
+};
+use rustc_hash::FxHashMap;
const TRI_DOWN: &[u8] = include_bytes!("../../assets/icons/ui/tri_down.svg");
const TRI_UP: &[u8] = include_bytes!("../../assets/icons/ui/tri_up.svg");
@@ -65,7 +75,7 @@ pub const FILE_EXPORT: &[u8] = include_bytes!("../../assets/icons/ui/file_export
pub const PRINT: &[u8] = include_bytes!("../../assets/icons/ui/print.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 DIRTY_DOT: &[u8] = include_bytes!("../../assets/icons/ui/dirty_dot.svg");
pub const ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg");
// ββ Status-bar toggle icons (issue #216) ββββββββββββββββββββββββββββββββββ
@@ -82,40 +92,415 @@ pub const ST_FILTER: &[u8] = include_bytes!("../../assets/icons/status/filter.sv
pub const ST_SELCYCLE: &[u8] = include_bytes!("../../assets/icons/status/selcycle.svg");
pub const ST_CLEANSCREEN: &[u8] = include_bytes!("../../assets/icons/status/cleanscreen.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> {
+// Tool SVGs share a small source palette. These colours are semantic rather
+// than literal: cyan is the accent, pale grey is foreground, yellow is warning,
+// and so on. `SemanticIcon` resolves those roles from Iced's active extended
+// palette at draw time, retaining the artwork's multiple colours across themes.
+const SEMANTIC_CACHE_LIMIT: usize = 2048;
+
+#[derive(Clone, Copy, Hash, PartialEq, Eq)]
+struct SemanticCacheKey {
+ address: usize,
+ length: usize,
+ palette: [[u8; 4]; 6],
+}
+
+thread_local! {
+ static SEMANTIC_CACHE: RefCell> =
+ RefCell::new(FxHashMap::default());
+}
+
+#[derive(Clone, Copy)]
+struct SemanticColors {
+ background: [u8; 7],
+ text: [u8; 7],
+ primary_weak: [u8; 7],
+ primary: [u8; 7],
+ primary_strong: [u8; 7],
+ secondary_weak: [u8; 7],
+ secondary: [u8; 7],
+ secondary_strong: [u8; 7],
+ success_weak: [u8; 7],
+ success: [u8; 7],
+ warning: [u8; 7],
+ warning_strong: [u8; 7],
+ danger_weak: [u8; 7],
+ danger: [u8; 7],
+}
+
+impl SemanticColors {
+ fn from_theme(theme: &Theme) -> Self {
+ let palette = theme.extended_palette();
+ Self {
+ background: color_hex(palette.background.strong.color),
+ text: color_hex(palette.background.base.text),
+ primary_weak: color_hex(palette.primary.weak.color),
+ primary: color_hex(palette.primary.base.color),
+ primary_strong: color_hex(palette.primary.strong.color),
+ secondary_weak: color_hex(palette.secondary.weak.color),
+ secondary: color_hex(palette.secondary.base.color),
+ secondary_strong: color_hex(palette.secondary.strong.color),
+ success_weak: color_hex(palette.success.weak.color),
+ success: color_hex(palette.success.base.color),
+ warning: color_hex(palette.warning.base.color),
+ warning_strong: color_hex(palette.warning.strong.color),
+ danger_weak: color_hex(palette.danger.weak.color),
+ danger: color_hex(palette.danger.base.color),
+ }
+ }
+}
+
+struct SemanticIcon {
+ bytes: &'static [u8],
+ size: f32,
+ opacity: f32,
+}
+
+impl Widget for SemanticIcon {
+ fn size(&self) -> Size {
+ Size::new(Length::Fixed(self.size), Length::Fixed(self.size))
+ }
+
+ fn layout(
+ &mut self,
+ _tree: &mut Tree,
+ _renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ layout::atomic(
+ limits,
+ Length::Fixed(self.size),
+ Length::Fixed(self.size),
+ )
+ }
+
+ fn draw(
+ &self,
+ _tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Theme,
+ _style: &renderer::Style,
+ layout: Layout<'_>,
+ _cursor: iced::advanced::mouse::Cursor,
+ _viewport: &Rectangle,
+ ) {
+ let handle = semantic_handle(self.bytes, theme);
+ let measured = renderer.measure_svg(&handle);
+ if measured.width == 0 || measured.height == 0 {
+ return;
+ }
+
+ let image_size = Size::new(measured.width as f32, measured.height as f32);
+ let bounds = layout.bounds();
+ let fitted = ContentFit::Contain.fit(image_size, bounds.size());
+ let position = Point::new(
+ bounds.center_x() - fitted.width / 2.0,
+ bounds.center_y() - fitted.height / 2.0,
+ );
+
+ renderer.draw_svg(
+ core_svg::Svg {
+ handle,
+ color: None,
+ rotation: Radians(0.0),
+ opacity: self.opacity,
+ },
+ Rectangle::new(position, fitted),
+ bounds,
+ );
+ }
+}
+
+/// Render a multi-colour tool icon using semantic colours from the active theme.
+pub fn semantic<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
+ Element::new(SemanticIcon {
+ bytes,
+ size,
+ opacity: 1.0,
+ })
+}
+
+/// Render a disabled multi-colour tool icon without flattening its colour roles.
+pub fn semantic_disabled<'a, M: 'a>(
+ bytes: &'static [u8],
+ size: f32,
+) -> Element<'a, M> {
+ Element::new(SemanticIcon {
+ bytes,
+ size,
+ opacity: 0.42,
+ })
+}
+
+fn semantic_handle(bytes: &'static [u8], theme: &Theme) -> svg::Handle {
+ let key = SemanticCacheKey {
+ address: bytes.as_ptr() as usize,
+ length: bytes.len(),
+ palette: palette_key(theme),
+ };
+
+ SEMANTIC_CACHE.with(|cache| {
+ let mut cache = cache.borrow_mut();
+ if let Some(handle) = cache.get(&key) {
+ return handle.clone();
+ }
+
+ let handle = svg::Handle::from_memory(recolor_semantic_svg(bytes, theme));
+ if cache.len() >= SEMANTIC_CACHE_LIMIT {
+ cache.clear();
+ }
+ cache.insert(key, handle.clone());
+ handle
+ })
+}
+
+fn palette_key(theme: &Theme) -> [[u8; 4]; 6] {
+ let palette = theme.palette();
+ [
+ palette.background.into_rgba8(),
+ palette.text.into_rgba8(),
+ palette.primary.into_rgba8(),
+ palette.success.into_rgba8(),
+ palette.warning.into_rgba8(),
+ palette.danger.into_rgba8(),
+ ]
+}
+
+fn recolor_semantic_svg(source: &[u8], theme: &Theme) -> Vec {
+ let colors = SemanticColors::from_theme(theme);
+ let mut output = Vec::with_capacity(source.len());
+ let mut index = 0;
+
+ while index < source.len() {
+ if source[index] == b'#' {
+ let mut end = index + 1;
+ while end < source.len() && source[end].is_ascii_hexdigit() {
+ end += 1;
+ }
+ let digit_count = end - index - 1;
+ if matches!(digit_count, 3 | 4 | 6 | 8) {
+ if let Some(replacement) =
+ semantic_color(&source[index..end], &colors)
+ {
+ output.extend_from_slice(replacement);
+ index = end;
+ continue;
+ }
+ }
+ } else if starts_with_word_ignore_ascii_case(source, index, b"white") {
+ output.extend_from_slice(&colors.text);
+ index += 5;
+ continue;
+ }
+
+ output.push(source[index]);
+ index += 1;
+ }
+
+ output
+}
+
+fn semantic_color<'a>(
+ token: &[u8],
+ colors: &'a SemanticColors,
+) -> Option<&'a [u8; 7]> {
+ if is_one_of(token, &["#e0e0e0", "#eeeeee", "#ffffff", "#e1e1e1"]) {
+ Some(&colors.text)
+ } else if is_one_of(token, &["#cccccc", "#bdbdbd", "#aaaaaa"]) {
+ Some(&colors.secondary_strong)
+ } else if is_one_of(
+ token,
+ &["#888888", "#888", "#9e9e9e", "#90a4ae", "#78909c", "#7a7a7a"],
+ ) {
+ Some(&colors.secondary)
+ } else if is_one_of(
+ token,
+ &[
+ "#505050", "#555", "#606060", "#666", "#616161", "#546e7a",
+ "#455a64", "#37474f",
+ ],
+ ) {
+ Some(&colors.secondary_weak)
+ } else if is_one_of(token, &["#1a1a1a"]) {
+ Some(&colors.background)
+ } else if is_one_of(token, &["#4cc9f0", "#4bc8f0", "#4a9eff", "#0099e5"]) {
+ Some(&colors.primary)
+ } else if is_one_of(token, &["#1565c0"]) {
+ Some(&colors.primary_strong)
+ } else if is_one_of(token, &["#0d47a1"]) {
+ Some(&colors.primary_weak)
+ } else if is_one_of(token, &["#4ccf6f"]) {
+ Some(&colors.success)
+ } else if is_one_of(token, &["#00695c", "#004d40"]) {
+ Some(&colors.success_weak)
+ } else if is_one_of(token, &["#f0c040", "#ffd740", "#fdd835"]) {
+ Some(&colors.warning)
+ } else if is_one_of(token, &["#f9a825"]) {
+ Some(&colors.warning_strong)
+ } else if is_one_of(
+ token,
+ &[
+ "#e05050", "#ef5350", "#e06c6c", "#e53935", "#ff0000", "#e10000",
+ ],
+ ) {
+ Some(&colors.danger)
+ } else if is_one_of(token, &["#b71c1c"]) {
+ Some(&colors.danger_weak)
+ } else {
+ None
+ }
+}
+
+fn is_one_of(token: &[u8], candidates: &[&str]) -> bool {
+ candidates
+ .iter()
+ .any(|candidate| token.eq_ignore_ascii_case(candidate.as_bytes()))
+}
+
+fn starts_with_word_ignore_ascii_case(
+ source: &[u8],
+ index: usize,
+ word: &[u8],
+) -> bool {
+ let Some(end) = index.checked_add(word.len()) else {
+ return false;
+ };
+ if end > source.len()
+ || !source[index..end].eq_ignore_ascii_case(word)
+ || index > 0 && source[index - 1].is_ascii_alphabetic()
+ || end < source.len() && source[end].is_ascii_alphabetic()
+ {
+ return false;
+ }
+ true
+}
+
+fn color_hex(color: Color) -> [u8; 7] {
+ const HEX: &[u8; 16] = b"0123456789abcdef";
+ let [red, green, blue, _] = color.into_rgba8();
+ [
+ b'#',
+ HEX[(red >> 4) as usize],
+ HEX[(red & 0x0f) as usize],
+ HEX[(green >> 4) as usize],
+ HEX[(green & 0x0f) as usize],
+ HEX[(blue >> 4) as usize],
+ HEX[(blue & 0x0f) as usize],
+ ]
+}
+
+/// Render a chrome icon with the active Iced theme's normal text color.
+pub fn themed<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
svg(svg::Handle::from_memory(bytes))
.width(size)
.height(size)
- .style(move |_: &Theme, _| svg::Style { color: Some(color) })
+ .style(|theme: &Theme, _| svg::Style {
+ color: Some(theme.extended_palette().background.base.text),
+ })
.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)
+/// Render secondary chrome with the active Iced theme's text color.
+pub fn themed_secondary<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
+ svg(svg::Handle::from_memory(bytes))
+ .width(size)
+ .height(size)
+ .style(|theme: &Theme, _| svg::Style {
+ color: Some(
+ theme
+ .extended_palette()
+ .background
+ .base
+ .text
+ .scale_alpha(0.72),
+ ),
+ })
+ .into()
}
-/// 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> {
+/// Render disabled chrome with the active Iced theme's text color.
+pub fn themed_disabled<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
+ svg(svg::Handle::from_memory(bytes))
+ .width(size)
+ .height(size)
+ .style(|theme: &Theme, _| svg::Style {
+ color: Some(
+ theme
+ .extended_palette()
+ .background
+ .base
+ .text
+ .scale_alpha(0.42),
+ ),
+ })
+ .into()
+}
+
+/// Render an emphasized chrome icon with the active Iced theme's primary color.
+pub fn themed_primary<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
+ svg(svg::Handle::from_memory(bytes))
+ .width(size)
+ .height(size)
+ .style(|theme: &Theme, _| svg::Style {
+ color: Some(theme.extended_palette().primary.base.color),
+ })
+ .into()
+}
+
+/// Render a positive-state chrome icon with the active Iced theme's success color.
+pub fn themed_success<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
+ svg(svg::Handle::from_memory(bytes))
+ .width(size)
+ .height(size)
+ .style(|theme: &Theme, _| svg::Style {
+ color: Some(theme.extended_palette().success.base.color),
+ })
+ .into()
+}
+
+/// Render a warning-state chrome icon from the active Iced theme.
+pub fn themed_warning<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
+ svg(svg::Handle::from_memory(bytes))
+ .width(size)
+ .height(size)
+ .style(|theme: &Theme, _| svg::Style {
+ color: Some(theme.extended_palette().warning.base.color),
+ })
+ .into()
+}
+
+/// Render a destructive-state chrome icon from the active Iced theme.
+pub fn themed_danger<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
+ svg(svg::Handle::from_memory(bytes))
+ .width(size)
+ .height(size)
+ .style(|theme: &Theme, _| svg::Style {
+ color: Some(theme.extended_palette().danger.base.color),
+ })
+ .into()
+}
+
+/// Render an icon with the foreground chosen for a danger-coloured surface.
+pub fn themed_danger_text<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
+ svg(svg::Handle::from_memory(bytes))
+ .width(size)
+ .height(size)
+ .style(|theme: &Theme, _| svg::Style {
+ color: Some(theme.extended_palette().danger.base.text),
+ })
+ .into()
+}
+
+/// Fixed-width check column colored from the active Iced theme.
+pub fn themed_check_cell<'a, M: 'a>(active: bool) -> Element<'a, M> {
let inner: Element<'a, M> = if active {
- tinted(CHECK, 11.0, color)
+ themed_primary(CHECK, 11.0)
} 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()
-}
-
/// SVG bytes for an OSNAP mode's marker symbol, for the snap menu. (#138)
pub fn osnap(snap: crate::snap::SnapType) -> &'static [u8] {
use crate::snap::SnapType as S;
@@ -166,46 +551,102 @@ pub fn layer_lock(locked: bool) -> &'static [u8] {
}
}
-/// Downward dropdown caret (replaces `βΎ`).
-pub fn arrow_down<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
- icon(TRI_DOWN, size, color)
+pub fn themed_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
+ themed(TRI_DOWN, size)
}
-/// 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)
+pub fn themed_arrow_up<'a, M: 'a>(size: f32) -> Element<'a, M> {
+ themed(TRI_UP, size)
}
-/// 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)
+pub fn themed_arrow_right<'a, M: 'a>(size: f32) -> Element<'a, M> {
+ themed(TRI_RIGHT, size)
}
-/// Leftward caret.
-pub fn arrow_left<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
- icon(TRI_LEFT, size, color)
+pub fn themed_arrow_left<'a, M: 'a>(size: f32) -> Element<'a, M> {
+ themed(TRI_LEFT, size)
}
-/// House glyph β the ViewCube "home view" button.
-pub fn home<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
- icon(HOME, size, color)
+pub fn themed_primary_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
+ themed_primary(TRI_DOWN, size)
+}
+
+pub fn themed_secondary_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
+ themed_secondary(TRI_DOWN, size)
+}
+
+pub fn themed_disabled_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
+ themed_disabled(TRI_DOWN, size)
+}
+
+pub fn themed_home<'a, M: 'a>(size: f32) -> Element<'a, M> {
+ themed(HOME, size)
}
/// Caret that flips up/down with `open`.
-pub fn arrow_toggle<'a, M: 'a>(open: bool, size: f32, color: Color) -> Element<'a, M> {
+pub fn themed_arrow_toggle<'a, M: 'a>(open: bool, size: f32) -> Element<'a, M> {
if open {
- arrow_up(size, color)
+ themed_arrow_up(size)
} else {
- arrow_down(size, color)
+ themed_arrow_down(size)
}
}
-/// Undo curved arrow (replaces `βΆ`).
-pub fn undo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
- icon(UNDO, size, color)
+pub fn themed_undo<'a, M: 'a>(size: f32, enabled: bool) -> Element<'a, M> {
+ if enabled {
+ themed(UNDO, size)
+ } else {
+ themed_disabled(UNDO, size)
+ }
}
-/// Redo curved arrow (replaces `β·`).
-pub fn redo<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
- icon(REDO, size, color)
+pub fn themed_redo<'a, M: 'a>(size: f32, enabled: bool) -> Element<'a, M> {
+ if enabled {
+ themed(REDO, size)
+ } else {
+ themed_disabled(REDO, size)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn semantic_svg_uses_multiple_theme_roles() {
+ let source = br##""##;
+ let theme = Theme::Dark;
+ let colors = SemanticColors::from_theme(&theme);
+ let themed = recolor_semantic_svg(source, &theme);
+
+ assert!(contains(&themed, &colors.text));
+ assert!(contains(&themed, &colors.primary));
+ assert!(contains(&themed, &colors.warning));
+ assert!(contains(&themed, &colors.danger));
+ assert!(contains(&themed, &colors.success));
+ assert!(contains(&themed, b"#123456"));
+ }
+
+ #[test]
+ fn semantic_svg_maps_named_white() {
+ let theme = Theme::Dark;
+ let colors = SemanticColors::from_theme(&theme);
+ let themed =
+ recolor_semantic_svg(br##""##, &theme);
+
+ assert!(contains(&themed, &colors.text));
+ }
+
+ fn contains(source: &[u8], needle: &[u8]) -> bool {
+ source
+ .windows(needle.len())
+ .any(|window| window == needle)
+ }
}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 38640b8d1..d6ce49f54 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -2,6 +2,17 @@
/// Change this to scale the ribbon, layer manager rows, and property panel rows uniformly.
pub const ROW_H: f32 = 26.0;
+/// Place `content` at fixed top-left coordinates inside a fill-sized layer.
+/// Negative coordinates clamp to the layer edge.
+pub fn pin_at<'a, Message: 'a>(
+ position: iced::Point,
+ content: impl Into>,
+) -> iced::Element<'a, Message> {
+ iced::widget::pin(content)
+ .position(iced::Point::new(position.x.max(0.0), position.y.max(0.0)))
+ .into()
+}
+
pub mod color_select;
pub mod command_line;
pub mod icons;
diff --git a/src/ui/modal.rs b/src/ui/modal.rs
index 5b30ab83d..64a0a8877 100644
--- a/src/ui/modal.rs
+++ b/src/ui/modal.rs
@@ -7,34 +7,7 @@
use crate::app::Message;
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 {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 1.0,
-};
-const BORDER_C: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-/// Title-bar background.
-const TITLE_C: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.18,
- a: 1.0,
-};
-/// Drag-handle arrow β bright green so the move affordance stands out.
-const GRIP_C: Color = Color {
- r: 0.2,
- g: 1.0,
- b: 0.3,
- a: 1.0,
-};
+use iced::{Background, Border, Element, Length, Padding, Theme, Vector};
/// Stack `content` over `base` behind a dimmed backdrop, framed with a
/// draggable title bar (the β close button at its right end). The backdrop only
@@ -52,15 +25,9 @@ pub fn modal<'a>(
offset: Vector,
resizable: bool,
) -> Element<'a, Message> {
- let close = button(crate::ui::icons::tinted(
+ let close = button(crate::ui::icons::themed_danger(
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])
@@ -71,10 +38,12 @@ pub fn modal<'a>(
// screen width β the dialog stays sized to its content. Pressing the grip
// starts a drag (handled in `update`).
let grip = mouse_area(
- container(crate::ui::icons::tinted(crate::ui::icons::MOVE, 14.0, GRIP_C))
+ container(crate::ui::icons::themed_primary(crate::ui::icons::MOVE, 14.0))
.padding([1, 7])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TITLE_C)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weakest.color,
+ )),
border: Border {
radius: 4.0.into(),
..Default::default()
@@ -89,12 +58,7 @@ pub fn modal<'a>(
// overlaid at the right edge. The bar takes an explicit `title_width`
// (the caller's content width) instead of `Fill` β a Fill child inside
// the Shrink frame would blow the dialog out to the full screen.
- let title_text = iced::widget::text(title).size(15).color(Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
- });
+ let title_text = iced::widget::text(title).size(15);
let title_bar = stack![
container(title_text)
.width(Length::Fixed(title_width))
@@ -108,10 +72,12 @@ pub fn modal<'a>(
.align_y(iced::alignment::Vertical::Center),
];
- let panel_style = |_: &Theme| container::Style {
- background: Some(Background::Color(PANEL)),
+ let panel_style = |theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
border: Border {
- color: BORDER_C,
+ color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 6.0.into(),
},
@@ -125,7 +91,7 @@ pub fn modal<'a>(
// top-right above the content either way (`align_x(Right)`).
let body = if resizable {
let resize = mouse_area(
- container(crate::ui::icons::tinted(crate::ui::icons::RESIZE, 15.0, GRIP_C))
+ container(crate::ui::icons::themed_primary(crate::ui::icons::RESIZE, 15.0))
.padding([0, 2]),
)
.on_press(Message::ModalResizeGrab)
@@ -157,11 +123,15 @@ pub fn modal<'a>(
.center_x(Length::Fill)
.center_y(Length::Fill)
.padding(pad)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- a: 0.55,
- ..Color::BLACK
- })),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme
+ .extended_palette()
+ .background
+ .strongest
+ .color
+ .scale_alpha(0.55),
+ )),
..Default::default()
}),
)
@@ -177,24 +147,15 @@ pub fn modal<'a>(
.into()
}
-fn close_style(_: &Theme, status: button::Status) -> button::Style {
- let bg = match status {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.7,
- g: 0.2,
- b: 0.2,
- a: 1.0,
- },
- _ => Color {
- r: 0.25,
- g: 0.25,
- b: 0.25,
- a: 1.0,
- },
+fn close_style(theme: &Theme, status: button::Status) -> button::Style {
+ let palette = theme.extended_palette();
+ let pair = match status {
+ button::Status::Hovered | button::Status::Pressed => palette.danger.strong,
+ _ => palette.background.weakest,
};
button::Style {
- background: Some(Background::Color(bg)),
- text_color: Color::WHITE,
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
radius: 4.0.into(),
..Default::default()
diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs
index f333049e5..31e31b279 100644
--- a/src/ui/overlay.rs
+++ b/src/ui/overlay.rs
@@ -292,7 +292,7 @@ struct SelectionCanvas {
hover_locked: bool,
}
-fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker) {
+fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme) {
let sp = grip.pos;
let h = crate::scene::pick::grip::GRIP_HALF_PX;
let path = match grip.shape {
@@ -332,30 +332,13 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker) {
};
if grip.is_hot {
- frame.fill(
- &path,
- Color {
- r: 1.0,
- g: 0.15,
- b: 0.10,
- a: 1.0,
- },
- );
+ frame.fill(&path, theme.extended_palette().danger.base.color);
} else {
- let color = Color {
- r: 0.10,
- g: 0.45,
- b: 0.90,
- a: 1.0,
- };
+ let palette = theme.extended_palette();
+ let color = palette.primary.base.color;
frame.fill(
&path,
- Color {
- r: 0.10,
- g: 0.10,
- b: 0.20,
- a: 0.7,
- },
+ palette.background.base.color.scale_alpha(0.7),
);
frame.stroke(
&path,
@@ -442,7 +425,7 @@ impl canvas::Program for SelectionCanvas {
&self,
_state: &(),
renderer: &iced::Renderer,
- _theme: &Theme,
+ theme: &Theme,
bounds: iced::Rectangle,
cursor: mouse::Cursor,
) -> Vec {
@@ -452,18 +435,13 @@ impl canvas::Program for SelectionCanvas {
// Filled bars in the pane_grid spacing gaps, so adjacent panes read as
// distinct viewports. Drawn first so all other overlays sit on top.
if !self.dividers.is_empty() {
- const DIVIDER: Color = Color {
- r: 0.46,
- g: 0.52,
- b: 0.62,
- a: 1.0,
- };
+ let divider = theme.extended_palette().background.neutral.color;
for d in &self.dividers {
let bar = canvas::Path::rectangle(
Point::new(d.x, d.y),
iced::Size::new(d.width.max(1.0), d.height.max(1.0)),
);
- frame.fill(&bar, DIVIDER);
+ frame.fill(&bar, divider);
}
}
@@ -472,23 +450,13 @@ impl canvas::Program for SelectionCanvas {
// under the cursor, and drag a translucent ghost card along the cursor
// so the pane is visibly "moving".
if let Some(src) = self.pane_move_rect {
- let accent = Color {
- r: 0.30,
- g: 0.62,
- b: 1.0,
- a: 1.0,
- };
+ let accent = theme.extended_palette().primary.base.color;
// Source pane: dimmed + dashed-feel outline (it has been lifted).
let src_path =
canvas::Path::rectangle(Point::new(src.x, src.y), iced::Size::new(src.width, src.height));
frame.fill(
&src_path,
- Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.28,
- },
+ theme.extended_palette().background.strong.color.scale_alpha(0.28),
);
frame.stroke(
&src_path,
@@ -539,18 +507,20 @@ impl canvas::Program for SelectionCanvas {
// Draw a selection marquee (green crossing / blue window) as a filled,
// stroked rectangle between two canvas points. Shared by the live
// box-selection and the preview-only window marquee (#291).
- fn draw_marquee(frame: &mut canvas::Frame, a: Point, b: Point, crossing: bool) {
- let (fill, stroke) = if crossing {
- (
- Color { r: 0.20, g: 0.72, b: 0.44, a: 0.12 },
- Color { r: 0.20, g: 0.72, b: 0.44, a: 0.9 },
- )
+ fn draw_marquee(
+ frame: &mut canvas::Frame,
+ a: Point,
+ b: Point,
+ crossing: bool,
+ theme: &Theme,
+ ) {
+ let base = if crossing {
+ theme.extended_palette().success.base.color
} else {
- (
- Color { r: 0.20, g: 0.44, b: 0.72, a: 0.12 },
- Color { r: 0.20, g: 0.44, b: 0.72, a: 0.9 },
- )
+ theme.extended_palette().primary.base.color
};
+ let fill = base.scale_alpha(0.12);
+ let stroke = base.scale_alpha(0.9);
let x0 = a.x.min(b.x);
let y0 = a.y.min(b.y);
let w = (a.x - b.x).abs();
@@ -568,45 +538,21 @@ impl canvas::Program for SelectionCanvas {
}
if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) {
- draw_marquee(&mut frame, a, b, self.selection.box_crossing);
+ draw_marquee(&mut frame, a, b, self.selection.box_crossing, theme);
}
// Preview marquee for point-picked windows (STRETCH) β same look, no pick.
if let Some((a, b, crossing)) = self.selection.preview_box {
- draw_marquee(&mut frame, a, b, crossing);
+ draw_marquee(&mut frame, a, b, crossing, theme);
}
if self.selection.poly_active && self.selection.poly_points.len() > 1 {
- let (fill, stroke) = if self.selection.poly_crossing {
- (
- Color {
- r: 0.20,
- g: 0.72,
- b: 0.44,
- a: 0.12,
- },
- Color {
- r: 0.20,
- g: 0.72,
- b: 0.44,
- a: 0.9,
- },
- )
+ let base = if self.selection.poly_crossing {
+ theme.extended_palette().success.base.color
} else {
- (
- Color {
- r: 0.20,
- g: 0.44,
- b: 0.72,
- a: 0.12,
- },
- Color {
- r: 0.20,
- g: 0.44,
- b: 0.72,
- a: 0.9,
- },
- )
+ theme.extended_palette().primary.base.color
};
+ let fill = base.scale_alpha(0.12);
+ let stroke = base.scale_alpha(0.9);
if let Some(cur) = self.selection.last_move_pos {
let start = self.selection.poly_points[0];
let fill_path = canvas::Path::new(|p| {
@@ -667,7 +613,7 @@ impl canvas::Program for SelectionCanvas {
};
frame.with_clip(grip_clip, |frame| {
for grip in &self.grips {
- draw_grip_marker(frame, grip);
+ draw_grip_marker(frame, grip, theme);
}
});
}
@@ -962,12 +908,12 @@ impl canvas::Program for SelectionCanvas {
// PAN mode replaces the crosshair with a hand cursor.
if !over_viewcube && !over_divider && !self.pan_mode && !self.suppressed {
if let Some(cp) = self.selection.last_move_pos {
- let color = Color {
- r: 0.85,
- g: 0.85,
- b: 0.85,
- a: 0.90,
- };
+ let color = theme
+ .extended_palette()
+ .background
+ .base
+ .text
+ .scale_alpha(0.90);
let stroke = canvas::Stroke {
width: 1.0,
style: canvas::Style::Solid(color),
@@ -1010,8 +956,9 @@ impl canvas::Program for SelectionCanvas {
// the hovered object sits on a locked layer (issue: locked
// objects are visible + snappable but not selectable/editable).
if self.hover_locked {
- let amber = Color { r: 0.96, g: 0.76, b: 0.26, a: 0.98 };
- let dark = Color { r: 0.12, g: 0.10, b: 0.04, a: 1.0 };
+ let warning = theme.extended_palette().warning.base;
+ let amber = warning.color.scale_alpha(0.98);
+ let dark = warning.text;
let bx = cp.x + sq + 7.0;
let by = cp.y - sq - 13.0;
// Lock body (filled).
@@ -1058,12 +1005,7 @@ impl canvas::Program for SelectionCanvas {
}
// ββ Object Snap Tracking βββββββββββββββββββββββββββββββββββββββββββββ
- let track_color = Color {
- r: 0.15,
- g: 0.85,
- b: 0.95,
- a: 0.7,
- };
+ let track_color = theme.extended_palette().primary.base.color.scale_alpha(0.7);
// The alignment line the cursor is currently locked to β drawn at its
// real angle from the acquired point through the lock and a little
// beyond, dashed so it reads as a construction guide. This covers the
@@ -2142,10 +2084,12 @@ struct DynInputCanvas {
}
impl DynInputCanvas {
- fn dotted() -> canvas::Stroke<'static> {
+ fn dotted(theme: &Theme) -> canvas::Stroke<'static> {
canvas::Stroke {
width: 1.0,
- style: canvas::Style::Solid(Color { r: 0.55, g: 0.55, b: 0.58, a: 0.9 }),
+ style: canvas::Style::Solid(
+ theme.extended_palette().background.neutral.color.scale_alpha(0.9)
+ ),
line_dash: canvas::LineDash { segments: &[2.0, 3.0], offset: 0 },
..Default::default()
}
@@ -2160,13 +2104,19 @@ impl DynInputCanvas {
}
/// Draw a value box centred at `center`, clamped inside `bounds`.
- fn draw_box(frame: &mut canvas::Frame, b: &DynBox, center: Point, bounds: iced::Rectangle) {
+ fn draw_box(
+ frame: &mut canvas::Frame,
+ b: &DynBox,
+ center: Point,
+ bounds: iced::Rectangle,
+ theme: &Theme,
+ ) {
let content = Self::box_content(b);
let w = (content.len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0;
let x = (center.x - w * 0.5).clamp(0.0, (bounds.width - w).max(0.0));
let y = (center.y - DYN_BOX_H * 0.5).clamp(0.0, (bounds.height - DYN_BOX_H).max(0.0));
let rect = canvas::Path::rectangle(Point { x, y }, Size { width: w, height: DYN_BOX_H });
- let (fill, border) = Self::box_colors(b);
+ let (fill, border, text) = Self::box_colors(b, theme);
frame.fill(&rect, fill);
frame.stroke(
&rect,
@@ -2177,7 +2127,7 @@ impl DynInputCanvas {
frame.fill_text(canvas::Text {
content,
position: Point { x: x + DYN_PAD, y: y + DYN_PAD },
- color: Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 },
+ color: text,
size: iced::Pixels(DYN_FONT),
// Force Advanced shaping: the default `Auto` uses Basic shaping for
// ASCII-only strings, which the web (wgpu/webgl) backend fails to
@@ -2188,43 +2138,48 @@ impl DynInputCanvas {
});
}
- fn box_colors(b: &DynBox) -> (Color, Color) {
+ fn box_colors(b: &DynBox, theme: &Theme) -> (Color, Color, Color) {
+ let palette = theme.extended_palette();
if b.active {
(
- Color { r: 0.12, g: 0.18, b: 0.30, a: 0.95 },
- Color { r: 0.45, g: 0.70, b: 1.0, a: 1.0 },
+ palette.primary.weak.color,
+ palette.primary.base.color,
+ palette.primary.weak.text,
)
} else if b.locked {
(
- Color { r: 0.05, g: 0.05, b: 0.12, a: 0.9 },
- Color { r: 0.95, g: 0.75, b: 0.30, a: 0.9 },
+ palette.warning.weak.color,
+ palette.warning.base.color,
+ palette.warning.weak.text,
)
} else {
(
- Color { r: 0.05, g: 0.05, b: 0.12, a: 0.9 },
- Color { r: 0.35, g: 0.55, b: 0.90, a: 0.9 },
+ palette.background.weak.color,
+ palette.background.neutral.color,
+ palette.background.weak.text,
)
}
}
/// Prompt pill at `pos`.
- fn draw_prompt(&self, frame: &mut canvas::Frame, pos: Point) {
+ fn draw_prompt(&self, frame: &mut canvas::Frame, pos: Point, theme: &Theme) {
if self.prompt.is_empty() {
return;
}
+ let palette = theme.extended_palette();
let pw = (self.prompt.len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0;
let rect = canvas::Path::rectangle(pos, Size { width: pw, height: DYN_BOX_H });
- frame.fill(&rect, Color { r: 0.10, g: 0.10, b: 0.12, a: 1.0 });
+ frame.fill(&rect, palette.background.strong.color);
frame.stroke(
&rect,
canvas::Stroke::default()
- .with_color(Color { r: 0.35, g: 0.55, b: 0.90, a: 0.9 })
+ .with_color(palette.primary.base.color.scale_alpha(0.9))
.with_width(1.0),
);
frame.fill_text(canvas::Text {
content: self.prompt.clone(),
position: Point { x: pos.x + DYN_PAD, y: pos.y + DYN_PAD },
- color: Color { r: 0.70, g: 0.85, b: 0.70, a: 1.0 },
+ color: palette.background.strong.text,
size: iced::Pixels(DYN_FONT),
shaping: iced::advanced::text::Shaping::Advanced,
..Default::default()
@@ -2233,7 +2188,13 @@ impl DynInputCanvas {
/// Guided layout: draw the guide geometry anchored at `base`, then place
/// each box according to its role.
- fn draw_guided(&self, frame: &mut canvas::Frame, bounds: iced::Rectangle, base: Point) {
+ fn draw_guided(
+ &self,
+ frame: &mut canvas::Frame,
+ bounds: iced::Rectangle,
+ base: Point,
+ theme: &Theme,
+ ) {
let cursor_raw = self.cursor_screen;
let (vx, vy) = (cursor_raw.x - base.x, cursor_raw.y - base.y);
let raw_len = (vx * vx + vy * vy).sqrt().max(1.0);
@@ -2308,7 +2269,7 @@ impl DynInputCanvas {
y: base.y + a_ref.sin() * len,
});
});
- frame.stroke(&href, Self::dotted());
+ frame.stroke(&href, Self::dotted(theme));
let arc = canvas::Path::new(|p| {
let steps = 48;
for k in 0..=steps {
@@ -2324,14 +2285,14 @@ impl DynInputCanvas {
}
}
});
- frame.stroke(&arc, Self::dotted());
+ frame.stroke(&arc, Self::dotted(theme));
}
DynGuide::Radius => {
let line = canvas::Path::new(|p| {
p.move_to(base);
p.line_to(cursor);
});
- frame.stroke(&line, Self::dotted());
+ frame.stroke(&line, Self::dotted(theme));
}
DynGuide::Perp => {
if let Some((end, _, _)) = perp_info {
@@ -2340,7 +2301,7 @@ impl DynInputCanvas {
p.move_to(base);
p.line_to(end);
});
- frame.stroke(&line, Self::dotted());
+ frame.stroke(&line, Self::dotted(theme));
}
}
DynGuide::PerpDim => {
@@ -2351,14 +2312,14 @@ impl DynInputCanvas {
p.move_to(ob);
p.line_to(oe);
});
- frame.stroke(&dim, Self::dotted());
+ frame.stroke(&dim, Self::dotted(theme));
let ext = canvas::Path::new(|p| {
p.move_to(base);
p.line_to(ob);
p.move_to(end);
p.line_to(oe);
});
- frame.stroke(&ext, Self::dotted());
+ frame.stroke(&ext, Self::dotted(theme));
}
}
DynGuide::AxisDelta | DynGuide::RectSides => {
@@ -2368,7 +2329,7 @@ impl DynInputCanvas {
p.line_to(corner);
p.line_to(cursor);
});
- frame.stroke(&legs, Self::dotted());
+ frame.stroke(&legs, Self::dotted(theme));
if self.guide == DynGuide::RectSides {
// Close the rectangle so both side pairs read as a box.
let rest = canvas::Path::new(|p| {
@@ -2376,7 +2337,7 @@ impl DynInputCanvas {
p.line_to(Point { x: base.x, y: cursor.y });
p.line_to(cursor);
});
- frame.stroke(&rest, Self::dotted());
+ frame.stroke(&rest, Self::dotted(theme));
}
}
DynGuide::None => {}
@@ -2423,12 +2384,12 @@ impl DynInputCanvas {
y: base.y + dy * len * 0.5 + ny * 16.0,
},
};
- Self::draw_box(frame, b, center, bounds);
+ Self::draw_box(frame, b, center, bounds, theme);
}
}
/// Fallback row layout near the cursor (no anchor / `None` guide).
- fn draw_row(&self, frame: &mut canvas::Frame, bounds: iced::Rectangle) {
+ fn draw_row(&self, frame: &mut canvas::Frame, bounds: iced::Rectangle, theme: &Theme) {
let texts: Vec = self
.boxes
.iter()
@@ -2466,7 +2427,7 @@ impl DynInputCanvas {
py = (by - pad - DYN_BOX_H).max(0.0);
}
if has_prompt {
- self.draw_prompt(frame, Point { x: bx, y: py });
+ self.draw_prompt(frame, Point { x: bx, y: py }, theme);
}
let mut x = bx;
@@ -2474,7 +2435,7 @@ impl DynInputCanvas {
let w = widths[i];
let rect =
canvas::Path::rectangle(Point { x, y: by }, Size { width: w, height: DYN_BOX_H });
- let (fill, border) = Self::box_colors(b);
+ let (fill, border, text) = Self::box_colors(b, theme);
frame.fill(&rect, fill);
frame.stroke(
&rect,
@@ -2485,7 +2446,7 @@ impl DynInputCanvas {
frame.fill_text(canvas::Text {
content: texts[i].clone(),
position: Point { x: x + DYN_PAD, y: by + DYN_PAD },
- color: Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 },
+ color: text,
size: iced::Pixels(DYN_FONT),
shaping: iced::advanced::text::Shaping::Advanced,
..Default::default()
@@ -2511,7 +2472,7 @@ impl canvas::Program for DynInputCanvas {
&self,
_state: &(),
renderer: &iced::Renderer,
- _theme: &Theme,
+ theme: &Theme,
bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec {
@@ -2529,15 +2490,15 @@ impl canvas::Program for DynInputCanvas {
if py + DYN_BOX_H > bounds.height {
py = (self.cursor_screen.y - DYN_BOX_H - 4.0).max(0.0);
}
- self.draw_prompt(&mut frame, Point { x: px, y: py });
+ self.draw_prompt(&mut frame, Point { x: px, y: py }, theme);
}
return vec![frame.into_geometry()];
}
// Guided layouts need the anchor; without it fall back to a cursor row.
match (self.guide, self.base_screen) {
- (DynGuide::None, _) | (_, None) => self.draw_row(&mut frame, bounds),
- (_, Some(base)) => self.draw_guided(&mut frame, bounds, base),
+ (DynGuide::None, _) | (_, None) => self.draw_row(&mut frame, bounds, theme),
+ (_, Some(base)) => self.draw_guided(&mut frame, bounds, base, theme),
}
vec![frame.into_geometry()]
}
diff --git a/src/ui/popup/cycle_popup.rs b/src/ui/popup/cycle_popup.rs
index 2f0f0f273..979a913c1 100644
--- a/src/ui/popup/cycle_popup.rs
+++ b/src/ui/popup/cycle_popup.rs
@@ -2,8 +2,8 @@
//! two or more overlapping objects. Each row names a candidate; clicking it
//! adds that object to the current selection. Clicking outside dismisses it.
-use iced::widget::{button, column, container, mouse_area, opaque, row, text, Space};
-use iced::{Background, Border, Color, Element, Fill, Length, Theme};
+use iced::widget::{button, column, container, mouse_area, opaque, text};
+use iced::{Element, Fill, Length};
use crate::app::Message;
@@ -19,41 +19,19 @@ pub fn cycle_popup_overlay(
.collect();
let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(container::bordered_box)
.width(Length::Fixed(150.0));
- let positioned = column![
- Space::new().height(Length::Fixed(anchor.y.max(0.0))),
- row![
- Space::new().width(Length::Fixed(anchor.x.max(0.0))),
- opaque(panel),
- ],
- ]
- .width(Fill)
- .height(Fill);
+ let positioned = crate::ui::pin_at(anchor, opaque(panel));
mouse_area(positioned).on_press(Message::CycleCancel).into()
}
fn item_row(handle: acadrust::Handle, label: String) -> Element<'static, Message> {
- let content = text(label).size(11).color(LABEL).align_y(iced::Center);
+ let content = text(label).size(11).align_y(iced::Center);
let btn = button(content)
.on_press(Message::CycleSelect(handle))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(button::subtle)
.width(Fill)
.padding([4, 10]);
// Highlight the underlying object while the cursor is over this row.
@@ -62,30 +40,3 @@ fn item_row(handle: acadrust::Handle, label: String) -> Element<'static, Message
.on_exit(Message::CycleHoverExit(handle))
.into()
}
-
-// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const PANEL_BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const PANEL_BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-const ROW_HOVER: Color = Color {
- r: 0.22,
- g: 0.45,
- b: 0.62,
- a: 1.0,
-};
-const LABEL: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
diff --git a/src/ui/popup/isolate_popup.rs b/src/ui/popup/isolate_popup.rs
index a112dad9b..de2200bfe 100644
--- a/src/ui/popup/isolate_popup.rs
+++ b/src/ui/popup/isolate_popup.rs
@@ -1,75 +1,51 @@
-//! Isolate action menu β Isolate / Hide / End Isolation. Opened from the
-//! ISO status pill, rendered as a floating overlay above the status bar
-//! (same pattern as the scale picker). The same actions are also offered
-//! in the viewport right-click menu.
+//! Isolate / Hide / End Isolation status menu.
-use iced::widget::{button, column, container, mouse_area, row, text};
-use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
+use iced::widget::{button, row, text};
+use iced::{Element, Fill};
use crate::app::Message;
+use crate::ui::statusbar::status_menu::Entry;
-/// Full-screen overlay: transparent click-catcher + action list pinned
-/// bottom-right, above the status bar.
-///
/// - `has_selection`: enables Isolate / Hide (they act on the selection).
/// - `isolation_active`: enables End Isolation (something is hidden).
-pub fn isolate_popup_overlay(
+pub fn menu_entries(
has_selection: bool,
isolation_active: bool,
- pill: Option,
- win: (f32, f32),
-) -> Element<'static, Message> {
- let rows = column![
- action_row(
+) -> Vec> {
+ vec![
+ action_entry(
"Isolate Objects",
has_selection,
Message::Command("ISOLATEOBJECTS".to_string()),
),
- action_row(
+ action_entry(
"Hide Objects",
has_selection,
Message::Command("HIDEOBJECTS".to_string()),
),
- action_row(
+ action_entry(
"End Isolation",
isolation_active,
Message::Command("UNISOLATEOBJECTS".to_string()),
),
- ];
+ ]
+}
- let panel = container(rows)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
- .width(Length::Fixed(160.0));
-
- let positioned = super::position_statusbar_popup(panel.into(), pill, win, 160.0, true);
-
- mouse_area(positioned)
- .on_press(Message::CloseIsolatePopup)
- .into()
+fn action_entry(label: &'static str, enabled: bool, msg: Message) -> Entry<'static> {
+ let row = action_row(label, enabled, msg);
+ if enabled {
+ Entry::close(row)
+ } else {
+ Entry::stay(row)
+ }
}
fn action_row(label: &'static str, enabled: bool, msg: Message) -> Element<'static, Message> {
- let lbl = text(label)
- .size(11)
- .color(if enabled { LABEL_ON } else { LABEL_OFF });
+ let lbl = text(label).size(11);
let content = row![lbl].align_y(iced::Center);
let mut btn = button(content)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (enabled, status) {
- (true, button::Status::Hovered) => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(button::subtle)
.width(Fill)
.padding([4, 12]);
if enabled {
@@ -77,36 +53,3 @@ fn action_row(label: &'static str, enabled: bool, msg: Message) -> Element<'stat
}
btn.into()
}
-
-// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const PANEL_BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const PANEL_BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-const ROW_HOVER: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
-};
-const LABEL_ON: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
-const LABEL_OFF: Color = Color {
- r: 0.5,
- g: 0.5,
- b: 0.5,
- a: 1.0,
-};
diff --git a/src/ui/popup/mod.rs b/src/ui/popup/mod.rs
index 8d7c7ea91..e5ff95d10 100644
--- a/src/ui/popup/mod.rs
+++ b/src/ui/popup/mod.rs
@@ -5,76 +5,3 @@ pub mod scale_popup;
pub mod selection_filter_popup;
pub mod snap_popup;
pub mod units_popup;
-
-use iced::widget::container;
-use iced::{Element, Fill, Padding, Rectangle};
-
-use crate::app::Message;
-
-/// Decide a status-bar popup's horizontal anchor and paddings from its pill's
-/// screen bounds. Returns `(align_right, horizontal_pad, bottom_pad)`.
-///
-/// The popup opens just above the pill. Horizontally it prefers `prefer_right`
-/// (right edge aligned to the pill's right, growing left) but flips to the other
-/// side when that direction would cross the window edge.
-pub fn popup_anchor(
- pill: Option,
- win: (f32, f32),
- popup_w: f32,
- prefer_right: bool,
-) -> (bool, f32, f32) {
- let Some(b) = pill else {
- // No recorded position yet β fall back to the bottom-right corner.
- return (prefer_right, 4.0, 27.0);
- };
- let bottom = (win.1 - b.y).max(0.0);
- let right_pad = (win.0 - (b.x + b.width)).max(4.0);
- let left_pad = b.x.max(4.0);
-
- if prefer_right {
- // Right-aligned popup grows left; flip left-aligned if it would pass the
- // left window edge.
- if (b.x + b.width) - popup_w < 2.0 {
- (false, left_pad, bottom)
- } else {
- (true, right_pad, bottom)
- }
- } else {
- // Left-aligned popup grows right; flip right-aligned if it would pass the
- // right window edge.
- if b.x + popup_w > win.0 - 2.0 {
- (true, right_pad, bottom)
- } else {
- (false, left_pad, bottom)
- }
- }
-}
-
-/// Wrap a status-bar popup `panel` in a full-window container positioned just
-/// above its pill, flipping horizontally to stay on screen.
-pub fn position_statusbar_popup<'a>(
- panel: Element<'a, Message>,
- pill: Option,
- win: (f32, f32),
- popup_w: f32,
- prefer_right: bool,
-) -> Element<'a, Message> {
- let (align_right, h_pad, bottom) = popup_anchor(pill, win, popup_w, prefer_right);
- let pad = Padding {
- top: 0.0,
- bottom,
- left: if align_right { 0.0 } else { h_pad },
- right: if align_right { h_pad } else { 0.0 },
- };
- let c = container(panel)
- .align_bottom(Fill)
- .width(Fill)
- .height(Fill)
- .padding(pad);
- let c = if align_right {
- c.align_right(Fill)
- } else {
- c.align_left(Fill)
- };
- c.into()
-}
diff --git a/src/ui/popup/polar_popup.rs b/src/ui/popup/polar_popup.rs
index 7ec7d1939..4a1fa26d5 100644
--- a/src/ui/popup/polar_popup.rs
+++ b/src/ui/popup/polar_popup.rs
@@ -1,12 +1,10 @@
-//! Polar-tracking angle picker β sets the polar increment used by POLAR
-//! tracking. Rendered as a floating overlay above the status bar, same pattern
-//! as the units / scale pickers. Offers the common AutoCAD-style increments
-//! plus a free-text field for any custom angle (#264).
+//! Polar-tracking angle status menu.
-use iced::widget::{button, column, container, mouse_area, row, text, text_input};
-use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
+use iced::widget::{button, container, row, text, text_input};
+use iced::{Element, Fill, Length};
use crate::app::Message;
+use crate::ui::statusbar::status_menu::Entry;
/// Angle increments offered in the picker, in degrees. Matches the common
/// drafting set and adds the fine 1Β° step requested in #264.
@@ -21,20 +19,15 @@ pub fn angle_label(deg: f32) -> String {
}
}
-/// Full-screen overlay: transparent click-catcher + angle list pinned
-/// bottom-right, above the status bar. `custom` is the live text of the
-/// free-entry field.
-pub fn polar_popup_overlay<'a>(
+pub fn menu_entries<'a>(
current: f32,
custom: &'a str,
- pill: Option,
- win: (f32, f32),
-) -> Element<'a, Message> {
- let mut rows: Vec> = PRESETS
+) -> Vec> {
+ let mut entries: Vec> = PRESETS
.iter()
.map(|°| {
let active = (current - deg).abs() < 1e-3;
- angle_row(deg, active)
+ Entry::close(angle_row(deg, active))
})
.collect();
@@ -48,91 +41,27 @@ pub fn polar_popup_overlay<'a>(
let custom_row = container(
row![
custom_field,
- text("Β°").size(11).color(LABEL_OFF),
+ text("Β°").size(11),
]
.spacing(4)
.align_y(iced::Center),
)
.padding([5, 10]);
- rows.push(custom_row.into());
-
- let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
- .width(Length::Fixed(120.0));
-
- let positioned = super::position_statusbar_popup(panel.into(), pill, win, 120.0, true);
-
- mouse_area(positioned)
- .on_press(Message::ClosePolarPopup)
- .into()
+ entries.push(Entry::stay(custom_row));
+ entries
}
fn angle_row<'a>(deg: f32, active: bool) -> Element<'a, Message> {
- let check = crate::ui::icons::check_cell(active, CHECK_COLOR);
+ let check = crate::ui::icons::themed_check_cell(active);
- let lbl = text(angle_label(deg))
- .size(11)
- .color(if active { LABEL_ON } else { LABEL_OFF });
+ let lbl = text(angle_label(deg)).size(11);
let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content)
.on_press(Message::SetPolarAngle(deg))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(button::subtle)
.width(Fill)
.padding([4, 10])
.into()
}
-
-// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const PANEL_BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const PANEL_BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-const ROW_HOVER: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
-};
-const CHECK_COLOR: Color = Color {
- r: 0.35,
- g: 0.75,
- b: 1.00,
- a: 1.0,
-};
-const LABEL_ON: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
-const LABEL_OFF: Color = Color {
- r: 0.65,
- g: 0.65,
- b: 0.65,
- a: 1.0,
-};
diff --git a/src/ui/popup/scale_popup.rs b/src/ui/popup/scale_popup.rs
index b0c6aeb41..33375ae4d 100644
--- a/src/ui/popup/scale_popup.rs
+++ b/src/ui/popup/scale_popup.rs
@@ -1,29 +1,24 @@
-//! Scale picker dropdown β annotation scale (model space) or viewport scale (paper space).
-//!
-//! Rendered as a floating overlay above the status bar, same pattern as snap_popup.
+//! Annotation / viewport scale status menu.
-use iced::widget::{button, column, container, mouse_area, row, text};
-use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
+use iced::widget::{button, row, text};
+use iced::{Element, Fill};
use crate::app::Message;
+use crate::ui::statusbar::status_menu::Entry;
-/// Full-screen overlay: transparent click-catcher + scale list panel pinned bottom-right.
-///
/// - `is_model`: true = model space (dispatches SetAnnotationScale), false = paper space (SetViewportScale).
/// - `current_anno_scale`: current annotation_scale from Scene (used to highlight active row in model space).
/// - `viewport_scale`: current effective vp scale, view_height-first (used to highlight in paper space).
/// - `file_scales`: scale list read from the drawing (`ACAD_SCALELIST`). Only
/// scales actually stored in the file are shown; the picker never injects
/// scales of its own.
-pub fn scale_popup_overlay(
+pub fn menu_entries(
is_model: bool,
current_anno_scale: f32,
viewport_scale: Option,
file_scales: Vec<(String, f32, f64)>,
- pill: Option,
- win: (f32, f32),
-) -> Element<'static, Message> {
- let mut rows: Vec> = file_scales
+) -> Vec> {
+ let mut entries: Vec> = file_scales
.into_iter()
.map(|(label, anno_scale, vp_scale)| {
let active = if is_model {
@@ -38,108 +33,36 @@ pub fn scale_popup_overlay(
} else {
Message::SetViewportScale(vp_scale)
};
- scale_row(label, active, msg)
+ Entry::close(scale_row(label, active, msg))
})
.collect();
- // Model space gets a "Manage scales..." row that opens the scale manager.
if is_model {
- rows.push(manage_row());
+ entries.push(Entry::close(manage_row()));
}
-
- let width = if is_model { 150.0 } else { 120.0 };
- let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
- .width(Length::Fixed(width));
-
- let positioned = super::position_statusbar_popup(panel.into(), pill, win, width, true);
-
- mouse_area(positioned)
- .on_press(Message::CloseScalePopup)
- .into()
+ entries
}
fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Message> {
- let check = crate::ui::icons::check_cell(active, CHECK_COLOR);
+ let check = crate::ui::icons::themed_check_cell(active);
- let lbl = text(label)
- .size(11)
- .color(if active { LABEL_ON } else { LABEL_OFF });
+ let lbl = text(label).size(11);
let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content)
.on_press(msg)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(button::subtle)
.width(Fill)
.padding([4, 10])
.into()
}
fn manage_row() -> Element<'static, Message> {
- button(text("Manage...").size(11).color(CHECK_COLOR))
+ button(text("Manage...").size(11))
.on_press(Message::ScaleManagerOpen)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(button::primary)
.width(Fill)
.padding([5, 10])
.into()
}
-
-// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const PANEL_BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const PANEL_BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-const ROW_HOVER: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
-};
-const CHECK_COLOR: Color = Color {
- r: 0.35,
- g: 0.75,
- b: 1.00,
- a: 1.0,
-};
-const LABEL_ON: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
-const LABEL_OFF: Color = Color {
- r: 0.65,
- g: 0.65,
- b: 0.65,
- a: 1.0,
-};
diff --git a/src/ui/popup/selection_filter_popup.rs b/src/ui/popup/selection_filter_popup.rs
index b86b616c5..085e66776 100644
--- a/src/ui/popup/selection_filter_popup.rs
+++ b/src/ui/popup/selection_filter_popup.rs
@@ -1,25 +1,21 @@
-//! Selection-filter type picker β choose which entity types are selectable.
+//! Selection-filter status menu β choose which entity types are selectable.
//! A checked row means that type can be picked; unchecking it excludes the
//! type from interactive selection. Opened from the FILTER status pill.
use rustc_hash::FxHashSet as HashSet;
-use iced::widget::{button, column, container, mouse_area, row, text};
-use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
+use iced::widget::{button, container, row, text};
+use iced::{Background, Element, Fill, Theme};
use crate::app::Message;
+use crate::ui::statusbar::status_menu::Entry;
-/// Full-screen overlay: transparent click-catcher + type list pinned
-/// bottom-right, above the status bar.
-///
/// - `types`: entity-type names present in the current layout.
/// - `excluded`: types currently filtered out (unchecked).
-pub fn selection_filter_popup_overlay(
+pub fn menu_entries(
types: Vec,
excluded: &HashSet,
- pill: Option,
- win: (f32, f32),
-) -> Element<'static, Message> {
+) -> Vec> {
// "Select All / Clear All" header, mirroring the OSNAP popup: Select All
// clears every exclusion, Clear All excludes every present type.
let has_types = !types.is_empty();
@@ -41,144 +37,68 @@ pub fn selection_filter_popup_overlay(
.padding([4u16, 8]);
let divider = container(iced::widget::Space::new().height(1))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(DIVIDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color,
+ )),
..Default::default()
})
.width(Fill)
.padding([0, 4]);
- let rows: Vec> = if types.is_empty() {
- vec![empty_row()]
+ let rows: Vec> = if types.is_empty() {
+ vec![Entry::stay(empty_row())]
} else {
types
.into_iter()
.map(|name| {
let included = !excluded.contains(&name);
- type_row(name, included)
+ Entry::stay(type_row(name, included))
})
.collect()
};
- let panel = container(column![header, divider, column(rows)])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
- .width(Length::Fixed(180.0));
-
- let positioned = super::position_statusbar_popup(panel.into(), pill, win, 180.0, true);
-
- mouse_area(positioned)
- .on_press(Message::CloseSelectionFilterPopup)
- .into()
+ let mut entries = vec![Entry::stay(header), Entry::stay(divider)];
+ entries.extend(rows);
+ entries
}
fn type_row(name: String, included: bool) -> Element<'static, Message> {
- let check = crate::ui::icons::check_cell(included, CHECK_COLOR);
+ let check = crate::ui::icons::themed_check_cell(included);
- let lbl = text(name.clone())
- .size(11)
- .color(if included { LABEL_ON } else { LABEL_OFF });
+ let lbl = text(name.clone()).size(11);
let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content)
.on_press(Message::ToggleSelectionFilterType(name))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(button::subtle)
.width(Fill)
.padding([4, 10])
.into()
}
fn empty_row() -> Element<'static, Message> {
- container(text("No objects").size(11).color(LABEL_OFF))
+ container(
+ text("No objects").size(11).style(|theme: &Theme| text::Style {
+ color: Some(
+ theme
+ .extended_palette()
+ .background
+ .base
+ .text
+ .scale_alpha(0.42),
+ ),
+ }),
+ )
.padding([4, 10])
.into()
}
fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> {
- let color = if enabled {
- Color { r: 0.70, g: 0.70, b: 0.70, a: 1.0 }
- } else {
- Color { r: 0.38, g: 0.38, b: 0.38, a: 1.0 }
- };
- let b = button(text(label).size(10).color(color));
+ let b = button(text(label).size(10));
let b = if enabled { b.on_press(msg) } else { b };
- b.style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => BTN_BG,
- })),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 2.0.into(),
- },
- ..Default::default()
- })
+ b.style(button::secondary)
.padding([3, 8])
.into()
}
-
-// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const PANEL_BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const PANEL_BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-const ROW_HOVER: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
-};
-const DIVIDER: Color = Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
-};
-const BTN_BG: Color = Color {
- r: 0.20,
- g: 0.20,
- b: 0.20,
- a: 1.0,
-};
-const CHECK_COLOR: Color = Color {
- r: 0.35,
- g: 0.75,
- b: 1.00,
- a: 1.0,
-};
-const LABEL_ON: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
-const LABEL_OFF: Color = Color {
- r: 0.6,
- g: 0.6,
- b: 0.6,
- a: 1.0,
-};
diff --git a/src/ui/popup/snap_popup.rs b/src/ui/popup/snap_popup.rs
index 37d7ff3b6..6259a9f4c 100644
--- a/src/ui/popup/snap_popup.rs
+++ b/src/ui/popup/snap_popup.rs
@@ -1,27 +1,16 @@
-//! OpenCADStudio-style OSNAP dropdown popup panel.
-//!
-//! Rendered as a floating overlay above the status bar. The popup is only
-//! inserted into the view stack when `snap_popup_open` is true.
+//! OpenCADStudio-style OSNAP status menu.
-use iced::widget::{button, column, container, mouse_area, row, text};
-use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
+use iced::widget::{button, container, row, text};
+use iced::{Background, Element, Fill, Length, Theme};
use crate::app::Message;
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
+use crate::ui::statusbar::status_menu::Entry;
-/// Returns a full-screen overlay element:
-/// - a transparent click-catcher that closes the popup on outside click
-/// - the popup panel pinned to the bottom-right (above the status bar)
-pub fn snap_popup_overlay<'a>(
- snapper: &'a Snapper,
- pill: Option,
- win: (f32, f32),
-) -> Element<'a, Message> {
- // ββ Panel content βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec> {
let all_on = snapper.all_on();
let none_on = snapper.none_on();
- // "Select All / Clear All" header row
let header = row![
header_btn("Select All", Message::SnapSelectAll, !all_on),
header_btn("Clear All", Message::SnapClearAll, !none_on),
@@ -31,61 +20,41 @@ pub fn snap_popup_overlay<'a>(
// Divider
let divider = container(iced::widget::Space::new().height(1))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(DIVIDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color,
+ )),
..Default::default()
})
.width(Fill)
.padding([0, 4]);
- // Snap mode rows
- let mut rows: Vec> = Vec::new();
+ let mut entries = vec![Entry::stay(header), Entry::stay(divider)];
for &(snap_type, _glyph, label) in ALL_SNAP_MODES {
- rows.push(snap_row(snap_type, label, snapper.is_on(snap_type)));
+ entries.push(Entry::stay(snap_row(
+ snap_type,
+ label,
+ snapper.is_on(snap_type),
+ )));
}
-
- let panel_content = column![header, divider, column(rows)];
-
- const PANEL_W: f32 = 210.0;
- let panel = container(panel_content)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
- .width(Length::Fixed(PANEL_W));
-
- // Anchored just above its pill, flipping horizontally to stay on screen.
- let positioned = super::position_statusbar_popup(panel.into(), pill, win, PANEL_W, true);
-
- // ββ Click-catcher: closes popup on any outside click ββββββββββββββββββ
- mouse_area(positioned)
- .on_press(Message::CloseSnapPopup)
- .into()
+ entries
}
// ββ Individual snap row βββββββββββββββββββββββββββββββββββββββββββββββββββ
fn snap_row<'a>(snap_type: SnapType, label: &'a str, active: bool) -> Element<'a, Message> {
- let checkmark = crate::ui::icons::check_cell(active, CHECK_COLOR);
+ let checkmark = crate::ui::icons::themed_check_cell(active);
// SVG marker (not a Unicode glyph) so the symbols render on the web build,
// whose bundled font lacks them and showed tofu boxes. (#138)
- let icon_el = container(crate::ui::icons::tinted::(
+ let icon_el = container(crate::ui::icons::themed_success::(
crate::ui::icons::osnap(snap_type),
13.0,
- ICON_COLOR,
))
.width(Length::Fixed(16.0))
.align_x(iced::Center);
- let label_el = text(label)
- .size(11)
- .color(if active { LABEL_ON } else { LABEL_OFF });
+ let label_el = text(label).size(11);
let content = row![checkmark, icon_el, label_el]
.spacing(4)
@@ -93,105 +62,16 @@ fn snap_row<'a>(snap_type: SnapType, label: &'a str, active: bool) -> Element<'a
button(content)
.on_press(Message::ToggleSnap(snap_type))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(button::subtle)
.width(Fill)
.padding([3, 8])
.into()
}
fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> {
- let b = button(text(label).size(10).color(if enabled {
- Color {
- r: 0.70,
- g: 0.70,
- b: 0.70,
- a: 1.0,
- }
- } else {
- Color {
- r: 0.38,
- g: 0.38,
- b: 0.38,
- a: 1.0,
- }
- }));
+ let b = button(text(label).size(10));
let b = if enabled { b.on_press(msg) } else { b };
- b.style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => BTN_BG,
- })),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 2.0.into(),
- },
- ..Default::default()
- })
+ b.style(button::secondary)
.padding([3, 8])
.into()
}
-
-// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const PANEL_BG: Color = Color {
- r: 0.16,
- g: 0.16,
- b: 0.16,
- a: 0.98,
-};
-const PANEL_BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-const DIVIDER: Color = Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
-};
-const ROW_HOVER: Color = Color {
- r: 0.24,
- g: 0.24,
- b: 0.24,
- a: 1.0,
-};
-const BTN_BG: Color = Color {
- r: 0.20,
- g: 0.20,
- b: 0.20,
- a: 1.0,
-};
-const CHECK_COLOR: Color = Color {
- r: 0.20,
- g: 0.75,
- b: 0.35,
- a: 1.0,
-}; // green β
-const ICON_COLOR: Color = Color {
- r: 0.25,
- g: 0.75,
- b: 0.45,
- a: 1.0,
-}; // green icon
-const LABEL_ON: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
-const LABEL_OFF: Color = Color {
- r: 0.52,
- g: 0.52,
- b: 0.52,
- a: 1.0,
-};
-
diff --git a/src/ui/popup/units_popup.rs b/src/ui/popup/units_popup.rs
index 17421569b..d8301824c 100644
--- a/src/ui/popup/units_popup.rs
+++ b/src/ui/popup/units_popup.rs
@@ -1,10 +1,10 @@
-//! Drawing-units picker β sets the INSUNITS header value. Rendered as a
-//! floating overlay above the status bar, same pattern as the scale picker.
+//! Drawing-units status menu.
-use iced::widget::{button, column, container, mouse_area, row, text};
-use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
+use iced::widget::{button, row, text};
+use iced::{Element, Fill};
use crate::app::Message;
+use crate::ui::statusbar::status_menu::Entry;
/// Units offered in the picker: (INSUNITS code, menu label).
const UNITS: &[(i16, &str)] = &[
@@ -35,95 +35,30 @@ pub fn unit_short(code: i16) -> &'static str {
}
}
-/// Full-screen overlay: transparent click-catcher + units list pinned
-/// bottom-right, above the status bar.
-pub fn units_popup_overlay(
- current: i16,
- pill: Option,
- win: (f32, f32),
-) -> Element<'static, Message> {
- let rows: Vec> = UNITS
+pub fn menu_entries(current: i16) -> Vec> {
+ UNITS
.iter()
- .map(|&(code, label)| unit_row(label, code == current, Message::SetDrawingUnits(code)))
- .collect();
-
- let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
+ .map(|&(code, label)| {
+ Entry::close(unit_row(
+ label,
+ code == current,
+ Message::SetDrawingUnits(code),
+ ))
})
- .width(Length::Fixed(140.0));
-
- let positioned = super::position_statusbar_popup(panel.into(), pill, win, 140.0, true);
-
- mouse_area(positioned)
- .on_press(Message::CloseUnitsPopup)
- .into()
+ .collect()
}
fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> {
- let check = crate::ui::icons::check_cell(active, CHECK_COLOR);
+ let check = crate::ui::icons::themed_check_cell(active);
- let lbl = text(label)
- .size(11)
- .color(if active { LABEL_ON } else { LABEL_OFF });
+ let lbl = text(label).size(11);
let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content)
.on_press(msg)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(button::subtle)
.width(Fill)
.padding([4, 10])
.into()
}
-
-// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const PANEL_BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const PANEL_BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-const ROW_HOVER: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
-};
-const CHECK_COLOR: Color = Color {
- r: 0.35,
- g: 0.75,
- b: 1.00,
- a: 1.0,
-};
-const LABEL_ON: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
-const LABEL_OFF: Color = Color {
- r: 0.65,
- g: 0.65,
- b: 0.65,
- a: 1.0,
-};
diff --git a/src/ui/properties.rs b/src/ui/properties.rs
index d5d49303f..dadfdfdaa 100644
--- a/src/ui/properties.rs
+++ b/src/ui/properties.rs
@@ -208,9 +208,11 @@ impl PropertiesPanel {
pub fn view(&self) -> Element<'_, Message> {
// ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- let header = container(text("Properties").size(12).color(Color::WHITE))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(HEADER_BG)),
+ let header = container(text("Properties").size(12))
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color,
+ )),
..Default::default()
})
.width(Length::Fill)
@@ -218,7 +220,10 @@ impl PropertiesPanel {
// ββ Title bar (entity type / "No Selection") βββββββββββββββββββββ
let title_content: Element<'_, Message> = if self.selection_groups.is_empty() {
- text(crate::ui::text_util::elide(&self.title, 34)).size(FONT_SZ).color(SECTION_LABEL).into()
+ text(crate::ui::text_util::elide(&self.title, 34))
+ .size(FONT_SZ)
+ .style(muted_text_style)
+ .into()
} else {
combo_box(
&self.selection_group_combo,
@@ -235,14 +240,17 @@ impl PropertiesPanel {
};
let title_bar = container(title_content)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(SECTION_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weakest.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
+ }
})
.width(Length::Fill)
.padding([4, 10]);
@@ -252,7 +260,7 @@ impl PropertiesPanel {
container(
text("Select an object to view properties")
.size(10)
- .color(HINT_COLOR),
+ .style(hint_text_style),
)
.padding([10, 10])
.into()
@@ -265,14 +273,17 @@ impl PropertiesPanel {
};
container(column![header, title_bar, content])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
+ }
})
.width(250)
.height(Length::Fill)
@@ -286,15 +297,22 @@ impl PropertiesPanel {
if self.sections.is_empty() {
return None;
}
- let title = container(text(crate::ui::text_util::elide(&self.title, 34)).size(FONT_SZ).color(SECTION_LABEL))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(SECTION_BG)),
+ let title = container(
+ text(crate::ui::text_util::elide(&self.title, 34))
+ .size(FONT_SZ)
+ .style(muted_text_style),
+ )
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weakest.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
+ }
})
.width(Length::Fill)
.padding([4, 10]);
@@ -306,14 +324,17 @@ impl PropertiesPanel {
Some(
container(col)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
+ }
})
.width(230)
.into(),
@@ -324,15 +345,18 @@ impl PropertiesPanel {
fn render_section<'a>(&'a self, section: &'a PropSection) -> Element<'a, Message> {
// Section header
- let hdr = container(text(§ion.title).size(10).color(Color::WHITE))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(SECTION_HDR_BG)),
+ let hdr = container(text(§ion.title).size(10))
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
+ }
})
.width(Length::Fill)
.padding([3, 8]);
@@ -508,31 +532,25 @@ impl PropertiesPanel {
fn render_color_varies_row<'a>(&'a self, label: &'a str) -> Element<'a, Message> {
let color_btn = button(
row![
- container(text("?").size(10).color(VALUE_COLOR))
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
- })),
+ container(text("?").size(10))
+ .style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.strong.color)),
border: Border {
- color: Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.5
- },
+ color: palette.background.neutral.color,
width: 1.0,
radius: 2.0.into()
},
+ text_color: Some(palette.background.strong.text),
..Default::default()
+ }
})
.width(SWATCH_SZ)
.height(SWATCH_SZ)
.align_x(iced::Center)
.align_y(iced::Center),
- text(VARIES_LABEL).size(FONT_SZ).color(VALUE_COLOR),
+ text(VARIES_LABEL).size(FONT_SZ),
]
.spacing(4)
.align_y(iced::Center),
@@ -724,7 +742,7 @@ impl PropertiesPanel {
.on_input(move |v| Message::PropGeomInput { field, value: v })
.on_submit(Message::PropGeomCommit(field))
.size(FONT_SZ)
- .style(|_: &Theme, status| text_input::Style {
+ .style(|theme: &Theme, status| text_input::Style {
// The wrapping container draws the border; keep the input flat
// so field + caret read as one control.
border: Border {
@@ -732,33 +750,32 @@ impl PropertiesPanel {
width: 0.0,
radius: 0.0.into(),
},
- ..text_input_style(&Theme::Dark, status)
+ ..text_input_style(theme, status)
})
.padding([3, 6])
.width(Length::Fill);
let caret = button(
- container(crate::ui::icons::arrow_toggle(
- self.edit_choice_open,
- FONT_SZ,
- VALUE_COLOR,
- ))
+ container(if self.edit_choice_open {
+ crate::ui::icons::themed_arrow_up(FONT_SZ)
+ } else {
+ crate::ui::icons::themed_arrow_down(FONT_SZ)
+ })
.height(Length::Fill)
.align_y(iced::Center),
)
.on_press(Message::PropEditChoiceToggle)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
- },
- _ => VALUE_BG,
- })),
- text_color: VALUE_COLOR,
+ .style(|theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match status {
+ button::Status::Hovered | button::Status::Pressed => palette.background.weak,
+ _ => palette.background.base,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border::default(),
..Default::default()
+ }
})
.padding(Padding {
top: 0.0,
@@ -768,14 +785,17 @@ impl PropertiesPanel {
})
.height(Length::Fixed(ROW_H - 6.0));
let head = container(row![input, caret].align_y(iced::Center))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(VALUE_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
+ }
})
.width(Length::Fill);
@@ -794,39 +814,15 @@ impl PropertiesPanel {
}
let value = opt.clone();
list = list.push(
- button(text(opt.as_str()).size(FONT_SZ).color(VALUE_COLOR))
+ button(text(opt.as_str()).size(FONT_SZ))
.on_press(Message::PropGeomChoiceChanged { field, value })
- .style(|_: &Theme, status| button::Style {
- background: matches!(status, button::Status::Hovered).then_some(
- Background::Color(Color {
- r: 0.25,
- g: 0.45,
- b: 0.70,
- a: 1.0,
- }),
- ),
- text_color: VALUE_COLOR,
- ..Default::default()
- })
+ .style(button::subtle)
.padding([2, 6])
.width(Length::Fill),
);
}
let popup = container(scrollable(list).height(Length::Shrink))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.17,
- g: 0.17,
- b: 0.17,
- a: 1.0,
- })),
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 2.0.into(),
- },
- ..Default::default()
- })
+ .style(container::bordered_box)
.padding(2)
.width(200)
.max_height(220.0);
@@ -940,13 +936,13 @@ pub fn color_picker_dropdown<'a>(
r.push(
button(text("").width(18).height(18))
.on_press(msg)
- .style(move |_: &Theme, status| button::Style {
+ .style(move |theme: &Theme, status| button::Style {
background: Some(Background::Color(bg)),
border: Border {
color: if matches!(status, button::Status::Hovered) {
- Color::WHITE
+ theme.extended_palette().primary.base.color
} else {
- Color::BLACK
+ theme.extended_palette().background.neutral.color
},
width: if matches!(status, button::Status::Hovered) {
1.5
@@ -955,6 +951,7 @@ pub fn color_picker_dropdown<'a>(
},
radius: 2.0.into(),
},
+ text_color: theme.extended_palette().background.base.text,
..Default::default()
})
.padding(0),
@@ -965,20 +962,20 @@ pub fn color_picker_dropdown<'a>(
// "More Colorsβ¦" toggle button
let more_btn = button(
row![
- crate::ui::icons::arrow_toggle(palette_open, 9.0, HINT_COLOR),
+ if palette_open {
+ crate::ui::icons::themed_arrow_up(9.0)
+ } else {
+ crate::ui::icons::themed_arrow_down(9.0)
+ },
text(if palette_open { "Less" } else { "More Colorsβ¦" })
.size(10)
- .color(HINT_COLOR),
+ .style(hint_text_style),
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(palette_toggle_msg)
- .style(|_: &Theme, _| button::Style {
- background: Some(Background::Color(PICKER_BG)),
- text_color: HINT_COLOR,
- ..Default::default()
- })
+ .style(button::subtle)
.padding([2, 6])
.width(Length::Fill);
@@ -989,14 +986,17 @@ pub fn color_picker_dropdown<'a>(
};
let mut col = column![container(inner)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PICKER_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into()
},
..Default::default()
+ }
})
.padding([6, 8])
.width(Length::Fill)]
@@ -1019,18 +1019,13 @@ pub fn color_picker_dropdown<'a>(
r = r.push(
button(text("").width(12).height(12))
.on_press(msg)
- .style(move |_: &Theme, status| button::Style {
+ .style(move |theme: &Theme, status| button::Style {
background: Some(Background::Color(bg)),
border: Border {
color: if matches!(status, button::Status::Hovered) {
- Color::WHITE
+ theme.extended_palette().primary.base.color
} else {
- Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.4,
- }
+ theme.extended_palette().background.neutral.color
},
width: if matches!(status, button::Status::Hovered) {
1.5
@@ -1039,6 +1034,7 @@ pub fn color_picker_dropdown<'a>(
},
radius: 1.0.into(),
},
+ text_color: theme.extended_palette().background.base.text,
..Default::default()
})
.padding(0),
@@ -1049,14 +1045,17 @@ pub fn color_picker_dropdown<'a>(
}
col = col.push(
container(scrollable(rows).height(160))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PICKER_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
+ }
})
.padding([4, 6])
.width(Length::Fill),
@@ -1071,22 +1070,23 @@ pub fn color_picker_dropdown<'a>(
/// A boolean toggle button row (for "Invisible" etc.).
fn render_stepper_row<'a>(label: &'a str, display: &'a str) -> Element<'a, Message> {
let arrow = |glyph: &'static str, delta: i8| {
- button(text(glyph).size(FONT_SZ).color(VALUE_COLOR))
+ button(text(glyph).size(FONT_SZ))
.on_press(Message::PropVertexStep(delta))
.padding([0, 6])
- .style(|_: &Theme, status| {
- let bg = match status {
- button::Status::Hovered | button::Status::Pressed => HOVER_BG,
- _ => VALUE_BG,
+ .style(|theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match status {
+ button::Status::Hovered | button::Status::Pressed => palette.background.weak,
+ _ => palette.background.base,
};
button::Style {
- background: Some(Background::Color(bg)),
+ background: Some(Background::Color(pair.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 2.0.into(),
},
- text_color: VALUE_COLOR,
+ text_color: pair.text,
..Default::default()
}
})
@@ -1095,7 +1095,6 @@ fn render_stepper_row<'a>(label: &'a str, display: &'a str) -> Element<'a, Messa
arrow("β", -1),
text(display)
.size(FONT_SZ)
- .color(VALUE_COLOR)
.width(Length::Fill)
.align_x(iced::Center),
arrow("βΆ", 1),
@@ -1111,22 +1110,29 @@ fn render_bool_row<'a>(label: &'a str, field: &'static str, value: bool) -> Elem
button(
text(btn_label)
.size(FONT_SZ)
- .color(if value { WARN_COLOR } else { VALUE_COLOR }),
+ .style(move |theme: &Theme| iced::widget::text::Style {
+ color: value.then_some(theme.extended_palette().warning.base.color),
+ }),
)
.on_press(Message::PropBoolToggle(field))
- .style(move |_: &Theme, status| {
- let bg = match status {
- button::Status::Hovered | button::Status::Pressed => HOVER_BG,
- _ => VALUE_BG,
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match status {
+ button::Status::Hovered | button::Status::Pressed => palette.background.weak,
+ _ => palette.background.base,
};
button::Style {
- background: Some(Background::Color(bg)),
+ background: Some(Background::Color(pair.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 2.0.into(),
},
- text_color: if value { WARN_COLOR } else { VALUE_COLOR },
+ text_color: if value {
+ palette.warning.base.color
+ } else {
+ pair.text
+ },
..Default::default()
}
})
@@ -1211,10 +1217,14 @@ fn render_group_row(
let label_btn = button(
container(
row![
- crate::ui::icons::arrow_toggle(expanded, FONT_SZ, LABEL_COLOR),
+ if expanded {
+ crate::ui::icons::themed_arrow_down(FONT_SZ)
+ } else {
+ crate::ui::icons::themed_arrow_right(FONT_SZ)
+ },
text(crate::ui::text_util::elide(base, 16))
.size(FONT_SZ)
- .color(LABEL_COLOR),
+ .style(muted_text_style),
]
.spacing(4)
.align_y(iced::Center),
@@ -1223,20 +1233,7 @@ fn render_group_row(
.align_y(iced::Center),
)
.on_press(Message::PropGroupToggle(key))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.24,
- g: 0.24,
- b: 0.24,
- a: 1.0,
- },
- _ => LABEL_BG,
- })),
- text_color: LABEL_COLOR,
- border: Border::default(),
- ..Default::default()
- })
+ .style(button::subtle)
.padding(Padding {
top: 0.0,
bottom: 0.0,
@@ -1246,8 +1243,10 @@ fn render_group_row(
.width(Length::Fill)
.height(Length::Fixed(ROW_H));
let label_col = container(label_btn)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LABEL_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weakest.color,
+ )),
..Default::default()
})
.width(Length::FillPortion(5))
@@ -1262,8 +1261,10 @@ fn render_group_row(
.padding([3, 6])
.width(Length::Fill);
let value_col = container(value_field)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(VALUE_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.width(Length::FillPortion(6))
@@ -1278,9 +1279,9 @@ fn render_group_row(
container(row![label_col, value_col])
.height(Length::Fixed(ROW_H))
- .style(|_: &Theme| container::Style {
+ .style(|theme: &Theme| container::Style {
border: Border {
- color: BORDER,
+ color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@@ -1305,9 +1306,15 @@ fn render_ro_row<'a>(label: &'a str, value: &'a str) -> Element<'a, Message> {
/// Build a label | widget property row.
fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> {
- let label_col = container(text(crate::ui::text_util::elide(label, 18)).size(FONT_SZ).color(LABEL_COLOR))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LABEL_BG)),
+ let label_col = container(
+ text(crate::ui::text_util::elide(label, 18))
+ .size(FONT_SZ)
+ .style(muted_text_style),
+ )
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weakest.color,
+ )),
..Default::default()
})
.width(Length::FillPortion(5))
@@ -1320,8 +1327,10 @@ fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<
right: 6.0,
});
let value_col = container(widget)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(VALUE_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.width(Length::FillPortion(6))
@@ -1335,9 +1344,9 @@ fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<
});
container(row![label_col, value_col])
.height(Length::Fixed(ROW_H))
- .style(|_: &Theme| container::Style {
+ .style(|theme: &Theme| container::Style {
border: Border {
- color: BORDER,
+ color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@@ -1348,18 +1357,9 @@ fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<
/// A plain text button used inside the color picker for ByLayer / ByBlock.
fn picker_text_btn(label: &str, msg: Message) -> Element<'_, Message> {
- button(text(label).size(FONT_SZ).color(VALUE_COLOR))
+ button(text(label).size(FONT_SZ))
.on_press(msg)
- .style(|_: &Theme, _| button::Style {
- background: Some(Background::Color(LABEL_BG)),
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 2.0.into(),
- },
- text_color: VALUE_COLOR,
- ..Default::default()
- })
+ .style(button::secondary)
.padding([2, 8])
.into()
}
@@ -1419,49 +1419,41 @@ fn aci_label(idx: u8) -> &'static str {
// ββ Widget style helpers ββββββββββββββββββββββββββββββββββββββββββββββββββ
-fn combo_btn_style(_theme: &Theme, status: button::Status) -> button::Style {
- let bg = match status {
- button::Status::Hovered | button::Status::Pressed => HOVER_BG,
- _ => VALUE_BG,
+fn combo_btn_style(theme: &Theme, status: button::Status) -> button::Style {
+ let palette = theme.extended_palette();
+ let pair = match status {
+ button::Status::Hovered | button::Status::Pressed => palette.background.weak,
+ _ => palette.background.base,
};
button::Style {
- background: Some(Background::Color(bg)),
+ background: Some(Background::Color(pair.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 2.0.into(),
},
- text_color: VALUE_COLOR,
+ text_color: pair.text,
..Default::default()
}
}
-fn text_input_style(_theme: &Theme, status: text_input::Status) -> text_input::Style {
+fn text_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
let border_color = match status {
- text_input::Status::Focused { .. } => Color {
- r: 0.3,
- g: 0.6,
- b: 1.0,
- a: 1.0,
- },
- _ => BORDER,
+ text_input::Status::Focused { .. } => palette.primary.base.color,
+ _ => palette.background.neutral.color,
};
text_input::Style {
- background: Background::Color(VALUE_BG),
+ background: Background::Color(palette.background.base.color),
border: Border {
color: border_color,
width: 1.0,
radius: 2.0.into(),
},
icon: Color::TRANSPARENT,
- placeholder: HINT_COLOR,
- value: VALUE_COLOR,
- selection: Color {
- r: 0.2,
- g: 0.4,
- b: 0.8,
- a: 0.5,
- },
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
}
}
@@ -1472,109 +1464,30 @@ fn combo_input_style(theme: &Theme, status: text_input::Status) -> text_input::S
/// Style for a read-only-but-selectable value field: flat (no input box or
/// focus highlight, so it reads as plain text, unlike the bordered editable
/// fields) yet with a visible selection colour so Ctrl+C copy is discoverable.
-fn ro_input_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
+fn ro_input_style(theme: &Theme, _status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
text_input::Style {
- background: Background::Color(VALUE_BG),
+ background: Background::Color(palette.background.base.color),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
icon: Color::TRANSPARENT,
- placeholder: HINT_COLOR,
- value: VALUE_COLOR,
- selection: Color {
- r: 0.2,
- g: 0.4,
- b: 0.8,
- a: 0.5,
- },
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
}
}
-// ββ Colour constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+fn muted_text_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.72)),
+ }
+}
-const PANEL_BG: Color = Color {
- r: 0.19,
- g: 0.19,
- b: 0.19,
- a: 1.0,
-};
-const HEADER_BG: Color = Color {
- r: 0.24,
- g: 0.24,
- b: 0.24,
- a: 1.0,
-};
-const SECTION_BG: Color = Color {
- r: 0.21,
- g: 0.21,
- b: 0.21,
- a: 1.0,
-};
-const SECTION_HDR_BG: Color = Color {
- r: 0.26,
- g: 0.26,
- b: 0.28,
- a: 1.0,
-};
-const LABEL_BG: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
-};
-const VALUE_BG: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.18,
- a: 1.0,
-};
-const HOVER_BG: Color = Color {
- r: 0.25,
- g: 0.25,
- b: 0.28,
- a: 1.0,
-};
-const PICKER_BG: Color = Color {
- r: 0.16,
- g: 0.16,
- b: 0.18,
- a: 1.0,
-};
-const LABEL_COLOR: Color = Color {
- r: 0.70,
- g: 0.70,
- b: 0.70,
- a: 1.0,
-};
-const VALUE_COLOR: Color = Color {
- r: 0.90,
- g: 0.90,
- b: 0.90,
- a: 1.0,
-};
-const HINT_COLOR: Color = Color {
- r: 0.45,
- g: 0.45,
- b: 0.50,
- a: 1.0,
-};
-const SECTION_LABEL: Color = Color {
- r: 0.75,
- g: 0.75,
- b: 0.75,
- a: 1.0,
-};
-const BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-const WARN_COLOR: Color = Color {
- r: 1.00,
- g: 0.60,
- b: 0.10,
- a: 1.0,
-};
+fn hint_text_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.48)),
+ }
+}
diff --git a/src/ui/ribbon/collapse.rs b/src/ui/ribbon/collapse.rs
index 6f753c342..c7c193e24 100644
--- a/src/ui/ribbon/collapse.rs
+++ b/src/ui/ribbon/collapse.rs
@@ -15,7 +15,7 @@ use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, Widget};
use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell};
use iced::{
- Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
+ Background, Border, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
Theme, Vector,
};
@@ -113,8 +113,6 @@ pub struct CollapsePanels<'a> {
open: Option,
/// Fallback row height, used only when there are no panels to measure.
row_h: f32,
- /// Colour of the 1px divider drawn between panels.
- divider: Color,
/// Chosen degradation level per panel; set during layout.
levels: RefCell>,
/// If set, the measured row height is written here each layout (read when
@@ -129,13 +127,12 @@ pub struct CollapsePanels<'a> {
}
impl<'a> CollapsePanels<'a> {
- pub fn new(panels: Vec>, open: Option, row_h: f32, divider: Color) -> Self {
+ pub fn new(panels: Vec>, open: Option, row_h: f32) -> Self {
let n = panels.len();
Self {
panels,
open,
row_h,
- divider,
levels: RefCell::new(vec![FULL; n]),
height_out: None,
tight_out: None,
@@ -480,7 +477,7 @@ impl<'a> Widget for CollapsePanels<'a> {
shadow: Shadow::default(),
snap: true,
},
- Background::Color(self.divider),
+ Background::Color(theme.extended_palette().background.neutral.color),
);
}
}
diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs
index 22a44c3eb..85190521e 100644
--- a/src/ui/ribbon/mod.rs
+++ b/src/ui/ribbon/mod.rs
@@ -12,7 +12,7 @@ use std::sync::Arc;
use rustc_hash::FxHashMap as HashMap;
use acadrust::types::{Color as AcadColor, LineWeight};
-use iced::widget::{button, column, container, mouse_area, row, scrollable, svg, text};
+use iced::widget::{button, column, container, mouse_area, row, scrollable, text};
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
use crate::app::Message;
@@ -419,58 +419,41 @@ impl Ribbon {
let is_active = i == self.active;
let is_contextual = module.id() == "layout";
- let accent = if is_contextual {
- ACCENT_GOLD
- } else {
- ACCENT_BLUE
- };
- let text_inactive = if is_contextual {
- Color {
- r: 0.90,
- g: 0.72,
- b: 0.30,
- a: 1.0,
- }
- } else {
- Color {
- r: 0.75,
- g: 0.75,
- b: 0.75,
- a: 1.0,
- }
- };
- let hover_bg = if is_contextual {
- Color {
- r: 0.28,
- g: 0.24,
- b: 0.12,
- a: 1.0,
- }
- } else {
- Color {
- r: 0.25,
- g: 0.25,
- b: 0.25,
- a: 1.0,
- }
- };
let btn = container(
button(text(module.title()).size(12))
.on_press(Message::RibbonSelectTab(i))
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (is_active, status) {
- (true, _) => RIBBON_BG,
- (false, button::Status::Hovered) => hover_bg,
- _ => Color::TRANSPARENT,
- })),
- text_color: if is_active {
- Color::WHITE
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let accent = if is_contextual {
+ palette.warning.base
} else {
- text_inactive
+ palette.primary.base
+ };
+ let pair = match (is_active, status) {
+ (true, _) => palette.background.weakest,
+ (false, button::Status::Hovered) => {
+ if is_contextual {
+ palette.warning.weak
+ } else {
+ palette.background.weak
+ }
+ }
+ _ => palette.background.base,
+ };
+ button::Style {
+ background: (is_active
+ || matches!(status, button::Status::Hovered))
+ .then_some(Background::Color(pair.color)),
+ text_color: if is_active {
+ pair.text
+ } else if is_contextual {
+ accent.color
+ } else {
+ palette.background.base.text.scale_alpha(0.72)
},
border: Border {
color: if is_active {
- accent
+ accent.color
} else {
Color::TRANSPARENT
},
@@ -479,13 +462,18 @@ impl Ribbon {
},
shadow: iced::Shadow::default(),
snap: false,
+ }
})
.padding([5, 14]),
)
- .style(move |_: &Theme| container::Style {
+ .style(move |theme: &Theme| container::Style {
border: Border {
color: if is_active {
- accent
+ if is_contextual {
+ theme.extended_palette().warning.base.color
+ } else {
+ theme.extended_palette().primary.base.color
+ }
} else {
Color::TRANSPARENT
},
@@ -514,9 +502,11 @@ impl Ribbon {
// persisted (see `Ribbon::set_collapse_mode`). It hides itself once the
// tool row is tight, giving the cramped tab row its space back.
let dd_open = self.open_dropdown.as_deref() == Some(COLLAPSE_MODE_ID);
- let mode_btn = button(crate::ui::icons::arrow_down(10.0, ARROW_COLOR))
+ let mode_btn = button(crate::ui::icons::themed_arrow_down(10.0))
.on_press(Message::ToggleRibbonDropdown(COLLAPSE_MODE_ID.to_string()))
- .style(move |_: &Theme, status| top_hist_btn_style(true, dd_open, status))
+ .style(move |theme: &Theme, status| {
+ top_hist_btn_style(theme, true, dd_open, status)
+ })
.height(24)
.padding([2, 8]);
let mode_dd = PosReport::new(COLLAPSE_MODE_ID, mode_btn);
@@ -534,8 +524,10 @@ impl Ribbon {
}
let tab_bar = container(tab_row)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TOPBAR_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.padding(Padding {
@@ -653,10 +645,12 @@ impl Ribbon {
self.active_lineweight,
&style_ctx,
))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(RIBBON_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weakest.color,
+ )),
border: Border {
- color: BORDER_DARK,
+ color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@@ -666,7 +660,7 @@ impl Ribbon {
}
})
.collect();
- CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H, BORDER_DARK)
+ CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
.report_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone())
.mode(self.collapse_mode)
@@ -676,10 +670,12 @@ impl Ribbon {
};
let tool_bar = container(tool_area)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(RIBBON_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weakest.color,
+ )),
border: Border {
- color: BORDER_DARK,
+ color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@@ -712,19 +708,13 @@ impl Ribbon {
.enumerate()
.map(|(idx, label)| {
let step = idx + 1;
- button(text(label.clone()).size(11).color(LABEL_ON))
+ button(text(label.clone()).size(11))
.on_press(if is_undo {
Message::UndoMany(step)
} else {
Message::RedoMany(step)
})
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(popup_row_style)
.width(Fill)
.padding([5, 10])
.into()
@@ -732,15 +722,7 @@ impl Ribbon {
.collect();
let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(popup_panel_style)
.width(Length::Fixed(170.0));
let (align_right, h_pad, top) = self.dd_anchor(open_id, 170.0, win.0);
@@ -755,27 +737,16 @@ impl Ribbon {
let rows: Vec> = CollapseMode::ALL
.iter()
.map(|&m| {
- let mark: Element = if m == current {
- crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
- } else {
- iced::widget::Space::new().width(0).into()
- };
button(
row![
- container(mark).width(Length::Fixed(16.0)),
- text(m.label()).size(11).color(LABEL_ON),
+ crate::ui::icons::themed_check_cell(m == current),
+ text(m.label()).size(11),
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::SetRibbonCollapseMode(m))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(popup_row_style)
.width(Fill)
.padding([5, 10])
.into()
@@ -783,15 +754,7 @@ impl Ribbon {
.collect();
let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(popup_panel_style)
.width(Length::Fixed(W));
let (align_right, h_pad, top) = self.dd_anchor(open_id, W, win.0);
@@ -852,28 +815,25 @@ impl Ribbon {
.iter()
.map(|(cmd, label, item_icon)| {
let is_current = *cmd == last_cmd;
- let checkmark: Element<'_, Message> = container(if is_current {
- crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
- } else {
- iced::widget::Space::new().width(0).into()
- })
- .width(Length::Fixed(14.0))
- .into();
- let icon_el: Element = match *item_icon {
- IconKind::Glyph(s) => text(s)
- .size(13)
- .color(ICON_COLOR)
+ let checkmark: Element<'_, Message> =
+ crate::ui::icons::themed_check_cell(is_current);
+ let icon_el: Element =
+ container(make_icon(*item_icon, 20.0))
.width(Length::Fixed(20.0))
- .into(),
- IconKind::Svg(bytes) => {
- let handle = svg::Handle::from_memory(bytes);
- svg(handle).width(20).height(20).into()
- }
- };
+ .into();
let label_el =
text(*label)
.size(11)
- .color(if is_current { LABEL_ON } else { LABEL_OFF });
+ .style(move |theme: &Theme| iced::widget::text::Style {
+ color: (!is_current).then_some(
+ theme
+ .extended_palette()
+ .background
+ .base
+ .text
+ .scale_alpha(0.72),
+ ),
+ });
button(
row![checkmark, icon_el, label_el]
@@ -884,13 +844,7 @@ impl Ribbon {
dropdown_id: dd_id,
cmd: *cmd,
})
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into()
@@ -898,15 +852,7 @@ impl Ribbon {
.collect();
let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(popup_panel_style)
.width(Length::Fixed(190.0));
let (align_right, h_pad, top) = self.dd_anchor(open_id, 190.0, win.0);
@@ -923,15 +869,9 @@ impl Ribbon {
// on it flips that state instead of bubbling up to the row's
// make-active handler (#133).
let icon_btn = |bytes: &'static [u8], msg: Message| -> Element<'_, Message> {
- button(crate::ui::icons::raw(bytes, 14.0))
+ button(crate::ui::icons::semantic(bytes, 14.0))
.on_press(msg)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(popup_row_style)
.padding([2, 4])
.into()
};
@@ -952,10 +892,10 @@ impl Ribbon {
let name = info.name.clone();
let swatch = container(text(""))
- .style(move |_: &Theme| container::Style {
+ .style(move |theme: &Theme| container::Style {
background: Some(Background::Color(lc)),
border: Border {
- color: SWATCH_BORDER,
+ color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 1.0.into(),
},
@@ -976,29 +916,27 @@ impl Ribbon {
crate::ui::icons::layer_lock(ll),
Message::LayerToggleLock(index),
);
- let checkmark: Element<'_, Message> = container(if is_active {
- 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 checkmark: Element<'_, Message> =
+ crate::ui::icons::themed_check_cell(is_active);
let label =
text(&info.name)
.size(11)
- .color(if is_active { LABEL_ON } else { LABEL_OFF });
+ .style(move |theme: &Theme| iced::widget::text::Style {
+ color: (!is_active).then_some(
+ theme
+ .extended_palette()
+ .background
+ .base
+ .text
+ .scale_alpha(0.72),
+ ),
+ });
// The swatch + label area selects the layer as active; the
// icon buttons above handle their own toggles.
let select = button(row![swatch, label].spacing(5).align_y(iced::Center))
.on_press(Message::RibbonLayerChanged(name))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(popup_row_style)
.width(Fill)
.padding([4, 4]);
@@ -1021,34 +959,7 @@ impl Ribbon {
let search = iced::widget::text_input("Search layersβ¦", &self.layer_filter)
.on_input(Message::RibbonLayerFilterChanged)
.size(11)
- .padding([4, 6])
- .style(|_: &Theme, _| iced::widget::text_input::Style {
- background: Background::Color(Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 1.0,
- }),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 2.0.into(),
- },
- icon: Color::WHITE,
- placeholder: Color {
- r: 0.45,
- g: 0.45,
- b: 0.45,
- a: 1.0,
- },
- value: Color::WHITE,
- selection: Color {
- r: 0.20,
- g: 0.44,
- b: 0.72,
- a: 0.5,
- },
- });
+ .padding([4, 6]);
let panel = container(
column![
container(search).padding([4, 4]),
@@ -1056,15 +967,7 @@ impl Ribbon {
]
.spacing(2),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(popup_panel_style)
.width(Length::Fixed(220.0));
let (align_right, h_pad, top) = self.dd_anchor(LAYER_COMBO_ID, 220.0, win.0);
@@ -1113,35 +1016,29 @@ impl Ribbon {
};
let active = ctx.active_for(style_key).to_string();
- let row_style = |_: &Theme, status: button::Status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- };
-
let mut rows: Vec> = ctx
.names_for(style_key)
.iter()
.map(|name| {
let is_sel = name.as_str() == active.as_str();
let n = name.clone();
- let checkmark: Element = container(if is_sel {
- 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 checkmark: Element =
+ crate::ui::icons::themed_check_cell(is_sel);
button(
row![
checkmark,
- text(name.clone()).size(11).color(if is_sel {
- LABEL_ON
- } else {
- LABEL_OFF
- }),
+ text(name.clone())
+ .size(11)
+ .style(move |theme: &Theme| iced::widget::text::Style {
+ color: (!is_sel).then_some(
+ theme
+ .extended_palette()
+ .background
+ .base
+ .text
+ .scale_alpha(0.72),
+ ),
+ }),
]
.spacing(4)
.align_y(iced::Center),
@@ -1150,7 +1047,7 @@ impl Ribbon {
key: style_key,
name: n,
})
- .style(row_style)
+ .style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into()
@@ -1159,9 +1056,9 @@ impl Ribbon {
if let Some(mgr) = manager_cmd {
rows.push(
- button(text("Manageβ¦").size(11).color(LABEL_ON))
+ button(text("Manageβ¦").size(11))
.on_press(Message::Command(mgr.to_string()))
- .style(row_style)
+ .style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into(),
@@ -1169,15 +1066,7 @@ impl Ribbon {
}
let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(popup_panel_style)
.width(Length::Fixed(LARGE_W * 2.3));
let (align_right, h_pad, top) = self.dd_anchor(open_id, LARGE_W * 2.3, win.0);
@@ -1200,15 +1089,7 @@ impl Ribbon {
);
let panel = container(picker)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(popup_panel_style)
.width(Length::Fixed(200.0));
let (align_right, h_pad, top) = self.dd_anchor(PROP_COLOR_ID, 200.0, win.0);
@@ -1243,23 +1124,22 @@ impl Ribbon {
.into_iter()
.map(|lt| {
let is_cur = lt.name == *active_lt;
- 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 check: Element<'_, Message> =
+ crate::ui::icons::themed_check_cell(is_cur);
let name_col = text(lt.name.clone())
.size(11)
- .color(if is_cur { LABEL_ON } else { LABEL_OFF })
+ .style(move |theme: &Theme| iced::widget::text::Style {
+ color: (!is_cur).then_some(
+ theme
+ .extended_palette()
+ .background
+ .base
+ .text
+ .scale_alpha(0.72),
+ ),
+ })
.width(Length::Fixed(90.0));
- let art_col = text(lt.art.clone()).size(9).color(Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
- });
+ let art_col = text(lt.art.clone()).size(9).style(muted_text_style);
let name = lt.name.clone();
button(
row![check, name_col, art_col]
@@ -1267,13 +1147,7 @@ impl Ribbon {
.align_y(iced::Center),
)
.on_press(Message::RibbonLinetypeChanged(name))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(popup_row_style)
.width(Fill)
.padding([4, 6])
.into()
@@ -1281,15 +1155,7 @@ impl Ribbon {
.collect();
let list = container(scrollable(column(rows)).height(Length::Fixed(200.0)))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(popup_panel_style)
.width(Length::Fixed(220.0));
let (align_right, h_pad, top) = self.dd_anchor(PROP_LINETYPE_ID, 220.0, win.0);
@@ -1308,31 +1174,29 @@ impl Ribbon {
.map(|item| {
let is_cur = item.0 == active_lw;
let label = item.to_string();
- 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 check: Element<'_, Message> =
+ crate::ui::icons::themed_check_cell(is_cur);
button(
row![
check,
text(label)
.size(11)
- .color(if is_cur { LABEL_ON } else { LABEL_OFF })
+ .style(move |theme: &Theme| iced::widget::text::Style {
+ color: (!is_cur).then_some(
+ theme
+ .extended_palette()
+ .background
+ .base
+ .text
+ .scale_alpha(0.72),
+ ),
+ })
]
.spacing(5)
.align_y(iced::Center),
)
.on_press(Message::RibbonLineweightChanged(item.0))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(popup_row_style)
.width(Fill)
.padding([4, 8])
.into()
@@ -1350,15 +1214,7 @@ impl Ribbon {
win: (f32, f32),
) -> Option> {
let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ .style(popup_panel_style)
.width(Length::Fixed(width));
let (align_right, h_pad, top) = self.dd_anchor(dd_id, width, win.0);
@@ -1439,7 +1295,7 @@ fn render_group<'a>(
column![
tools_el,
- container(text(group.title).size(9).color(GROUP_LABEL)).padding([1, 4]),
+ container(text(group.title).size(9).style(muted_text_style)).padding([1, 4]),
]
.align_x(iced::Center)
.spacing(0)
@@ -1526,8 +1382,8 @@ fn collapse_button<'a>(
column![
icon,
row![
- text(title.to_string()).size(9).color(GROUP_LABEL),
- crate::ui::icons::arrow_down(8.0, GROUP_LABEL),
+ text(title.to_string()).size(9).style(muted_text_style),
+ crate::ui::icons::themed_secondary_arrow_down(8.0),
]
.spacing(3)
.align_y(iced::Center),
@@ -1536,22 +1392,7 @@ fn collapse_button<'a>(
.spacing(2),
)
.on_press(Message::ToggleRibbonPanel(title.to_string()))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => Color {
- r: 0.25,
- g: 0.25,
- b: 0.25,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- border: Border {
- radius: 2.0.into(),
- ..Default::default()
- },
- ..Default::default()
- })
+ .style(button::subtle)
.padding([3, 5])
.into();
}
@@ -1594,29 +1435,14 @@ fn collapse_button<'a>(
let opener = button(
row![
- text(title.to_string()).size(9).color(GROUP_LABEL),
- crate::ui::icons::arrow_down(8.0, GROUP_LABEL),
+ text(title.to_string()).size(9).style(muted_text_style),
+ crate::ui::icons::themed_secondary_arrow_down(8.0),
]
.spacing(3)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonPanel(title.to_string()))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => Color {
- r: 0.25,
- g: 0.25,
- b: 0.25,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- border: Border {
- radius: 2.0.into(),
- ..Default::default()
- },
- ..Default::default()
- })
+ .style(button::subtle)
.padding([1, 4]);
// The large face fills a fixed slot so a collapsed panel is shorter than a full
diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs
index f555e6795..d6b3b0dd7 100644
--- a/src/ui/ribbon/widgets.rs
+++ b/src/ui/ribbon/widgets.rs
@@ -8,7 +8,7 @@ use acadrust::types::{Color as AcadColor, LineWeight};
// Ribbon tooltips anchor to the right of their button so the cursor β which
// rests on the button itself β never covers the tip text. (#143)
use iced::widget::tooltip::Position as TipPos;
-use iced::widget::{button, column, container, row, svg, text, tooltip};
+use iced::widget::{button, column, container, row, text, tooltip};
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
use crate::app::Message;
@@ -77,175 +77,6 @@ pub(super) const PROP_COLOR_ID: &str = "PROP_COLOR";
pub(super) const PROP_LINETYPE_ID: &str = "PROP_LINETYPE";
pub(super) const PROP_LW_ID: &str = "PROP_LW";
-// ββ Colours ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-/// Light chrome grey for the quick-access file-command icons on the top strip.
-pub(super) const QA_ICON_COLOR: Color = Color {
- r: 0.82,
- g: 0.83,
- b: 0.85,
- a: 1.0,
-};
-pub(super) const TOPBAR_BG: Color = Color {
- r: 0.17,
- g: 0.17,
- b: 0.17,
- a: 1.0,
-};
-pub(super) const RIBBON_BG: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
-};
-pub(super) const BORDER_DARK: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
-pub(super) const ACCENT_BLUE: Color = Color {
- r: 0.20,
- g: 0.55,
- b: 0.90,
- a: 1.0,
-};
-pub(super) const ACCENT_GOLD: Color = Color {
- r: 0.90,
- g: 0.65,
- b: 0.10,
- a: 1.0,
-};
-pub(super) const LABEL_COLOR: Color = Color {
- r: 0.82,
- g: 0.82,
- b: 0.82,
- a: 1.0,
-};
-pub(super) const GROUP_LABEL: Color = Color {
- r: 0.50,
- g: 0.50,
- b: 0.50,
- a: 1.0,
-};
-pub(super) const TOOL_HOVER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-pub(super) const TOOL_ACTIVE: Color = Color {
- r: 0.18,
- g: 0.42,
- b: 0.70,
- a: 1.0,
-};
-pub(super) const ARROW_COLOR: Color = Color {
- r: 0.65,
- g: 0.65,
- b: 0.65,
- a: 1.0,
-};
-pub(super) const PANEL_BG: Color = Color {
- r: 0.16,
- g: 0.16,
- b: 0.16,
- a: 0.98,
-};
-pub(super) const PANEL_BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-pub(super) const ROW_HOVER: Color = Color {
- r: 0.24,
- g: 0.24,
- b: 0.24,
- a: 1.0,
-};
-pub(super) const CHECK_COLOR: Color = Color {
- r: 0.20,
- g: 0.75,
- b: 0.35,
- a: 1.0,
-};
-pub(super) const ICON_COLOR: Color = Color {
- r: 0.25,
- g: 0.75,
- b: 0.45,
- a: 1.0,
-};
-pub(super) const LABEL_ON: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
-pub(super) const LABEL_OFF: Color = Color {
- r: 0.72,
- g: 0.72,
- b: 0.72,
- a: 1.0,
-};
-
-// ββ Combo / dropdown colors βββββββββββββββββββββββββββββββββββββββββββββββ
-
-pub(super) const COMBO_BG: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.18,
- a: 1.0,
-};
-pub(super) const COMBO_HOVER_BG: Color = Color {
- r: 0.26,
- g: 0.26,
- b: 0.26,
- a: 1.0,
-};
-pub(super) const COMBO_OPEN_BG: Color = Color {
- r: 0.14,
- g: 0.14,
- b: 0.14,
- a: 1.0,
-};
-pub(super) const COMBO_BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-pub(super) const COMBO_ACTIVE_BORDER: Color = Color {
- r: 0.45,
- g: 0.65,
- b: 0.90,
- a: 1.0,
-};
-pub(super) const COMBO_ARROW: Color = Color {
- r: 0.70,
- g: 0.70,
- b: 0.70,
- a: 1.0,
-};
-pub(super) const SWATCH_BORDER: Color = Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.5,
-};
-pub(super) const TIP_BG: Color = Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 0.97,
-};
-pub(super) const HIST_INACTIVE_BG: Color = Color {
- r: 0.20,
- g: 0.20,
- b: 0.20,
- a: 1.0,
-};
-
// ββ Style context (passed from Ribbon to render_large) ββββββββββββββββββββ
pub(super) struct StyleContext {
@@ -294,11 +125,8 @@ pub(super) fn flush_small_col<'a>(
pub(super) fn make_icon(icon: IconKind, size: f32) -> Element<'static, Message> {
match icon {
- IconKind::Glyph(s) => text(s).size(size * 0.7).color(Color::WHITE).into(),
- IconKind::Svg(bytes) => {
- let handle = svg::Handle::from_memory(bytes);
- svg(handle).width(size).height(size).into()
- }
+ IconKind::Glyph(s) => text(s).size(size * 0.7).into(),
+ IconKind::Svg(bytes) => icons::semantic(bytes, size),
}
}
@@ -309,22 +137,19 @@ pub(super) fn start_dimmed(state: &ToggleState, event: &ModuleEvent) -> bool {
&& !matches!(event, ModuleEvent::Command(c) if crate::app::commands::start_allowed(c))
}
-/// Label / glyph color for a possibly-dimmed tool.
-pub(super) const DIM_TOOL: Color = Color {
- r: 0.42,
- g: 0.42,
- b: 0.45,
- a: 1.0,
-};
-
-/// `make_icon`, greyed out when `dim` (SVGs render monochrome via tint).
+/// `make_icon`, faded when `dim` without flattening multi-colour SVGs.
pub(super) fn make_icon_dim(icon: IconKind, size: f32, dim: bool) -> Element<'static, Message> {
if !dim {
return make_icon(icon, size);
}
match icon {
- IconKind::Glyph(s) => text(s).size(size * 0.7).color(DIM_TOOL).into(),
- IconKind::Svg(bytes) => icons::tinted(bytes, size, DIM_TOOL),
+ IconKind::Glyph(s) => text(s)
+ .size(size * 0.7)
+ .style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.42)),
+ })
+ .into(),
+ IconKind::Svg(bytes) => icons::semantic_disabled(bytes, size),
}
}
@@ -349,15 +174,24 @@ pub(super) fn is_active_tool(
// ββ Button style βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-pub(super) fn tool_btn_style(is_active: bool, status: button::Status) -> button::Style {
+pub(super) fn tool_btn_style(
+ theme: &Theme,
+ is_active: bool,
+ status: button::Status,
+) -> button::Style {
+ let palette = theme.extended_palette();
+ let pair = match (is_active, status) {
+ (true, _) => palette.primary.weak,
+ (_, button::Status::Hovered) => palette.background.weak,
+ (_, button::Status::Pressed) => palette.primary.weak,
+ _ => palette.background.base,
+ };
button::Style {
- background: Some(Background::Color(match (is_active, status) {
- (true, _) => TOOL_ACTIVE,
- (_, button::Status::Hovered) => TOOL_HOVER,
- (_, button::Status::Pressed) => TOOL_ACTIVE,
- _ => Color::TRANSPARENT,
- })),
- text_color: Color::WHITE,
+ background: is_active
+ .then_some(Background::Color(pair.color))
+ .or_else(|| matches!(status, button::Status::Hovered | button::Status::Pressed)
+ .then_some(Background::Color(pair.color))),
+ text_color: pair.text,
border: Border {
radius: 3.0.into(),
color: Color::TRANSPARENT,
@@ -368,21 +202,93 @@ pub(super) fn tool_btn_style(is_active: bool, status: button::Status) -> button:
}
}
-// ββ Tooltip helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-pub(super) fn make_tip(tip: String) -> Element<'static, Message> {
- text(tip).size(11).color(Color::WHITE).into()
+pub(super) fn combo_btn_style(
+ theme: &Theme,
+ is_open: bool,
+ status: button::Status,
+ radius: f32,
+) -> button::Style {
+ let palette = theme.extended_palette();
+ let pair = if is_open {
+ palette.primary.weak
+ } else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
+ palette.background.weak
+ } else {
+ palette.background.weakest
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
+ border: Border {
+ radius: radius.into(),
+ width: 1.0,
+ color: if is_open {
+ palette.primary.base.color
+ } else {
+ palette.background.neutral.color
+ },
+ },
+ ..Default::default()
+ }
}
-pub(super) fn tip_style(_theme: &Theme) -> container::Style {
+pub(super) fn popup_row_style(theme: &Theme, status: button::Status) -> button::Style {
+ let palette = theme.extended_palette();
+ let pair = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
+ palette.background.weak
+ } else {
+ palette.background.base
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
+ ..Default::default()
+ }
+}
+
+pub(super) fn popup_panel_style(theme: &Theme) -> container::Style {
+ let palette = theme.extended_palette();
container::Style {
- background: Some(Background::Color(TIP_BG)),
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: COMBO_BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
- text_color: Some(Color::WHITE),
+ ..Default::default()
+ }
+}
+
+pub(super) fn muted_text_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.72)),
+ }
+}
+
+pub(super) fn tool_label_style(theme: &Theme, dim: bool) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: dim.then_some(
+ theme.extended_palette().background.base.text.scale_alpha(0.42),
+ ),
+ }
+}
+
+// ββ Tooltip helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+pub(super) fn make_tip(tip: String) -> Element<'static, Message> {
+ text(tip).size(11).into()
+}
+
+pub(super) fn tip_style(theme: &Theme) -> container::Style {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.strong.color)),
+ border: Border {
+ color: palette.background.neutral.color,
+ width: 1.0,
+ radius: 3.0.into(),
+ },
+ text_color: Some(palette.background.strong.text),
..Default::default()
}
}
@@ -408,7 +314,7 @@ pub(super) fn render_small<'a>(
let tip_text = format!("{}\nCommand: {}", t.label, t.id);
let btn = button(make_icon_dim(t.icon, SMALL_ICON, dim))
.on_press(Message::RibbonToolClick { tool_id, event })
- .style(move |_: &Theme, status| tool_btn_style(active, status))
+ .style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(SMALL_W))
.height(ROW_H)
.padding([4, 4]);
@@ -474,31 +380,22 @@ pub(super) fn render_small<'a>(
tool_id: last.to_string(),
event: ModuleEvent::Command(last.to_string()),
})
- .style(move |_: &Theme, status| tool_btn_style(active, status))
+ .style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(SMALL_W))
.height(ROW_H)
.padding([4, 4]);
let arr_tip = format!("{} options", cur_label);
let arr_btn = button(
- container(icons::arrow_down(8.0, ARROW_COLOR))
+ container(icons::themed_arrow_down(8.0))
.width(Fill)
.height(Fill)
.align_x(iced::Center)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => TOOL_HOVER,
- _ if dd_open => TOOL_ACTIVE,
- _ => Color::TRANSPARENT,
- })),
- border: Border {
- radius: 2.0.into(),
- ..Default::default()
- },
- ..Default::default()
+ .style(move |theme: &Theme, status| {
+ tool_btn_style(theme, dd_open, status)
})
.width(Length::Fixed(ARROW_W))
.height(ROW_H)
@@ -569,7 +466,9 @@ pub(super) fn render_large_dropdown<'a>(
let top_btn = button(
column![
make_icon_dim(cur_icon, LARGE_ICON, dim),
- text(label.to_string()).size(10).color(if dim { DIM_TOOL } else { LABEL_COLOR }),
+ text(label.to_string())
+ .size(10)
+ .style(move |theme: &Theme| tool_label_style(theme, dim)),
]
.align_x(iced::Center)
.spacing(3),
@@ -578,7 +477,7 @@ pub(super) fn render_large_dropdown<'a>(
tool_id: last.to_string(),
event: ModuleEvent::Command(last.to_string()),
})
- .style(move |_: &Theme, status| tool_btn_style(active, status))
+ .style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(LARGE_W))
.height(Fill)
.padding(Padding {
@@ -589,24 +488,15 @@ pub(super) fn render_large_dropdown<'a>(
});
let arr_btn = button(
- container(icons::arrow_down(9.0, ARROW_COLOR))
+ container(icons::themed_arrow_down(9.0))
.width(Fill)
.height(Fill)
.align_x(iced::Center)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => TOOL_HOVER,
- _ if dd_open => TOOL_ACTIVE,
- _ => Color::TRANSPARENT,
- })),
- border: Border {
- radius: 3.0.into(),
- ..Default::default()
- },
- ..Default::default()
+ .style(move |theme: &Theme, status| {
+ tool_btn_style(theme, dd_open, status)
})
.width(Length::Fixed(LARGE_W))
.height(LARGE_ARR)
@@ -659,13 +549,15 @@ pub(super) fn render_large<'a>(
let btn = button(
column![
make_icon_dim(t.icon, LARGE_ICON, dim),
- text(t.label).size(10).color(if dim { DIM_TOOL } else { LABEL_COLOR }),
+ text(t.label)
+ .size(10)
+ .style(move |theme: &Theme| tool_label_style(theme, dim)),
]
.align_x(iced::Center)
.spacing(3),
)
.on_press(Message::RibbonToolClick { tool_id, event })
- .style(move |_: &Theme, status| tool_btn_style(active, status))
+ .style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(LARGE_W))
.height(Fill)
.padding(Padding {
@@ -733,14 +625,14 @@ 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 = 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 vis_icon = icons::semantic(icons::layer_visible(lv), 14.0);
+ let freeze_icon = icons::semantic(icons::layer_freeze(lf), 14.0);
+ let lock_icon = icons::semantic(icons::layer_lock(ll), 14.0);
let swatch = container(text(""))
- .style(move |_: &Theme| container::Style {
+ .style(move |theme: &Theme| container::Style {
background: Some(Background::Color(lc)),
border: Border {
- color: SWATCH_BORDER,
+ color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 1.0.into(),
},
@@ -758,27 +650,17 @@ pub(super) fn render_large<'a>(
freeze_icon,
lock_icon,
swatch,
- container(text(active_layer).size(11).color(Color::WHITE))
+ container(text(active_layer).size(11))
.width(name_w)
.clip(true),
- icons::arrow_down(9.0, COMBO_ARROW),
+ icons::themed_arrow_down(9.0),
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(LAYER_COMBO_ID.to_string()))
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (is_open, status) {
- (true, _) => COMBO_OPEN_BG,
- (_, button::Status::Hovered) => COMBO_HOVER_BG,
- _ => COMBO_BG,
- })),
- border: Border {
- radius: 3.0.into(),
- width: 1.0,
- color: COMBO_BORDER,
- },
- ..Default::default()
+ .style(move |theme: &Theme, status| {
+ combo_btn_style(theme, is_open, status, 3.0)
})
.padding([3, 8])
.width(Fill);
@@ -793,20 +675,16 @@ pub(super) fn render_large<'a>(
let event = t.event.clone();
let icon_el: Element = if dim {
make_icon_dim(t.icon, 16.0, true)
- } else { match t.icon {
- IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(),
- IconKind::Svg(bytes) => {
- iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
- .width(16)
- .height(16)
- .into()
- }
- } };
+ } else {
+ make_icon(t.icon, 16.0)
+ };
let msg = module_event_to_message(event);
tooltip(
button(icon_el)
.on_press(msg)
- .style(move |_: &Theme, status| tool_btn_style(is_active, status))
+ .style(move |theme: &Theme, status| {
+ tool_btn_style(theme, is_active, status)
+ })
.padding([2, 5]),
make_tip(tip.to_string()),
TipPos::Right,
@@ -864,7 +742,9 @@ pub(super) fn render_large<'a>(
let mp_btn = button(
column![
make_icon_dim(match_prop.icon, LARGE_ICON, mp_dim),
- text(match_prop.label).size(10).color(if mp_dim { DIM_TOOL } else { LABEL_COLOR }),
+ text(match_prop.label)
+ .size(10)
+ .style(move |theme: &Theme| tool_label_style(theme, mp_dim)),
]
.align_x(iced::Center)
.spacing(3),
@@ -873,7 +753,7 @@ pub(super) fn render_large<'a>(
tool_id: mp_id,
event: mp_event,
})
- .style(move |_: &Theme, status| tool_btn_style(mp_active, status))
+ .style(move |theme: &Theme, status| tool_btn_style(theme, mp_active, status))
.width(Length::Fixed(LARGE_W))
.height(Fill)
.padding(Padding {
@@ -895,10 +775,10 @@ pub(super) fn render_large<'a>(
let is_open = open_dd.as_deref() == Some(dd_id);
let swatch_el: Element<'a, Message> = if let Some(c) = swatch {
container(text(""))
- .style(move |_: &Theme| container::Style {
+ .style(move |theme: &Theme| container::Style {
background: Some(Background::Color(c)),
border: Border {
- color: SWATCH_BORDER,
+ color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 1.0.into(),
},
@@ -913,40 +793,21 @@ pub(super) fn render_large<'a>(
button(
row![
swatch_el,
- container(text(label).size(10).color(Color::WHITE))
+ container(text(label).size(10))
.width(Fill)
.clip(true),
- icons::arrow_toggle(
- is_open,
- 8.0,
- Color {
- r: 0.6,
- g: 0.6,
- b: 0.6,
- a: 1.0,
- },
- ),
+ if is_open {
+ icons::themed_arrow_up(8.0)
+ } else {
+ icons::themed_arrow_down(8.0)
+ },
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(dd_id.to_string()))
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (is_open, status) {
- (true, _) => COMBO_OPEN_BG,
- (_, button::Status::Hovered) => COMBO_HOVER_BG,
- _ => COMBO_BG,
- })),
- border: Border {
- radius: 2.0.into(),
- width: 1.0,
- color: if is_open {
- COMBO_ACTIVE_BORDER
- } else {
- COMBO_BORDER
- },
- },
- ..Default::default()
+ .style(move |theme: &Theme, status| {
+ combo_btn_style(theme, is_open, status, 2.0)
})
.padding([3, 8])
.width(Length::Fixed(PROP_W))
@@ -999,31 +860,21 @@ pub(super) fn render_large<'a>(
// ββ combo button ββ
let combo_btn = button(
row![
- container(text(active.clone()).size(11).color(Color::WHITE))
+ container(text(active.clone()).size(11))
.width(Fill)
.clip(true),
- icons::arrow_toggle(is_open, 9.0, COMBO_ARROW),
+ if is_open {
+ icons::themed_arrow_up(9.0)
+ } else {
+ icons::themed_arrow_down(9.0)
+ },
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(combo_id.to_string()))
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (is_open, status) {
- (true, _) => COMBO_OPEN_BG,
- (_, button::Status::Hovered) => COMBO_HOVER_BG,
- _ => COMBO_BG,
- })),
- border: Border {
- radius: 3.0.into(),
- width: 1.0,
- color: if is_open {
- COMBO_ACTIVE_BORDER
- } else {
- COMBO_BORDER
- },
- },
- ..Default::default()
+ .style(move |theme: &Theme, status| {
+ combo_btn_style(theme, is_open, status, 3.0)
})
.padding([3, 8])
.width(Fill);
@@ -1045,20 +896,16 @@ pub(super) fn render_large<'a>(
let event = t.event.clone();
let icon_el: Element = if dim {
make_icon_dim(t.icon, 16.0, true)
- } else { match t.icon {
- IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(),
- IconKind::Svg(bytes) => {
- iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
- .width(16)
- .height(16)
- .into()
- }
- } };
+ } else {
+ make_icon(t.icon, 16.0)
+ };
let msg = module_event_to_message(event);
tooltip(
button(icon_el)
.on_press(msg)
- .style(move |_: &Theme, status| tool_btn_style(is_active, status))
+ .style(move |theme: &Theme, status| {
+ tool_btn_style(theme, is_active, status)
+ })
.padding([2, 5]),
make_tip(tip.to_string()),
TipPos::Right,
@@ -1128,7 +975,11 @@ pub(super) fn quick_access_btn<'a>(
// they read on the dark top strip (raw black is invisible there).
// On the Start tab, commands the start gate refuses render dimmed.
let dim = is_start && !crate::app::commands::start_allowed(cmd);
- let icon = icons::tinted(icon_bytes, 16.0, if dim { DIM_TOOL } else { QA_ICON_COLOR });
+ let icon = if dim {
+ icons::themed_disabled(icon_bytes, 16.0)
+ } else {
+ icons::themed(icon_bytes, 16.0)
+ };
let btn = button(
container(icon)
.width(Fill)
@@ -1137,22 +988,7 @@ pub(super) fn quick_access_btn<'a>(
.align_y(iced::Center),
)
.on_press(Message::Command(cmd.to_string()))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- border: Border {
- radius: 2.0.into(),
- ..Default::default()
- },
- ..Default::default()
- })
+ .style(button::subtle)
.width(Length::Fixed(TOP_HIST_W))
.height(24)
.padding([2, 0]);
@@ -1171,13 +1007,12 @@ 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)
+ icons::themed_undo(15.0, active)
} else {
- icons::redo(15.0, icon_color)
+ icons::themed_redo(15.0, active)
};
let btn = button(
container(glyph)
@@ -1186,7 +1021,9 @@ pub(super) fn render_history_control<'a>(
.align_x(iced::Center)
.align_y(iced::Center),
)
- .style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status))
+ .style(move |theme: &Theme, status| {
+ top_hist_btn_style(theme, active, dd_open, status)
+ })
.width(Length::Fixed(TOP_HIST_W))
.height(24)
.padding([2, 0]);
@@ -1211,16 +1048,19 @@ pub(super) fn render_history_control<'a>(
let arrow_btn = {
let btn = button(
- container(icons::arrow_down(
- 8.0,
- if active { ARROW_COLOR } else { LABEL_OFF },
- ))
+ container(if active {
+ icons::themed_arrow_down(8.0)
+ } else {
+ icons::themed_disabled_arrow_down(8.0)
+ })
.width(Fill)
.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: &Theme, status| {
+ top_hist_btn_style(theme, active, dd_open, status)
+ })
.width(Length::Fixed(TOP_ARR_W))
.height(24)
.padding(0);
@@ -1243,19 +1083,26 @@ pub(super) fn render_history_control<'a>(
}
pub(super) fn top_hist_btn_style(
+ theme: &Theme,
active: bool,
open: bool,
status: button::Status,
) -> button::Style {
+ let palette = theme.extended_palette();
+ let pair = match (active, open, status) {
+ (false, _, _) => palette.background.weakest,
+ (_, true, _) => palette.primary.weak,
+ (_, _, button::Status::Hovered) => palette.background.weak,
+ (_, _, button::Status::Pressed) => palette.primary.weak,
+ _ => palette.background.base,
+ };
button::Style {
- background: Some(Background::Color(match (active, open, status) {
- (false, _, _) => HIST_INACTIVE_BG,
- (_, true, _) => TOOL_ACTIVE,
- (_, _, button::Status::Hovered) => TOOL_HOVER,
- (_, _, button::Status::Pressed) => TOOL_ACTIVE,
- _ => Color::TRANSPARENT,
- })),
- text_color: Color::WHITE,
+ background: (!active || open || matches!(
+ status,
+ button::Status::Hovered | button::Status::Pressed
+ ))
+ .then_some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
radius: 3.0.into(),
color: Color::TRANSPARENT,
diff --git a/src/ui/side_toolbar.rs b/src/ui/side_toolbar.rs
index 9a3349c7f..9bad95bf6 100644
--- a/src/ui/side_toolbar.rs
+++ b/src/ui/side_toolbar.rs
@@ -7,15 +7,12 @@
//! module's tools can drive it. First used for paper-space viewport / plot
//! actions; reusable for any future context action set.
-use iced::widget::{button, column, container, svg, text, tooltip};
-use iced::{Background, Border, Color, Element, Length, Theme};
+use iced::widget::{button, column, container, text, tooltip};
+use iced::{Background, Border, Element, Length, Theme};
use crate::app::Message;
use crate::modules::{IconKind, ToolDef};
-const PANEL_BG: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 0.96 };
-const PANEL_BORDER: Color = Color { r: 0.32, g: 0.32, b: 0.32, a: 1.0 };
-const BTN_HOVER: Color = Color { r: 0.24, g: 0.24, b: 0.24, a: 1.0 };
const ICON_SIZE: f32 = 22.0;
const BTN_SIZE: f32 = 38.0;
/// Gap between the toolbar and the right edge of the canvas.
@@ -23,25 +20,26 @@ const EDGE_MARGIN: f32 = 8.0;
fn icon_el(icon: IconKind) -> Element<'static, Message> {
match icon {
- IconKind::Glyph(s) => text(s).size(ICON_SIZE * 0.85).color(Color::WHITE).into(),
- IconKind::Svg(bytes) => svg(svg::Handle::from_memory(bytes))
- .width(Length::Fixed(ICON_SIZE))
- .height(Length::Fixed(ICON_SIZE))
- .into(),
+ IconKind::Glyph(s) => text(s).size(ICON_SIZE * 0.85).into(),
+ IconKind::Svg(bytes) => crate::ui::icons::semantic(bytes, ICON_SIZE),
}
}
fn tip_panel(label: &'static str) -> Element<'static, Message> {
- container(text(label).size(11).color(Color::WHITE))
+ container(text(label).size(11))
.padding([2, 6])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.strong.color)),
border: Border {
- color: PANEL_BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
+ text_color: Some(palette.background.strong.text),
..Default::default()
+ }
})
.into()
}
@@ -63,17 +61,22 @@ pub fn view(tools: &[ToolDef]) -> Option> {
})
.width(Length::Fixed(BTN_SIZE))
.height(Length::Fixed(BTN_SIZE))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => BTN_HOVER,
- _ => Color::TRANSPARENT,
- })),
+ .style(|theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let hovered = matches!(
+ status,
+ button::Status::Hovered | button::Status::Pressed
+ );
+ button::Style {
+ background: hovered
+ .then_some(Background::Color(palette.background.strong.color)),
border: Border {
radius: 3.0.into(),
..Default::default()
},
- text_color: Color::WHITE,
+ text_color: palette.background.base.text,
..Default::default()
+ }
});
// Label tooltip on the left so it never runs off the right edge.
col = col.push(
@@ -81,14 +84,17 @@ pub fn view(tools: &[ToolDef]) -> Option> {
);
}
- let panel = container(col).padding(4).style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ let panel = container(col).padding(4).style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: PANEL_BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 5.0.into(),
},
..Default::default()
+ }
});
// Fill the canvas, pin the panel to the right edge and centre it
diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs
index e87bcde46..6ab82a431 100644
--- a/src/ui/statusbar/mod.rs
+++ b/src/ui/statusbar/mod.rs
@@ -2,12 +2,14 @@
pub mod statusbar_config;
pub mod statusbar_menu;
+pub mod status_menu;
use iced::widget::tooltip::Position as TipPos;
use iced::widget::{
- button, container, mouse_area, row, text, text_input, tooltip,
+ button, column, container, mouse_area, row, text, text_input, tooltip,
};
use iced::{Background, Border, Color, Element, Length, Theme};
+use iced_aw::ContextMenu;
use std::sync::Arc;
/// Scrollable id of the status-bar layout-tab strip (retained so the existing
@@ -22,17 +24,19 @@ pub const LAYOUT_RENAME_INPUT_ID: &str = "layout_rename_input";
use crate::app::Message;
use crate::snap::Snapper;
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
-use crate::ui::wrap_bar::{PosReport, WrapBar, WrapFlow};
+use crate::ui::statusbar::status_menu::Entry as StatusMenuEntry;
+use crate::ui::wrap_bar::{WrapBar, WrapFlow};
-// PosReport ids for anchoring each status-bar popup directly to its pill.
-pub const SB_OSNAP_ID: &str = "SB_OSNAP";
-pub const SB_SCALE_ID: &str = "SB_SCALE";
-pub const SB_UNITS_ID: &str = "SB_UNITS";
-pub const SB_ISOLATE_ID: &str = "SB_ISOLATE";
-pub const SB_FILTER_ID: &str = "SB_FILTER";
-pub const SB_MENU_ID: &str = "SB_MENU";
-pub const SB_LAYOUTLIST_ID: &str = "SB_LAYOUTLIST";
-pub const SB_POLAR_ID: &str = "SB_POLAR";
+pub struct StatusMenuData<'a> {
+ pub layout_names: Vec,
+ pub polar_custom_input: &'a str,
+ pub scale_is_model: bool,
+ pub scale_list: Vec<(String, f32, f64)>,
+ pub has_selection: bool,
+ pub selection_types: Vec,
+ pub selection_filter: &'a rustc_hash::FxHashSet,
+ pub tooltip_hidden: bool,
+}
#[derive(Clone, Default)]
pub struct StatusBar {
@@ -50,12 +54,9 @@ impl StatusBar {
pub fn view<'a>(
&'a self,
snapper: &'a Snapper,
- popup_open: bool,
ortho_mode: bool,
polar_mode: bool,
polar_increment_deg: f32,
- // True while the polar-angle picker popup is open.
- polar_popup_open: bool,
dyn_input: bool,
otrack: bool,
layouts: Vec,
@@ -75,8 +76,6 @@ impl StatusBar {
show_layout_tabs: bool,
// Current annotation scale for model space (1.0 = 1:1, 50.0 = 1:50, etc.).
annotation_scale: f32,
- // True when the scale picker popup is open.
- scale_popup_open: bool,
// True when the scale pill is interactive (always model space; paper space only when a viewport is active/selected).
scale_pill_enabled: bool,
// LWDISPLAY header flag β controls lineweight visibility in the viewport.
@@ -94,8 +93,6 @@ impl StatusBar {
clean_screen: bool,
// Drawing units (INSUNITS) for the units pill.
insertion_units: i16,
- // True while the drawing-units picker is open.
- units_popup_open: bool,
// True when objects are hidden by Isolate / Hide.
isolation_active: bool,
// Whether entity transparency is shown (Transparency pill state).
@@ -108,25 +105,28 @@ impl StatusBar {
selection_cycling: bool,
// Which pills the user has chosen to show on the bar.
config: &'a StatusBarConfig,
+ menu_data: StatusMenuData<'a>,
) -> Element<'a, Message> {
+ let StatusMenuData {
+ layout_names,
+ polar_custom_input,
+ scale_is_model,
+ scale_list,
+ has_selection,
+ selection_types,
+ selection_filter,
+ tooltip_hidden,
+ } = menu_data;
+
// Leftmost hamburger: opens a dropdown listing Model + every layout, so
// a layout can be picked directly even when the tab strip is scrolled.
- let menu_button = button(crate::ui::icons::tinted(
- crate::ui::icons::MENU,
- 16.0,
- if is_start { DISABLED_COLOR } else { ICON_COLOR },
- ))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => PILL_BG,
- _ => Color::TRANSPARENT,
- })),
- border: Border {
- radius: 3.0.into(),
- ..Default::default()
- },
- ..Default::default()
- })
+ let menu_icon = if is_start {
+ crate::ui::icons::themed_disabled(crate::ui::icons::MENU, 16.0)
+ } else {
+ crate::ui::icons::themed_secondary(crate::ui::icons::MENU, 16.0)
+ };
+ let menu_button = button(menu_icon)
+ .style(button::subtle)
.padding([4, 8]);
let menu_btn = if is_start {
tip(
@@ -134,18 +134,21 @@ impl StatusBar {
"Open or create a drawing to manage layouts.",
)
} else {
- menu_button.on_press(Message::ToggleLayoutList).into()
+ status_menu::menu_bar(
+ menu_tip(
+ menu_button
+ .on_press(Message::StatusMenuTooltipHidden(true))
+ .into(),
+ "Model and layout list",
+ tooltip_hidden,
+ ),
+ statusbar_menu::layout_entries(&layout_names, ¤t_layout),
+ 200.0,
+ )
};
- let add_button = button(text("+").size(12).color(if is_start {
- DISABLED_COLOR
- } else {
- ICON_COLOR
- }))
- .style(|_: &Theme, _| button::Style {
- background: Some(Background::Color(Color::TRANSPARENT)),
- ..Default::default()
- })
+ let add_button = button(text("+").size(12))
+ .style(button::subtle)
.padding([4, 8]);
let add_btn = if is_start {
tip(
@@ -173,9 +176,19 @@ impl StatusBar {
format_scale(viewport_scale)
};
let scale_element: Element<'_, Message> = if scale_pill_enabled {
- tip(
- popup_pill(&scale_label, scale_popup_open, Message::ToggleScalePopup),
- "Annotation / Viewport Scale\nClick to change",
+ status_menu::menu_bar(
+ menu_tip(
+ popup_pill(&scale_label),
+ "Annotation / Viewport Scale\nClick to change",
+ tooltip_hidden,
+ ),
+ crate::ui::popup::scale_popup::menu_entries(
+ scale_is_model,
+ annotation_scale,
+ viewport_scale,
+ scale_list,
+ ),
+ if scale_is_model { 150.0 } else { 120.0 },
)
} else {
status_pill(scale_label).into()
@@ -189,7 +202,7 @@ impl StatusBar {
let coords_label = format_coords(cursor_world, last_point, coords_mode, picking);
pills.push(
tip(
- popup_pill(&coords_label, false, Message::CycleCoordsMode),
+ action_pill(&coords_label, Message::CycleCoordsMode),
"Cursor coordinates ($COORDS)\nClick to cycle: static / live / polar",
)
.into(),
@@ -215,9 +228,14 @@ impl StatusBar {
}
if vis(StatusPill::Polar) {
pills.push(
- PosReport::new(
- SB_POLAR_ID,
- polar_pill(polar_mode, polar_increment_deg, polar_popup_open),
+ polar_pill(
+ polar_mode,
+ polar_increment_deg,
+ tooltip_hidden,
+ crate::ui::popup::polar_popup::menu_entries(
+ polar_increment_deg,
+ polar_custom_input,
+ ),
)
.into(),
);
@@ -242,9 +260,11 @@ impl StatusBar {
}
if vis(StatusPill::Osnap) {
pills.push(
- PosReport::new(
- SB_OSNAP_ID,
- osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
+ osnap_btn(
+ osnap_active,
+ snapper.snap_enabled,
+ tooltip_hidden,
+ crate::ui::popup::snap_popup::menu_entries(snapper),
)
.into(),
);
@@ -259,20 +279,18 @@ impl StatusBar {
);
}
if vis(StatusPill::Scale) {
- pills.push(PosReport::new(SB_SCALE_ID, scale_element).into());
+ pills.push(scale_element);
}
if vis(StatusPill::Units) {
pills.push(
- PosReport::new(
- SB_UNITS_ID,
- tip(
- popup_pill(
- crate::ui::popup::units_popup::unit_short(insertion_units),
- units_popup_open,
- Message::ToggleUnitsPopup,
- ),
+ status_menu::menu_bar(
+ menu_tip(
+ popup_pill(crate::ui::popup::units_popup::unit_short(insertion_units)),
"Drawing Units (INSUNITS)\nClick to change",
+ tooltip_hidden,
),
+ crate::ui::popup::units_popup::menu_entries(insertion_units),
+ 140.0,
)
.into(),
);
@@ -292,12 +310,21 @@ impl StatusBar {
}
if vis(StatusPill::Isolate) {
pills.push(
- PosReport::new(
- SB_ISOLATE_ID,
- tip(
- toggle_pill(crate::ui::icons::ST_ISOLATE, isolation_active, Message::ToggleIsolatePopup),
+ status_menu::menu_bar(
+ menu_tip(
+ toggle_pill(
+ crate::ui::icons::ST_ISOLATE,
+ isolation_active,
+ Message::StatusMenuTooltipHidden(true),
+ ),
"Isolate Objects\nClick for Isolate / Hide / End",
+ tooltip_hidden,
),
+ crate::ui::popup::isolate_popup::menu_entries(
+ has_selection,
+ isolation_active,
+ ),
+ 160.0,
)
.into(),
);
@@ -313,16 +340,21 @@ impl StatusBar {
}
if vis(StatusPill::SelFilter) {
pills.push(
- PosReport::new(
- SB_FILTER_ID,
- tip(
+ status_menu::menu_bar(
+ menu_tip(
toggle_pill(
crate::ui::icons::ST_FILTER,
selection_filter_active,
- Message::ToggleSelectionFilterPopup,
+ Message::StatusMenuTooltipHidden(true),
),
"Selection Filtering\nLimit which object types can be picked",
+ tooltip_hidden,
),
+ crate::ui::popup::selection_filter_popup::menu_entries(
+ selection_types,
+ selection_filter,
+ ),
+ 180.0,
)
.into(),
);
@@ -356,12 +388,14 @@ impl StatusBar {
}
// Customization handle: opens the pill show/hide menu.
pills.push(
- PosReport::new(
- SB_MENU_ID,
- tip(
+ status_menu::menu_bar(
+ menu_tip(
customize_btn(),
"Customization\nShow or hide status-bar items",
+ tooltip_hidden,
),
+ statusbar_menu::customization_entries(config),
+ 200.0,
)
.into(),
);
@@ -375,7 +409,7 @@ impl StatusBar {
// use the remaining space on the final tab row when they fit; otherwise
// WrapBar adds another right-aligned row.
let mut left: Vec> = Vec::new();
- left.push(PosReport::new(SB_LAYOUTLIST_ID, menu_btn).into());
+ left.push(menu_btn);
if show_layout_tabs {
let reorderable_layouts: Arc<[String]> = reorderable_layouts.into();
for name in layouts {
@@ -403,14 +437,17 @@ impl StatusBar {
.justify_end(true);
container(wrap)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BAR_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
+ }
})
.width(Length::Fill)
// One row matches the drawing (document) tab bar height so the three
@@ -451,19 +488,9 @@ fn format_coords(cursor: glam::DVec3, last: Option, mode: i16, pick
// ββ Customization handle ββββββββββββββββββββββββββββββββββββββββββββββββββ
fn customize_btn() -> Element<'static, Message> {
- button(crate::ui::icons::tinted(crate::ui::icons::MENU, 16.0, ICON_COLOR))
- .on_press(Message::ToggleStatusBarMenu)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => PILL_BG,
- _ => Color::TRANSPARENT,
- })),
- border: Border {
- radius: 3.0.into(),
- ..Default::default()
- },
- ..Default::default()
- })
+ button(crate::ui::icons::themed_secondary(crate::ui::icons::MENU, 16.0))
+ .on_press(Message::StatusMenuTooltipHidden(true))
+ .style(button::subtle)
.padding([4, 8])
.into()
}
@@ -471,7 +498,25 @@ fn customize_btn() -> Element<'static, Message> {
// ββ Tooltip helper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
fn tip<'a>(content: Element<'a, Message>, label: &'static str) -> Element<'a, Message> {
- tip_node(content, text(label).size(11).color(Color::WHITE).into())
+ tip_node(content, text(label).size(11).into())
+}
+
+/// A menu root shows its tooltip until clicked. Moving from the root into the
+/// opened menu resets suppression for the next hover without covering the menu.
+fn menu_tip<'a>(
+ content: Element<'a, Message>,
+ label: &'static str,
+ hidden: bool,
+) -> Element<'a, Message> {
+ let content = if hidden {
+ content
+ } else {
+ tip(content, label)
+ };
+
+ mouse_area(content)
+ .on_exit(Message::StatusMenuTooltipHidden(false))
+ .into()
}
/// Like [`tip`] but the tooltip body is any element β used to embed an SVG
@@ -481,26 +526,7 @@ fn tip_node<'a>(content: Element<'a, Message>, body: Element<'a, Message>) -> El
tooltip(
content,
container(body)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 0.97,
- })),
- border: Border {
- color: Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
- },
- width: 1.0,
- radius: 3.0.into(),
- },
- text_color: Some(Color::WHITE),
- ..Default::default()
- })
+ .style(container::bordered_box)
.padding([4, 8]),
TipPos::Top,
)
@@ -513,24 +539,26 @@ fn tip_node<'a>(content: Element<'a, Message>, body: Element<'a, Message>) -> El
/// text labels were too small to read). The name lives in the tooltip each
/// call site already wraps it with.
fn toggle_pill(icon: &'static [u8], active: bool, msg: Message) -> Element<'static, Message> {
- let color = if active { OSNAP_ON_TEXT } else { OSNAP_OFF_TEXT };
- button(crate::ui::icons::tinted(icon, 17.0, color))
+ let icon = if active {
+ crate::ui::icons::themed_primary(icon, 17.0)
+ } else {
+ crate::ui::icons::themed_secondary(icon, 17.0)
+ };
+ button(icon)
.on_press(msg)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (active, status) {
- (true, button::Status::Hovered) => SNAP_ON_HOVER,
- (true, _) => SNAP_ON_BG,
- (false, button::Status::Hovered) => SNAP_OFF_HOVER,
- (false, _) => SNAP_OFF_BG,
- })),
- border: Border {
- color: if active { SNAP_BORDER_ON } else { BORDER_COLOR },
- width: 1.0,
- radius: 2.0.into(),
- },
- text_color: color,
- shadow: iced::Shadow::default(),
- snap: false,
+ .style(move |theme: &Theme, status| {
+ let mut style = button::subtle(theme, status);
+ if active {
+ let palette = theme.extended_palette();
+ style.background = Some(Background::Color(match status {
+ button::Status::Hovered => palette.primary.base.color,
+ _ => palette.primary.weak.color,
+ }));
+ style.text_color = palette.primary.weak.text;
+ style.border.color = palette.primary.base.color;
+ style.border.width = 1.0;
+ }
+ style
})
.padding([4, 7])
.into()
@@ -543,49 +571,33 @@ fn toggle_pill(icon: &'static [u8], active: bool, msg: Message) -> Element<'stat
// and a dropdown caret, so both halves read as a single integrated control and
// the border / background / caret / sizing live in exactly ONE place.
-/// Pill text/icon tint for the lit vs. dim state β shared so `main` (built by
-/// the caller) and the caret always match.
-fn pill_text_color(active: bool) -> Color {
- if active {
- OSNAP_ON_TEXT
- } else {
- OSNAP_OFF_TEXT
- }
-}
-
-/// Wrap a caller-built, already-click-wired `main` element together with a
-/// dropdown caret that emits `caret_msg`. `active` lights the pill; `open`
-/// highlights the border while its popup is showing.
-fn split_pill(
+/// Wrap a caller-built, already-click-wired `main` element together with its
+/// menu-bearing dropdown caret.
+fn split_pill<'a>(
main: Element<'static, Message>,
- caret_msg: Message,
+ caret: Element<'a, Message>,
active: bool,
- open: bool,
-) -> Element<'static, Message> {
- let color = pill_text_color(active);
- let bg = if active { SNAP_ON_BG } else { SNAP_OFF_BG };
- let border_color = if open {
- ACCENT
- } else if active {
- SNAP_BORDER_ON
- } else {
- BORDER_COLOR
- };
-
- let caret = mouse_area(
- container(crate::ui::icons::arrow_down(9.0, color)).padding([0, 1]),
- )
- .on_press(caret_msg);
-
+) -> Element<'a, Message> {
container(row![main, caret].spacing(3).align_y(iced::Center))
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(bg)),
+ .style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(if active {
+ palette.primary.weak.color
+ } else {
+ palette.background.weakest.color
+ })),
border: Border {
- color: border_color,
+ color: if active {
+ palette.primary.base.color
+ } else {
+ palette.background.neutral.color
+ },
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
+ }
})
.padding([4, 6])
.into()
@@ -596,12 +608,16 @@ fn split_pill(
// Main: left-click toggles polar on/off; right-click cycles the increment.
// Caret: opens the angle picker.
-fn polar_pill(active: bool, increment_deg: f32, open: bool) -> Element<'static, Message> {
+fn polar_pill<'a>(
+ active: bool,
+ increment_deg: f32,
+ tooltip_hidden: bool,
+ entries: Vec>,
+) -> Element<'a, Message> {
let angle = crate::ui::popup::polar_popup::angle_label(increment_deg);
let tooltip_text = format!(
"Polar Tracking ({angle})\nF10 β left-click on/off\nRight-click cycles Β· βΎ picks angle",
);
- let color = pill_text_color(active);
// Right-click quick-cycles through the same increments the picker lists.
const CYCLE: &[f32] = &[90.0, 45.0, 30.0, 22.5, 18.0, 15.0, 10.0, 5.0, 1.0];
@@ -610,66 +626,129 @@ fn polar_pill(active: bool, increment_deg: f32, open: bool) -> Element<'static,
None => 45.0,
};
+ let polar_icon = if active {
+ crate::ui::icons::themed_primary(crate::ui::icons::ST_POLAR, 17.0)
+ } else {
+ crate::ui::icons::themed_secondary(crate::ui::icons::ST_POLAR, 17.0)
+ };
let main = mouse_area(
row![
- crate::ui::icons::tinted(crate::ui::icons::ST_POLAR, 17.0, color),
- text(angle).size(11).color(color),
+ polar_icon,
+ text(angle).size(11).style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ text::Style {
+ color: Some(if active {
+ palette.primary.base.color
+ } else {
+ palette.background.base.text.scale_alpha(0.72)
+ }),
+ }
+ }),
]
.spacing(2)
.align_y(iced::Center),
)
.on_press(Message::TogglePolar)
.on_right_press(Message::SetPolarAngle(next_angle));
-
- tooltip(
- split_pill(main.into(), Message::TogglePolarPopup, active, open),
- container(text(tooltip_text).size(11).color(Color::WHITE))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 0.95,
- })),
- border: Border {
- color: Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
- },
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
+ let main = tooltip(
+ main,
+ container(text(tooltip_text).size(11))
+ .style(container::bordered_box)
.padding([4, 8]),
TipPos::Top,
- )
- .into()
+ );
+
+ let caret = status_menu::menu_bar(
+ menu_tip(
+ mouse_area(
+ container(if active {
+ crate::ui::icons::themed_primary_arrow_down(9.0)
+ } else {
+ crate::ui::icons::themed_secondary_arrow_down(9.0)
+ })
+ .padding([4, 7]),
+ )
+ .on_press(Message::StatusMenuTooltipHidden(true))
+ .into(),
+ "Polar angle\nClick to choose",
+ tooltip_hidden,
+ ),
+ entries,
+ 120.0,
+ );
+
+ split_pill(main.into(), caret, active)
}
// ββ OSNAP pill βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
//
// Main: toggles the global snap on/off. Caret: opens the snap-type dropdown.
-fn osnap_btn(active: bool, snap_enabled: bool, open: bool) -> Element<'static, Message> {
+fn osnap_btn<'a>(
+ active: bool,
+ snap_enabled: bool,
+ tooltip_hidden: bool,
+ entries: Vec>,
+) -> Element<'a, Message> {
let on = active || snap_enabled;
- let main = mouse_area(crate::ui::icons::tinted(
- crate::ui::icons::ST_OSNAP,
- 17.0,
- pill_text_color(on),
- ))
- .on_press(Message::ToggleSnapEnabled);
+ let snap_icon = if on {
+ crate::ui::icons::themed_primary(crate::ui::icons::ST_OSNAP, 17.0)
+ } else {
+ crate::ui::icons::themed_secondary(crate::ui::icons::ST_OSNAP, 17.0)
+ };
+ let main = tip(
+ mouse_area(snap_icon)
+ .on_press(Message::ToggleSnapEnabled)
+ .into(),
+ "Object Snap: toggle on/off\nF3",
+ );
- tip(
- split_pill(main.into(), Message::ToggleSnapPopup, on, open),
- "Object Snap: toggle on/off Β· βΎ opens the snap list\nF3",
- )
+ let caret = status_menu::menu_bar(
+ menu_tip(
+ mouse_area(
+ container(if on {
+ crate::ui::icons::themed_primary_arrow_down(9.0)
+ } else {
+ crate::ui::icons::themed_secondary_arrow_down(9.0)
+ })
+ .padding([4, 7]),
+ )
+ .on_press(Message::StatusMenuTooltipHidden(true))
+ .into(),
+ "Object Snap list\nClick to choose snap types",
+ tooltip_hidden,
+ ),
+ entries,
+ 210.0,
+ );
+
+ split_pill(main, caret, on)
}
// ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+fn layout_tab_context_menu(name: String) -> Element<'static, Message> {
+ let item = |label: &'static str, msg: Message| {
+ button(text(label).size(12))
+ .on_press(msg)
+ .style(button::subtle)
+ .padding([4, 12])
+ .width(Length::Fill)
+ };
+
+ container(
+ column![
+ item("Rename", Message::LayoutRenameStart(name.clone())),
+ item("Delete", Message::LayoutDelete(name)),
+ ]
+ .spacing(0)
+ .width(160),
+ )
+ .style(container::bordered_box)
+ .padding([4, 0])
+ .into()
+}
+
/// A layout tab button.
///
/// When `rename_edit` is `Some(value)` the tab shows an inline text input
@@ -682,46 +761,34 @@ fn space_tab<'a>(
enabled: bool,
reorderable_layouts: Arc<[String]>,
) -> Element<'a, Message> {
- let bg = move |is_active: bool, hovered: bool| {
- if is_active {
- TAB_ACTIVE
- } else if hovered {
- TAB_HOVER
+ let tab_style = move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ let text_color = if !enabled {
+ palette.background.base.text.scale_alpha(0.42)
+ } else if is_active {
+ palette.primary.weak.text
} else {
- Color::TRANSPARENT
- }
- };
-
- let border = Border {
- color: if is_active {
- ACCENT
- } else {
- Color::TRANSPARENT
- },
- width: if is_active { 1.0 } else { 0.0 },
- radius: 2.0.into(),
- };
-
- let text_color = if !enabled {
- DISABLED_COLOR
- } else if is_active {
- Color::WHITE
- } else {
- Color {
- r: 0.65,
- g: 0.65,
- b: 0.65,
- a: 1.0,
+ palette.background.base.text.scale_alpha(0.72)
+ };
+ container::Style {
+ background: is_active.then_some(Background::Color(palette.primary.weak.color)),
+ border: Border {
+ color: if is_active {
+ palette.primary.base.color
+ } else {
+ Color::TRANSPARENT
+ },
+ width: if is_active { 1.0 } else { 0.0 },
+ radius: 2.0.into(),
+ },
+ text_color: Some(text_color),
+ ..Default::default()
}
};
if !enabled {
- let display = container(text(label.clone()).size(12).color(text_color))
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(bg(is_active, false))),
- border,
- ..Default::default()
- })
+ let display = container(text(label.clone()).size(12))
+ .style(tab_style)
.padding([4, 10]);
crate::ui::wrap_bar::PosReport::owned(
format!("SB_LAYOUT_TAB:{label}"),
@@ -738,49 +805,15 @@ fn space_tab<'a>(
.on_input(Message::LayoutRenameEdit)
.on_submit(Message::LayoutRenameCommit)
.size(12)
- .style(|_: &Theme, _| text_input::Style {
- background: Background::Color(TAB_ACTIVE),
- border: Border {
- color: ACCENT,
- width: 1.0,
- radius: 2.0.into(),
- },
- icon: Color::WHITE,
- placeholder: Color {
- r: 0.5,
- g: 0.5,
- b: 0.5,
- a: 1.0,
- },
- value: Color::WHITE,
- selection: Color {
- r: 0.20,
- g: 0.55,
- b: 0.90,
- a: 0.4,
- },
- })
.padding([3, 6])
.width(Length::Fixed(90.0));
- let cancel_btn = button(crate::ui::icons::tinted(
+ let cancel_btn = button(crate::ui::icons::themed_secondary(
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)),
- border: Border::default(),
- shadow: iced::Shadow::default(),
- snap: false,
- ..Default::default()
- })
+ .style(button::subtle)
.padding([4, 4]);
row![input, cancel_btn]
@@ -788,26 +821,17 @@ fn space_tab<'a>(
.align_y(iced::Center)
.into()
} else {
- // Normal clickable tab β left click switches, right click opens context menu.
- let display = container(text(label.clone()).size(12).color(text_color))
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(bg(is_active, false))),
- border,
- ..Default::default()
- })
+ // Normal clickable tab β left click switches. Paper-layout tabs are
+ // wrapped in `ContextMenu`, which owns right-click handling.
+ let display = container(text(label.clone()).size(12))
+ .style(tab_style)
.padding([4, 10]);
let switch_msg = Message::LayoutSwitch(label.clone());
- let ctx_msg = Message::LayoutContextMenu(label.clone());
-
- // Use mouse_area so we can capture right-click for the context menu.
- // PosReport records the tab's screen bounds so the context menu can
- // anchor next to the clicked tab instead of the screen's left edge
- // (#428).
- let tab = mouse_area(display)
- .on_press(switch_msg)
- .on_right_press(ctx_msg);
- let tab: Element<'a, Message> = if reorderable_layouts.contains(&label) {
+ let has_context_menu = reorderable_layouts.contains(&label);
+ let report_key = format!("SB_LAYOUT_TAB:{label}");
+ let tab = mouse_area(display).on_press(switch_msg);
+ let tab: Element<'a, Message> = if has_context_menu {
crate::ui::wrap_bar::ReorderTab::layout(
label.clone(),
reorderable_layouts,
@@ -818,8 +842,13 @@ fn space_tab<'a>(
tab.into()
};
+ let tab = if has_context_menu {
+ ContextMenu::new(tab, move || layout_tab_context_menu(label.clone())).into()
+ } else {
+ tab
+ };
crate::ui::wrap_bar::PosReport::owned(
- format!("SB_LAYOUT_TAB:{label}"),
+ report_key,
tab,
)
.into()
@@ -845,34 +874,21 @@ fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Mes
("PAPER", false, Some(Message::MspaceCommand))
};
- let text_color = if active {
- SNAP_BORDER_ON
- } else {
- OSNAP_OFF_TEXT
- };
- let bg_normal = if active { SNAP_ON_BG } else { SNAP_OFF_BG };
- let bg_hover = if active {
- SNAP_ON_HOVER
- } else {
- SNAP_OFF_HOVER
- };
- let border_color = if active { SNAP_BORDER_ON } else { BORDER_COLOR };
-
let clickable = on_press.is_some();
- let mut btn = button(text(label).size(12).color(text_color))
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered if clickable => bg_hover,
- _ => bg_normal,
- })),
- border: Border {
- color: border_color,
- width: 1.0,
- radius: 2.0.into(),
- },
- text_color,
- shadow: iced::Shadow::default(),
- snap: false,
+ let mut btn = button(text(label).size(12))
+ .style(move |theme: &Theme, status| {
+ let mut style = button::subtle(theme, status);
+ if active {
+ let palette = theme.extended_palette();
+ style.background = Some(Background::Color(match status {
+ button::Status::Hovered if clickable => palette.primary.base.color,
+ _ => palette.primary.weak.color,
+ }));
+ style.text_color = palette.primary.weak.text;
+ style.border.color = palette.primary.base.color;
+ style.border.width = 1.0;
+ }
+ style
})
.padding([4, 7]);
@@ -884,148 +900,24 @@ fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Mes
}
fn status_pill(label: impl Into) -> Element<'static, Message> {
- container(text(label.into()).size(12).color(Color {
- r: 0.65,
- g: 0.65,
- b: 0.65,
- a: 1.0,
- }))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PILL_BG)),
- border: Border {
- color: BORDER_COLOR,
- width: 1.0,
- radius: 2.0.into(),
- },
- ..Default::default()
- })
+ container(text(label.into()).size(12))
+ .style(container::bordered_box)
.padding([4, 8])
.into()
}
-// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const BAR_BG: Color = Color {
- r: 0.14,
- g: 0.14,
- b: 0.14,
- a: 1.0,
-};
-const TAB_ACTIVE: Color = Color {
- r: 0.25,
- g: 0.25,
- b: 0.25,
- a: 1.0,
-};
-const TAB_HOVER: Color = Color {
- r: 0.20,
- g: 0.20,
- b: 0.20,
- a: 1.0,
-};
-const PILL_BG: Color = Color {
- r: 0.19,
- g: 0.19,
- b: 0.19,
- a: 1.0,
-};
-const BORDER_COLOR: Color = Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
-};
-const ICON_COLOR: Color = Color {
- r: 0.70,
- g: 0.70,
- b: 0.70,
- a: 1.0,
-};
-const DISABLED_COLOR: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.20,
- g: 0.55,
- b: 0.90,
- a: 1.0,
-};
-
-const OSNAP_ON_TEXT: Color = Color {
- r: 0.35,
- g: 0.75,
- b: 1.00,
- a: 1.0,
-};
-const OSNAP_OFF_TEXT: Color = Color {
- r: 0.42,
- g: 0.42,
- b: 0.42,
- a: 1.0,
-};
-const SNAP_ON_BG: Color = Color {
- r: 0.10,
- g: 0.20,
- b: 0.32,
- a: 1.0,
-};
-const SNAP_ON_HOVER: Color = Color {
- r: 0.14,
- g: 0.27,
- b: 0.42,
- a: 1.0,
-};
-const SNAP_BORDER_ON: Color = Color {
- r: 0.20,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
-const SNAP_OFF_BG: Color = Color {
- r: 0.17,
- g: 0.17,
- b: 0.17,
- a: 1.0,
-};
-const SNAP_OFF_HOVER: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
-};
-
// ββ Scale popup button ββββββββββββββββββββββββββββββββββββββββββββββββββββ
-/// A labelled status-bar pill that opens a picker popup on click (units,
-/// scale, β¦). Lit + accent-bordered while its popup is `open`. Shared so the
-/// popup-opening pills don't each re-declare the same button chrome.
-fn popup_pill(label: &str, open: bool, msg: Message) -> Element<'static, Message> {
+/// Shared visual root for labelled status-bar menus (units, scale, β¦).
+fn popup_pill(label: &str) -> Element<'static, Message> {
+ action_pill(label, Message::StatusMenuTooltipHidden(true))
+}
+
+fn action_pill(label: &str, msg: Message) -> Element<'static, Message> {
let label = label.to_string();
- button(
- text(label)
- .size(12)
- .color(if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT }),
- )
+ button(text(label).size(12))
.on_press(msg)
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (open, status) {
- (true, button::Status::Hovered) => SNAP_ON_HOVER,
- (true, _) => SNAP_ON_BG,
- (false, button::Status::Hovered) => SNAP_OFF_HOVER,
- (false, _) => SNAP_OFF_BG,
- })),
- border: Border {
- color: if open { SNAP_BORDER_ON } else { BORDER_COLOR },
- width: 1.0,
- radius: 2.0.into(),
- },
- text_color: if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT },
- shadow: iced::Shadow::default(),
- snap: false,
- })
+ .style(button::subtle)
.padding([4, 7])
.into()
}
diff --git a/src/ui/statusbar/status_menu.rs b/src/ui/statusbar/status_menu.rs
new file mode 100644
index 000000000..48053ba65
--- /dev/null
+++ b/src/ui/statusbar/status_menu.rs
@@ -0,0 +1,80 @@
+//! Shared `iced_aw::MenuBar` plumbing for status-bar menus.
+
+use iced::{Background, Border, Color, Element, Length, Shadow, Theme};
+use iced_aw::menu::{DrawPath, Item, Menu, MenuBar};
+
+use crate::app::Message;
+
+/// One row in a status-bar menu.
+pub struct Entry<'a> {
+ content: Element<'a, Message>,
+ close_on_click: bool,
+}
+
+impl<'a> Entry<'a> {
+ /// Keep the menu open after this row is clicked.
+ pub fn stay(content: impl Into>) -> Self {
+ Self {
+ content: content.into(),
+ close_on_click: false,
+ }
+ }
+
+ /// Close the menu after this row is clicked.
+ pub fn close(content: impl Into>) -> Self {
+ Self {
+ content: content.into(),
+ close_on_click: true,
+ }
+ }
+}
+
+/// Attach a menu to `root`. Each status-bar menu uses one root so the existing
+/// wrapping layout can still move pills independently between rows.
+pub fn menu_bar<'a>(
+ root: impl Into>,
+ entries: Vec>,
+ width: f32,
+) -> Element<'a, Message> {
+ let items = entries
+ .into_iter()
+ .map(|entry| Item::new(entry.content).close_on_click(entry.close_on_click))
+ .collect();
+ let menu = Menu::new(items)
+ .width(Length::Fixed(width))
+ .padding(0)
+ .spacing(0)
+ .offset(1.0)
+ .close_on_background_click(true);
+
+ MenuBar::new(vec![Item::with_menu(root, menu)])
+ .safe_bounds_margin(0.0)
+ .close_on_background_click_global(true)
+ .draw_path(DrawPath::Backdrop)
+ .style(|theme: &Theme, _| {
+ let palette = theme.extended_palette();
+ iced_aw::style::menu_bar::Style {
+ bar_background: Background::Color(Color::TRANSPARENT),
+ bar_border: Border::default(),
+ bar_shadow: Shadow::default(),
+ menu_background: Background::Color(palette.background.weakest.color),
+ menu_border: Border {
+ color: palette.background.neutral.color,
+ width: 1.0,
+ radius: 3.0.into(),
+ },
+ menu_shadow: Shadow {
+ color: palette.background.strongest.color.scale_alpha(0.35),
+ offset: iced::Vector::new(0.0, -2.0),
+ blur_radius: 6.0,
+ },
+ path: Background::Color(palette.primary.weak.color),
+ path_border: Border {
+ color: palette.primary.base.color,
+ width: 1.0,
+ radius: 2.0.into(),
+ },
+ }
+ })
+ .into()
+}
diff --git a/src/ui/statusbar/statusbar_menu.rs b/src/ui/statusbar/statusbar_menu.rs
index d078bae0c..20777b084 100644
--- a/src/ui/statusbar/statusbar_menu.rs
+++ b/src/ui/statusbar/statusbar_menu.rs
@@ -1,105 +1,47 @@
-//! Status-bar customization menu β a checkmark list of every toggle pill,
-//! opened from the bar's far-right handle. Rendered as a floating overlay
-//! above the status bar, same pattern as the scale picker.
+//! Status-bar customization and layout-list menu entries.
-use iced::widget::{button, column, container, mouse_area, row, scrollable, text};
-use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
+use iced::widget::{button, row, text};
+use iced::{Background, Element, Fill, Theme};
use crate::app::Message;
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
+use crate::ui::statusbar::status_menu::Entry;
-/// Full-screen overlay: transparent click-catcher + the menu panel pinned to
-/// the bottom-right, just above the status bar.
-pub fn statusbar_menu_overlay(
- config: &StatusBarConfig,
- pill: Option,
- win: (f32, f32),
-) -> Element<'static, Message> {
- let rows: Vec> = StatusPill::ALL
+pub fn customization_entries(config: &StatusBarConfig) -> Vec> {
+ StatusPill::ALL
.iter()
.map(|&pill| {
- menu_row(
+ Entry::stay(menu_row(
pill.label(),
config.is_visible(pill),
Message::ToggleStatusPill(pill),
- )
+ ))
})
- .collect();
-
- let panel = container(column(rows))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
- .width(Length::Fixed(200.0));
-
- let positioned =
- crate::ui::popup::position_statusbar_popup(panel.into(), pill, win, 200.0, true);
-
- mouse_area(positioned)
- .on_press(Message::CloseStatusBarMenu)
- .into()
+ .collect()
}
-/// Dropdown listing Model + every paper layout, opened from the leftmost
-/// hamburger. Pinned bottom-left just above the status bar; a click selects a
-/// layout (and closes), an outside click just closes.
-pub fn layout_list_overlay<'a>(
+pub fn layout_entries<'a>(
layouts: &[String],
current: &str,
- pill: Option,
- win: (f32, f32),
-) -> Element<'a, Message> {
- let rows: Vec> = layouts
+) -> Vec> {
+ layouts
.iter()
- .map(|name| layout_row(name.clone(), name == current))
- .collect();
-
- let panel = container(scrollable(column(rows)))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
- border: Border {
- color: PANEL_BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- ..Default::default()
- })
- .width(Length::Fixed(200.0))
- .max_height(360.0);
-
- // Hamburger is on the left β prefer left-aligned (grows right).
- let positioned =
- crate::ui::popup::position_statusbar_popup(panel.into(), pill, win, 200.0, false);
-
- mouse_area(positioned)
- .on_press(Message::CloseLayoutList)
- .into()
+ .map(|name| Entry::close(layout_row(name.clone(), name == current)))
+ .collect()
}
fn layout_row<'a>(name: String, is_current: bool) -> Element<'a, Message> {
- let lbl = text(name.clone())
- .size(11)
- .color(if is_current { LABEL_ON } else { LABEL_OFF });
+ let lbl = text(name.clone()).size(11);
button(row![lbl].align_y(iced::Center))
.on_press(Message::LayoutSwitch(name))
- .style(move |_: &Theme, status| button::Style {
- background: Some(Background::Color(match (is_current, status) {
- (_, button::Status::Hovered) => ROW_HOVER,
- (true, _) => Color {
- r: 0.18,
- g: 0.26,
- b: 0.36,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
+ .style(move |theme: &Theme, status| {
+ let mut style = button::subtle(theme, status);
+ if is_current && status == button::Status::Active {
+ let palette = theme.extended_palette();
+ style.background = Some(Background::Color(palette.primary.weak.color));
+ style.text_color = palette.primary.weak.text;
+ }
+ style
})
.width(Fill)
.padding([4, 12])
@@ -107,63 +49,16 @@ fn layout_row<'a>(name: String, is_current: bool) -> Element<'a, Message> {
}
fn menu_row(label: &'static str, checked: bool, msg: Message) -> Element<'static, Message> {
- let check = crate::ui::icons::check_cell(checked, CHECK_COLOR);
+ let check = crate::ui::icons::themed_check_cell(checked);
- let lbl = text(label)
- .size(11)
- .color(if checked { LABEL_ON } else { LABEL_OFF });
+ let lbl = text(label).size(11);
let content = row![check, lbl].spacing(6).align_y(iced::Center);
button(content)
.on_press(msg)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => ROW_HOVER,
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(button::subtle)
.width(Fill)
.padding([4, 10])
.into()
}
-
-// ββ Colours βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const PANEL_BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const PANEL_BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
-};
-const ROW_HOVER: Color = Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
-};
-const CHECK_COLOR: Color = Color {
- r: 0.35,
- g: 0.75,
- b: 1.00,
- a: 1.0,
-};
-const LABEL_ON: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
-const LABEL_OFF: Color = Color {
- r: 0.65,
- g: 0.65,
- b: 0.65,
- a: 1.0,
-};
diff --git a/src/ui/style/anno_object_scale.rs b/src/ui/style/anno_object_scale.rs
index 693919cc0..477e86fb5 100644
--- a/src/ui/style/anno_object_scale.rs
+++ b/src/ui/style/anno_object_scale.rs
@@ -7,16 +7,9 @@
//! the style / scale managers' frame so it looks consistent.
use crate::app::Message;
-use crate::ui::style::style_manager::{hdivider, tb_button, BG, BORDER, DIM, LIST, TB, TEXT};
+use crate::ui::style::style_manager::{hdivider, muted_text_style, tb_button};
use iced::widget::{column, container, mouse_area, row, scrollable, text, Space};
-use iced::{Background, Border, Color, Element, Fill, Theme};
-
-const MEMBER_CHECK: Color = Color {
- r: 0.30,
- g: 0.82,
- b: 0.36,
- a: 1.0,
-};
+use iced::{Background, Border, Element, Fill, Theme};
/// `scales` is `(name, "paper:drawing" ratio, is_member)`. Every label is cloned
/// into the widget tree, so the returned element borrows nothing from the args.
@@ -26,17 +19,17 @@ pub fn view_window(
) -> Element<'static, Message> {
let toolbar = container(
row![
- text(format!("Object: {object_label}"))
- .size(11)
- .color(TEXT),
+ text(format!("Object: {object_label}")).size(11),
Space::new().width(Fill),
tb_button("Close", Message::CloseModal, true),
]
.spacing(4)
.align_y(iced::Center),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TB)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color
+ )),
..Default::default()
})
.width(Fill)
@@ -45,21 +38,17 @@ pub fn view_window(
let rows: Vec> = scales
.iter()
.map(|(name, ratio, member)| {
- let check = crate::ui::icons::check_cell(*member, MEMBER_CHECK);
+ let check = crate::ui::icons::themed_check_cell(*member);
let label = row![
check,
- text(name.clone()).size(11).color(TEXT).width(Fill),
- text(ratio.clone()).size(10).color(DIM),
+ text(name.clone()).size(11).width(Fill),
+ text(ratio.clone()).size(10).style(muted_text_style),
]
.spacing(4)
.align_y(iced::Center);
let cell = container(label)
.padding([4, 8])
- .width(Fill)
- .style(move |_: &Theme| container::Style {
- text_color: Some(TEXT),
- ..Default::default()
- });
+ .width(Fill);
mouse_area(cell)
.on_press(Message::AnnoObjectScaleToggle(name.clone()))
.into()
@@ -67,14 +56,17 @@ pub fn view_window(
.collect();
let list = container(scrollable(column(rows).spacing(1)).height(Fill))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LIST)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
+ }
})
.width(Fill)
.height(Fill)
@@ -84,7 +76,7 @@ pub fn view_window(
column![
text("Click a scale to add or remove the object's representation for it.")
.size(10)
- .color(DIM),
+ .style(muted_text_style),
list,
]
.spacing(6)
@@ -95,8 +87,10 @@ pub fn view_window(
.padding(12);
container(column![toolbar, hdivider(), body])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.width(Fill)
diff --git a/src/ui/style/dimstyle.rs b/src/ui/style/dimstyle.rs
index 375474fdb..07c9ad11e 100644
--- a/src/ui/style/dimstyle.rs
+++ b/src/ui/style/dimstyle.rs
@@ -4,44 +4,7 @@ use crate::app::{ColorPickTarget, DsField, Message};
use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space,
};
-use iced::{Background, Border, Color, Element, Fill, Theme};
-
-const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
-const ACTIVE: Color = Color {
- r: 0.20,
- g: 0.40,
- b: 0.70,
- a: 1.0,
-};
-const FIELD: Color = Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 1.0,
-};
+use iced::{Background, Border, Element, Fill, Theme};
/// All DimStyle field values needed by the view.
pub struct DimStyleValues<'a> {
@@ -131,44 +94,57 @@ pub struct DimStyleValues<'a> {
}
fn tab_btn_style(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
- move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match (active, st) {
- (true, _) => ACTIVE,
- (false, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
- },
- _ => Color {
- r: 0.20,
- g: 0.20,
- b: 0.20,
- a: 1.0,
- },
- })),
- text_color: TEXT,
+ move |theme: &Theme, st| {
+ let palette = theme.extended_palette();
+ let pair = match (active, st) {
+ (true, _) => palette.primary.strong,
+ (false, button::Status::Hovered | button::Status::Pressed) => {
+ palette.background.strong
+ }
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
+ }
}
}
-fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
+fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
+ let border = match status {
+ text_input::Status::Focused { .. } => palette.primary.base.color,
+ _ => palette.background.neutral.color,
+ };
text_input::Style {
- background: Background::Color(FIELD),
+ background: Background::Color(palette.background.base.color),
border: Border {
- color: BORDER,
+ color: border,
width: 1.0,
radius: 3.0.into(),
},
- icon: TEXT,
- placeholder: DIM,
- value: TEXT,
- selection: ACCENT,
+ icon: palette.background.base.text,
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
+ }
+}
+
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
+fn primary_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
}
}
@@ -176,8 +152,10 @@ fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1))
.width(Fill)
.height(1)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
})
.into()
@@ -221,7 +199,7 @@ pub fn view_window<'a>(
]
.spacing(2);
- let lbl = |s: &'static str| text(s).size(11).color(DIM).width(180);
+ let lbl = |s: &'static str| text(s).size(11).style(muted_style).width(180);
let mk_field = |fld: DsField, val: &'a str| -> Element<'a, Message> {
text_input("", val)
@@ -338,7 +316,7 @@ pub fn view_window<'a>(
let tab_content: Element<'_, Message> = match tab {
0 => column![
- text("Dimension Line").size(11).color(ACCENT),
+ text("Dimension Line").size(11).style(primary_style),
row![
lbl("Extension (DIMDLE)"),
mk_field(DsField::Dimdle, vals.dimdle)
@@ -359,7 +337,7 @@ pub fn view_window<'a>(
.align_y(iced::Center),
chk("Suppress 1st line (DIMSD1)", vals.dimsd1, DsField::Dimsd1),
chk("Suppress 2nd line (DIMSD2)", vals.dimsd2, DsField::Dimsd2),
- text("Extension Line").size(11).color(ACCENT),
+ text("Extension Line").size(11).style(primary_style),
row![
lbl("Extension (DIMEXE)"),
mk_field(DsField::Dimexe, vals.dimexe)
@@ -421,7 +399,7 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
1 => column![
- text("Arrows").size(11).color(ACCENT),
+ text("Arrows").size(11).style(primary_style),
hrow(
"Arrowhead (DIMBLK)",
vals.block_opts.clone(),
@@ -485,7 +463,7 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
2 => column![
- text("Text").size(11).color(ACCENT),
+ text("Text").size(11).style(primary_style),
row![
lbl("Height (DIMTXT)"),
mk_field(DsField::Dimtxt, vals.dimtxt)
@@ -551,7 +529,7 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
3 => column![
- text("Scale").size(11).color(ACCENT),
+ text("Scale").size(11).style(primary_style),
chk("Annotative", vals.annotative, DsField::Annotative),
row![
lbl("Overall scale (DIMSCALE)"),
@@ -565,7 +543,7 @@ pub fn view_window<'a>(
]
.spacing(8)
.align_y(iced::Center),
- text("Units").size(11).color(ACCENT),
+ text("Units").size(11).style(primary_style),
enum_field("Format (DIMLUNIT)", DsField::Dimlunit, vals.dimlunit, OPT_LUNIT),
row![
lbl("Decimals (DIMDEC)"),
@@ -636,7 +614,7 @@ pub fn view_window<'a>(
("3", "Leading & trailing"),
],
),
- text("Fit").size(11).color(ACCENT),
+ text("Fit").size(11).style(primary_style),
enum_field(
"Fit (DIMATFIT)",
DsField::Dimatfit,
@@ -680,7 +658,7 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
5 => column![
- text("Alternate Units").size(11).color(ACCENT),
+ text("Alternate Units").size(11).style(primary_style),
chk(
"Enable alternate units (DIMALT)",
vals.dimalt,
@@ -728,7 +706,7 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
_ => column![
- text("Tolerances").size(11).color(ACCENT),
+ text("Tolerances").size(11).style(primary_style),
chk("Generate tolerances (DIMTOL)", vals.dimtol, DsField::Dimtol),
chk("Limits generation (DIMLIM)", vals.dimlim, DsField::Dimlim),
row![
@@ -773,7 +751,7 @@ pub fn view_window<'a>(
// content container instead of the panel padding.
let right_panel = container(
column![
- text(format!("Editing: {selected}")).size(11).color(DIM),
+ text(format!("Editing: {selected}")).size(11).style(muted_style),
tabs,
hdivider(),
scrollable(container(tab_content).padding([12, 12]).width(Fill))
diff --git a/src/ui/style/mleaderstyle.rs b/src/ui/style/mleaderstyle.rs
index e9e7474fa..22c908c31 100644
--- a/src/ui/style/mleaderstyle.rs
+++ b/src/ui/style/mleaderstyle.rs
@@ -4,63 +4,41 @@ use crate::app::Message;
use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input,
};
-use iced::{Background, Border, Color, Element, Fill, Theme};
-
-const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
+use iced::{Background, Border, Element, Fill, Theme};
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
- move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match (accent, st) {
- (true, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.20,
- g: 0.42,
- b: 0.72,
- a: 1.0,
- },
- (false, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
- },
- (true, _) => ACCENT,
- _ => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- },
- })),
- text_color: TEXT,
+ move |theme: &Theme, st| {
+ let palette = theme.extended_palette();
+ let pair = match (accent, st) {
+ (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
+ (false, button::Status::Hovered | button::Status::Pressed) => {
+ palette.background.strong
+ }
+ (true, _) => palette.primary.base,
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
+ }
+ }
+}
+
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
+fn primary_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
}
}
@@ -106,7 +84,7 @@ pub struct MLeaderStyleView<'a> {
}
fn section<'a>(label: &'static str) -> Element<'a, Message> {
- text(label).size(11).color(ACCENT).into()
+ text(label).size(11).style(primary_style).into()
}
fn num_row<'a>(
@@ -116,7 +94,7 @@ fn num_row<'a>(
field: &'static str,
) -> Element<'a, Message> {
row![
- text(label).size(11).color(DIM).width(150),
+ text(label).size(11).style(muted_style).width(150),
text_input(placeholder, value)
.on_input(move |v| Message::MLeaderStyleEdit { field, value: v })
.size(11)
@@ -150,7 +128,7 @@ fn color_row<'a>(
Message::MLeaderColorMore(field),
Message::OpenColorWindow(crate::app::ColorPickTarget::MLeader(field)),
);
- row![text(label).size(11).color(DIM).width(150), selector]
+ row![text(label).size(11).style(muted_style).width(150), selector]
.spacing(8)
.align_y(iced::Center)
.into()
@@ -163,7 +141,7 @@ fn enum_row<'a>(
field: &'static str,
) -> Element<'a, Message> {
row![
- text(label).size(11).color(DIM).width(150),
+ text(label).size(11).style(muted_style).width(150),
pick_list(options, Some(selected), move |value| {
Message::MLeaderStyleSetEnum { field, value }
})
@@ -178,7 +156,7 @@ fn enum_row<'a>(
fn lineweight_row<'a>(selected: acadrust::types::LineWeight) -> Element<'a, Message> {
let selected = crate::ui::properties::LwItem(selected);
row![
- text("Line weight:").size(11).color(DIM).width(150),
+ text("Line weight:").size(11).style(muted_style).width(150),
pick_list(
crate::ui::properties::lw_options(),
Some(selected),
@@ -220,7 +198,7 @@ fn handle_row<'a>(
field: &'static str,
) -> Element<'a, Message> {
row![
- text(label).size(11).color(DIM).width(150),
+ text(label).size(11).style(muted_style).width(150),
pick_list(options, Some(selected), move |value| {
Message::MLeaderStyleSetHandle { field, value }
})
@@ -247,7 +225,7 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> {
scrollable(
column![
row![
- text("Name:").size(11).color(DIM).width(150),
+ text("Name:").size(11).style(muted_style).width(150),
text(s.name.clone()).size(11),
]
.spacing(8),
@@ -422,7 +400,7 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> {
.height(Fill)
.into()
} else {
- container(text("Select a style to view details.").size(11).color(DIM))
+ container(text("Select a style to view details.").size(11).style(muted_style))
.padding([12, 12])
.into()
};
diff --git a/src/ui/style/mlstyle.rs b/src/ui/style/mlstyle.rs
index bfe5ddb17..9a1c22d34 100644
--- a/src/ui/style/mlstyle.rs
+++ b/src/ui/style/mlstyle.rs
@@ -2,14 +2,13 @@
use crate::app::Message;
use iced::widget::{column, container, row, scrollable, text};
-use iced::{Color, Element, Fill};
+use iced::{Element, Fill, Theme};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
pub fn view_window<'a>(
styles: Vec,
@@ -22,7 +21,7 @@ pub fn view_window<'a>(
// ββ Right: Details panel ββββββββββββββββββββββββββββββββββββββββββββββ
let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
row![
- text(label).size(11).color(DIM).width(120),
+ text(label).size(11).style(muted_style).width(120),
text(val).size(11),
]
.spacing(8)
@@ -49,7 +48,7 @@ pub fn view_window<'a>(
&e.linetype
};
row![
- text(format!(" {idx}:")).size(10).color(DIM).width(24),
+ text(format!(" {idx}:")).size(10).style(muted_style).width(24),
text(format!("{:+.3}", e.offset)).size(10).width(70),
text(color_str).size(10).width(90),
text(lt).size(10),
@@ -65,7 +64,7 @@ pub fn view_window<'a>(
info_row("Elements:", s.elements.len().to_string()),
text(" Off Color Ltype")
.size(10)
- .color(DIM)
+ .style(muted_style)
.into(),
];
col_items.extend(elem_rows);
@@ -73,7 +72,7 @@ pub fn view_window<'a>(
.height(Fill)
.into()
} else {
- container(text("Select a style to view details.").size(11).color(DIM))
+ container(text("Select a style to view details.").size(11).style(muted_style))
.padding([12, 12])
.into()
};
diff --git a/src/ui/style/plotstyle.rs b/src/ui/style/plotstyle.rs
index 7db908505..0a4ef8c70 100644
--- a/src/ui/style/plotstyle.rs
+++ b/src/ui/style/plotstyle.rs
@@ -2,108 +2,55 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
-use iced::{Background, Border, Color, Element, Fill, Theme};
-
-const TB: Color = Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 1.0,
-};
-const BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
-const ACTIVE: Color = Color {
- r: 0.20,
- g: 0.40,
- b: 0.70,
- a: 1.0,
-};
-const FIELD: Color = Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 1.0,
-};
-const LIST: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
+use iced::{Background, Border, Element, Fill, Theme};
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
- move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match (accent, st) {
- (true, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.20,
- g: 0.42,
- b: 0.72,
- a: 1.0,
- },
- (false, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
- },
- (true, _) => ACCENT,
- _ => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- },
- })),
- text_color: TEXT,
+ move |theme: &Theme, st| {
+ let palette = theme.extended_palette();
+ let pair = match (accent, st) {
+ (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
+ (false, button::Status::Hovered | button::Status::Pressed) => {
+ palette.background.strong
+ }
+ (true, _) => palette.primary.base,
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
+ }
}
}
-fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
+fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
+ let border = match status {
+ text_input::Status::Focused { .. } => palette.primary.base.color,
+ _ => palette.background.neutral.color,
+ };
text_input::Style {
- background: Background::Color(FIELD),
+ background: Background::Color(palette.background.base.color),
border: Border {
- color: BORDER,
+ color: border,
width: 1.0,
radius: 3.0.into(),
},
- icon: TEXT,
- placeholder: DIM,
- value: TEXT,
- selection: ACCENT,
+ icon: palette.background.base.text,
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
+ }
+}
+
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}
}
@@ -111,8 +58,10 @@ fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1))
.width(Fill)
.height(1)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
})
.into()
@@ -122,8 +71,10 @@ fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill))
.width(1)
.height(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
})
.into()
@@ -156,13 +107,15 @@ pub fn view_window<'a>(
.style(btn_s(false))
.padding([4, 10]),
Space::new().width(Fill),
- text(table_name).size(10).color(DIM),
+ text(table_name).size(10).style(muted_style),
]
.spacing(4)
.align_y(iced::Center),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TB)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color
+ )),
..Default::default()
})
.width(Fill)
@@ -199,19 +152,22 @@ pub fn view_window<'a>(
};
button(text(label).size(10).font(iced::Font::MONOSPACE))
.on_press(Message::PlotStylePanelSelectAci(aci))
- .style(move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match (is_sel, st) {
- (true, _) => ACTIVE,
- (false, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.26,
- g: 0.26,
- b: 0.26,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- text_color: TEXT,
+ .style(move |theme: &Theme, st| {
+ let palette = theme.extended_palette();
+ let pair = match (is_sel, st) {
+ (true, _) => Some(palette.primary.strong),
+ (false, button::Status::Hovered | button::Status::Pressed) => {
+ Some(palette.background.strong)
+ }
+ _ => None,
+ };
+ button::Style {
+ background: pair.map(|p| Background::Color(p.color)),
+ text_color: pair
+ .map(|p| p.text)
+ .unwrap_or(palette.background.base.text),
..Default::default()
+ }
})
.padding([2, 8])
.width(Fill)
@@ -221,16 +177,19 @@ pub fn view_window<'a>(
let aci_list = container(
column![
- text("ACI Color Index").size(10).color(DIM),
+ text("ACI Color Index").size(10).style(muted_style),
container(scrollable(column(aci_items).spacing(1)).height(Fill))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LIST)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
+ }
})
.width(Fill)
.height(Fill)
@@ -269,12 +228,12 @@ pub fn view_window<'a>(
.map(|e| format!("{}%", e.screening))
.unwrap_or_else(|| "β".into());
- let lbl = |s: &'static str| text(s).size(11).color(DIM);
+ let lbl = |s: &'static str| text(s).size(11).style(muted_style);
let edit_panel = container(
column![
row![
- text("ACI:").size(11).color(DIM).width(100),
+ text("ACI:").size(11).style(muted_style).width(100),
text(format!("{selected_aci}")).size(11),
]
.spacing(8)
@@ -298,7 +257,7 @@ pub fn view_window<'a>(
.size(11)
.padding([4, 8]),
Space::new().height(8),
- text("Current values:").size(10).color(DIM),
+ text("Current values:").size(10).style(muted_style),
text(format!(" Color: {cur_color}")).size(10),
text(format!(" Lineweight: {cur_lw}")).size(10),
text(format!(" Screening: {cur_scr}")).size(10),
@@ -318,8 +277,10 @@ pub fn view_window<'a>(
let body = row![aci_list, vsep(), edit_panel].height(Fill);
container(column![toolbar, hdivider(), body].spacing(0))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.width(Fill)
diff --git a/src/ui/style/point_style.rs b/src/ui/style/point_style.rs
index d2a5ee6ad..539b23176 100644
--- a/src/ui/style/point_style.rs
+++ b/src/ui/style/point_style.rs
@@ -7,15 +7,7 @@
use crate::app::Message;
use iced::widget::{button, canvas, column, container, radio, row, text, text_input, Space};
-use iced::{mouse, Background, Border, Color, Element, Length, Point, Rectangle, Size, Theme};
-
-const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
-const WHITE: Color = Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 };
-const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
-const ACCENT: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 };
-const CELL: Color = Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 };
-const CELL_SEL: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 };
-const GLYPH: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
+use iced::{mouse, Background, Border, Element, Length, Point, Rectangle, Size, Theme};
const CELL_PX: f32 = 44.0;
@@ -37,22 +29,23 @@ impl canvas::Program for GlyphCanvas {
&self,
_state: &(),
renderer: &iced::Renderer,
- _theme: &Theme,
+ theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec {
let mut frame = canvas::Frame::new(renderer, bounds.size());
+ let glyph = theme.extended_palette().background.base.text;
let (cx, cy) = (bounds.width * 0.5, bounds.height * 0.5);
let r = bounds.width.min(bounds.height) * 0.30;
let stroke = canvas::Stroke {
width: 1.4,
- style: canvas::Style::Solid(GLYPH),
+ style: canvas::Style::Solid(glyph),
..Default::default()
};
let line = |a: Point, b: Point| canvas::Path::line(a, b);
match self.mode & 0x0F {
- 0 => frame.fill(&canvas::Path::circle(Point::new(cx, cy), 2.4), GLYPH),
+ 0 => frame.fill(&canvas::Path::circle(Point::new(cx, cy), 2.4), glyph),
1 => {}
2 => {
frame.stroke(&line(Point::new(cx - r, cy), Point::new(cx + r, cy)), stroke.clone());
@@ -89,39 +82,52 @@ fn cell<'a>(value: i16, selected: bool) -> Element<'a, Message> {
button(glyph)
.padding(0)
.on_press(Message::PointStyleSetMode(value))
- .style(move |_: &Theme, status| {
- let bg = if selected {
- CELL_SEL
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = if selected {
+ palette.primary.strong
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
- Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
+ palette.background.strong
} else {
- CELL
+ palette.background.weak
};
button::Style {
- background: Some(Background::Color(bg)),
+ background: Some(Background::Color(pair.color)),
border: Border {
- color: Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 },
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
+ text_color: pair.text,
..Default::default()
}
})
.into()
}
-fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
+fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
+ let border = match status {
+ text_input::Status::Focused { .. } => palette.primary.base.color,
+ _ => palette.background.neutral.color,
+ };
text_input::Style {
- background: Background::Color(Color { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }),
+ background: Background::Color(palette.background.base.color),
border: Border {
- color: Color { r: 0.3, g: 0.3, b: 0.3, a: 1.0 },
+ color: border,
width: 1.0,
radius: 4.0.into(),
},
- icon: DIM,
- placeholder: DIM,
- value: WHITE,
- selection: ACCENT,
+ icon: palette.background.base.text,
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
+ }
+}
+
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}
}
@@ -138,7 +144,7 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'
}
let size_row = row![
- text("Point Size:").size(13).color(WHITE),
+ text("Point Size:").size(13),
Space::new().width(10),
text_input("0", size_buf)
.on_input(Message::PointStyleSizeInput)
@@ -147,7 +153,7 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'
.size(13)
.width(110),
Space::new().width(6),
- text(if relative { "%" } else { "units" }).size(12).color(DIM),
+ text(if relative { "%" } else { "units" }).size(12).style(muted_style),
]
.align_y(iced::Center);
@@ -171,26 +177,14 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'
]
.spacing(6);
- let ok = button(text("OK").size(13).color(WHITE))
+ let ok = button(text("OK").size(13))
.padding([5, 22])
.on_press(Message::PointStyleOk)
- .style(|_: &Theme, status| {
- let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
- Color { r: 0.32, g: 0.55, b: 0.85, a: 1.0 }
- } else {
- ACCENT
- };
- button::Style {
- background: Some(Background::Color(bg)),
- text_color: WHITE,
- border: Border { radius: 4.0.into(), ..Default::default() },
- ..Default::default()
- }
- });
+ .style(button::primary);
container(
column![
- text("Point Style").size(18).color(WHITE),
+ text("Point Style").size(18),
Space::new().height(6),
grid,
Space::new().height(12),
@@ -203,8 +197,10 @@ pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'
.spacing(4)
.padding(20),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.into()
diff --git a/src/ui/style/scale_manager.rs b/src/ui/style/scale_manager.rs
index 0f51dfd88..fba9e3895 100644
--- a/src/ui/style/scale_manager.rs
+++ b/src/ui/style/scale_manager.rs
@@ -6,53 +6,36 @@
//! messages instead of the StyleKind machinery.
use crate::app::Message;
-use crate::ui::style::style_manager::{hdivider, tb_button, vsep, BG, BORDER, DIM, LIST, TB, TEXT};
+use crate::ui::style::style_manager::{
+ hdivider, muted_text_style, tb_button, vsep,
+};
use iced::widget::{
column, container, mouse_area, row, scrollable, text, text_input, Space,
};
-use iced::{Background, Border, Color, Element, Fill, Theme};
+use iced::{Background, Border, Element, Fill, Theme};
/// Inline-rename text-input id, so the rename-start handler can focus it.
pub fn rename_input_id() -> iced::widget::Id {
iced::widget::Id::new("scale-rename-input")
}
-const INPUT_BG: Color = Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 1.0,
-};
-const ACTIVE: Color = Color {
- r: 0.20,
- g: 0.40,
- b: 0.70,
- a: 1.0,
-};
-const CURRENT_CHECK: Color = Color {
- r: 0.30,
- g: 0.82,
- b: 0.36,
- a: 1.0,
-};
-
-fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
+fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
+ let border = match status {
+ text_input::Status::Focused { .. } => palette.primary.base.color,
+ _ => palette.background.neutral.color,
+ };
text_input::Style {
- background: Background::Color(INPUT_BG),
+ background: Background::Color(palette.background.base.color),
border: Border {
- color: BORDER,
+ color: border,
width: 1.0,
radius: 4.0.into(),
},
- icon: TEXT,
- placeholder: DIM,
- value: TEXT,
- selection: Color {
- r: 0.20,
- g: 0.46,
- b: 0.80,
- a: 0.45,
- },
+ icon: palette.background.base.text,
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
}
}
@@ -80,8 +63,10 @@ pub fn view_window<'a, 'b>(
.spacing(4)
.align_y(iced::Center),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TB)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color
+ )),
..Default::default()
})
.width(Fill)
@@ -105,21 +90,24 @@ pub fn view_window<'a, 'b>(
}
let is_sel = name.as_str() == selected;
let is_cur = name.eq_ignore_ascii_case(current);
- let check = crate::ui::icons::check_cell(is_cur, CURRENT_CHECK);
+ let check = crate::ui::icons::themed_check_cell(is_cur);
let label = row![
check,
- text(name.clone()).size(11).color(TEXT).width(Fill),
- text(ratio.clone()).size(10).color(DIM),
+ text(name.clone()).size(11).width(Fill),
+ text(ratio.clone()).size(10).style(muted_text_style),
]
.spacing(4)
.align_y(iced::Center);
let cell = container(label)
.padding([4, 8])
.width(Fill)
- .style(move |_: &Theme| container::Style {
- background: is_sel.then_some(Background::Color(ACTIVE)),
- text_color: Some(TEXT),
+ .style(move |theme: &Theme| {
+ let pair = theme.extended_palette().primary.strong;
+ container::Style {
+ background: is_sel.then_some(Background::Color(pair.color)),
+ text_color: is_sel.then_some(pair.text),
..Default::default()
+ }
});
mouse_area(cell)
.on_press(Message::ScaleManagerSelect(name.clone()))
@@ -130,16 +118,19 @@ pub fn view_window<'a, 'b>(
let list_panel = container(
column![
- text("Scales").size(10).color(DIM),
+ text("Scales").size(10).style(muted_text_style),
container(scrollable(column(rows).spacing(1)).height(Fill))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LIST)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
+ }
})
.width(Fill)
.height(Fill)
@@ -161,7 +152,7 @@ pub fn view_window<'a, 'b>(
let field =
|label: &'static str, ph: &'static str, value: &str, on: fn(String) -> Message| {
row![
- text(label).size(11).color(DIM).width(96),
+ text(label).size(11).style(muted_text_style).width(96),
text_input(ph, value)
.on_input(on)
.style(field_style)
@@ -175,13 +166,13 @@ pub fn view_window<'a, 'b>(
let editor = container(
column![
- text("Scale").size(10).color(DIM),
+ text("Scale").size(10).style(muted_text_style),
field("Paper units", "1", paper_buf, Message::ScaleManagerPaperBuf),
field("Drawing units", "50", drawing_buf, Message::ScaleManagerDrawingBuf),
Space::new().height(6),
text("Double-click a scale to rename it; edit its paper : drawing ratio here. New / Copy add a scale. Changes are kept only if you click Apply before closing.")
.size(10)
- .color(DIM),
+ .style(muted_text_style),
]
.spacing(8),
)
@@ -192,8 +183,10 @@ pub fn view_window<'a, 'b>(
let body = row![list_panel, vsep(), editor].height(Fill);
container(column![toolbar, hdivider(), body])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.width(Fill)
diff --git a/src/ui/style/style_list.rs b/src/ui/style/style_list.rs
index 3e0ee8198..14ab3ae98 100644
--- a/src/ui/style/style_list.rs
+++ b/src/ui/style/style_list.rs
@@ -10,7 +10,7 @@
use crate::app::{Message, StyleKind};
use iced::widget::{container, mouse_area, row, text, text_input};
-use iced::{Background, Color, Element, Fill, Theme};
+use iced::{Background, Element, Fill, Theme};
/// Shared id for the inline rename field, so the rename-start handler can focus
/// it the moment the row turns editable.
@@ -18,25 +18,6 @@ pub fn rename_input_id() -> iced::widget::Id {
iced::widget::Id::new("style-rename-input")
}
-const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
-};
-const ACTIVE: Color = Color {
- r: 0.20,
- g: 0.40,
- b: 0.70,
- a: 1.0,
-};
-const CURRENT_CHECK: Color = Color {
- r: 0.30,
- g: 0.82,
- b: 0.36,
- a: 1.0,
-};
-
/// One row of the style list. Renders an editable `text_input` when `name` is
/// the style being renamed (`rename_active`), otherwise a selectable row whose
/// double click starts the rename. The current style gets a green β.
@@ -61,16 +42,19 @@ pub fn item<'a>(
} else {
// Fixed-width β column keeps every name left-aligned whether or not the
// row is current.
- let check = crate::ui::icons::check_cell(is_current, CURRENT_CHECK);
- let label = row![check, text(name.to_string()).size(11).color(TEXT)]
+ let check = crate::ui::icons::themed_check_cell(is_current);
+ let label = row![check, text(name.to_string()).size(11)]
.align_y(iced::Center);
let cell = container(label)
.padding([4, 8])
.width(Fill)
- .style(move |_: &Theme| container::Style {
- background: is_selected.then_some(Background::Color(ACTIVE)),
- text_color: Some(TEXT),
+ .style(move |theme: &Theme| {
+ let pair = theme.extended_palette().primary.strong;
+ container::Style {
+ background: is_selected.then_some(Background::Color(pair.color)),
+ text_color: is_selected.then_some(pair.text),
..Default::default()
+ }
});
mouse_area(cell)
.on_press(on_select)
diff --git a/src/ui/style/style_manager.rs b/src/ui/style/style_manager.rs
index 8307028f3..499fe80d2 100644
--- a/src/ui/style/style_manager.rs
+++ b/src/ui/style/style_manager.rs
@@ -10,52 +10,7 @@
use crate::app::{Message, StyleKind};
use iced::widget::button::{Status, Style};
use iced::widget::{button, column, container, row, scrollable, text, Space};
-use iced::{Background, Border, Color, Element, Fill, Theme};
-
-// Shared palette β also reused by the scale manager so it matches the style
-// managers exactly ([[feedback_shared_infra]]).
-pub(crate) const TB: Color = Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 1.0,
-};
-pub(crate) const BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-pub(crate) const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-pub(crate) const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
-};
-pub(crate) const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
-pub(crate) const LIST: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
+use iced::{Background, Border, Element, Fill, Theme};
/// Everything the shared frame needs. The per-manager `editor` element is the
/// only bespoke part.
@@ -103,8 +58,10 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
.spacing(4)
.align_y(iced::Center);
let toolbar = container(bar)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TB)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color
+ )),
..Default::default()
})
.width(Fill)
@@ -131,16 +88,19 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
let list_panel = container(
column![
- text("Styles").size(10).color(DIM),
+ text("Styles").size(10).style(muted_text_style),
container(scrollable(column(rows).spacing(1)).height(Fill))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LIST)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
+ }
})
.width(Fill)
.height(Fill)
@@ -161,8 +121,10 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
let body = row![list_panel, vsep(), s.editor].height(Fill);
container(column![toolbar, hdivider(), body])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.width(Fill)
@@ -182,35 +144,30 @@ pub(crate) fn tb_button<'a>(label: &'a str, msg: Message, accent: bool) -> Eleme
}
fn btn_s(accent: bool) -> impl Fn(&Theme, Status) -> Style {
- move |_: &Theme, st| Style {
- background: Some(Background::Color(match (accent, st) {
- (true, Status::Hovered | Status::Pressed) => Color {
- r: 0.20,
- g: 0.42,
- b: 0.72,
- a: 1.0,
- },
- (false, Status::Hovered | Status::Pressed) => Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
- },
- (true, _) => ACCENT,
- _ => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- },
- })),
- text_color: TEXT,
+ move |theme: &Theme, st| {
+ let palette = theme.extended_palette();
+ let pair = match (accent, st) {
+ (true, Status::Hovered | Status::Pressed) => palette.primary.strong,
+ (false, Status::Hovered | Status::Pressed) => palette.background.strong,
+ (true, _) => palette.primary.base,
+ _ => palette.background.weak,
+ };
+ Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
+ }
+ }
+}
+
+pub(crate) fn muted_text_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}
}
@@ -218,8 +175,10 @@ pub(crate) fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1))
.width(Fill)
.height(1)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
})
.into()
@@ -229,8 +188,10 @@ pub(crate) fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill))
.width(1)
.height(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
})
.into()
diff --git a/src/ui/style/tablestyle.rs b/src/ui/style/tablestyle.rs
index c5bbfc0bd..b67505244 100644
--- a/src/ui/style/tablestyle.rs
+++ b/src/ui/style/tablestyle.rs
@@ -4,63 +4,41 @@ use crate::app::Message;
use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Column,
};
-use iced::{Background, Border, Color, Element, Fill, Theme};
-
-const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
+use iced::{Background, Border, Element, Fill, Theme};
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
- move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match (accent, st) {
- (true, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.20,
- g: 0.42,
- b: 0.72,
- a: 1.0,
- },
- (false, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
- },
- (true, _) => ACCENT,
- _ => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- },
- })),
- text_color: TEXT,
+ move |theme: &Theme, st| {
+ let palette = theme.extended_palette();
+ let pair = match (accent, st) {
+ (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
+ (false, button::Status::Hovered | button::Status::Pressed) => {
+ palette.background.strong
+ }
+ (true, _) => palette.primary.base,
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
+ }
+ }
+}
+
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
+fn primary_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
}
}
@@ -89,7 +67,7 @@ pub fn view_window<'a>(
// ββ Right: Details panel ββββββββββββββββββββββββββββββββββββββββββββββ
let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
row![
- text(label).size(11).color(DIM).width(160),
+ text(label).size(11).style(muted_style).width(160),
text(val).size(11),
]
.spacing(8)
@@ -108,7 +86,7 @@ pub fn view_window<'a>(
field: &'static str|
-> Element<'a, Message> {
row![
- text(label).size(11).color(DIM).width(150),
+ text(label).size(11).style(muted_style).width(150),
text_input(placeholder, value)
.on_input(move |v| Message::TableStyleCellEdit {
row,
@@ -142,14 +120,14 @@ pub fn view_window<'a>(
Message::TableColorMore(row, field),
Message::OpenColorWindow(crate::app::ColorPickTarget::Table(row, field)),
);
- row![text(label).size(11).color(DIM).width(150), selector]
+ row![text(label).size(11).style(muted_style).width(150), selector]
.spacing(8)
.align_y(iced::Center)
.into()
};
let mut col = Column::new()
.spacing(3)
- .push(text(row_label).size(11).color(ACCENT))
+ .push(text(row_label).size(11).style(primary_style))
.push(cell_in(
" Text style:",
"Standard",
@@ -161,7 +139,7 @@ pub fn view_window<'a>(
.push(cell_color(" Fill color:", &cell_fillcolor[r], "fillcolor"))
.push(
row![
- text(" Alignment:").size(11).color(DIM).width(150),
+ text(" Alignment:").size(11).style(muted_style).width(150),
pick_list(
[
"TopLeft",
@@ -199,7 +177,7 @@ pub fn view_window<'a>(
.push(
text(" Borders (type / weight / color / spacing / hidden)")
.size(10)
- .color(DIM),
+ .style(muted_style),
);
let borders: [(&'static str, &acadrust::objects::TableCellBorder); 6] = [
@@ -214,7 +192,7 @@ pub fn view_window<'a>(
let bu = b as u8;
col = col.push(
row![
- text(format!(" {bname}")).size(11).color(DIM).width(28),
+ text(format!(" {bname}")).size(11).style(muted_style).width(28),
pick_list(
["Single", "Double"]
.iter()
@@ -282,7 +260,7 @@ pub fn view_window<'a>(
column![
info_row("Name:", s.name.clone()),
row![
- text("Description:").size(11).color(DIM).width(160),
+ text("Description:").size(11).style(muted_style).width(160),
text_input("", description_buf)
.on_input(|v| Message::TableStyleEdit {
field: "description",
@@ -294,7 +272,7 @@ pub fn view_window<'a>(
.spacing(8)
.align_y(iced::Center),
row![
- text("Flow direction:").size(11).color(DIM).width(160),
+ text("Flow direction:").size(11).style(muted_style).width(160),
pick_list(
["Down", "Up"]
.iter()
@@ -314,7 +292,7 @@ pub fn view_window<'a>(
.size(14)
.text_size(11),
row![
- text("H Margin:").size(11).color(DIM).width(160),
+ text("H Margin:").size(11).style(muted_style).width(160),
text_input("1.5", hmargin_buf)
.on_input(|v| Message::TableStyleEdit {
field: "hmargin",
@@ -326,7 +304,7 @@ pub fn view_window<'a>(
.spacing(8)
.align_y(iced::Center),
row![
- text("V Margin:").size(11).color(DIM).width(160),
+ text("V Margin:").size(11).style(muted_style).width(160),
text_input("1.5", vmargin_buf)
.on_input(|v| Message::TableStyleEdit {
field: "vmargin",
@@ -362,7 +340,7 @@ pub fn view_window<'a>(
.height(Fill)
.into()
} else {
- container(text("Select a style to view details.").size(11).color(DIM))
+ container(text("Select a style to view details.").size(11).style(muted_style))
.padding([12, 12])
.into()
};
diff --git a/src/ui/style/textstyle.rs b/src/ui/style/textstyle.rs
index 16a3cbab2..9983f6fdb 100644
--- a/src/ui/style/textstyle.rs
+++ b/src/ui/style/textstyle.rs
@@ -5,7 +5,7 @@ use crate::app::StyleKind;
use iced::widget::{
button, canvas, checkbox, column, container, row, scrollable, text, text_input, Space,
};
-use iced::{mouse, Background, Border, Color, Element, Fill, Length, Point, Rectangle, Theme};
+use iced::{mouse, Background, Border, Element, Fill, Length, Point, Rectangle, Theme};
/// View-model for the Text Style editor window.
pub struct TextStyleView<'a> {
@@ -28,49 +28,6 @@ pub struct TextStyleView<'a> {
pub rename_buf: &'a str,
}
-const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
-const ACTIVE: Color = Color {
- r: 0.20,
- g: 0.40,
- b: 0.70,
- a: 1.0,
-};
-const FIELD: Color = Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 1.0,
-};
-const LIST: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
-
const BUILTIN_FONTS: &[&str] = &[
"Standard", "ISO", "Simplex", "RomanS", "RomanD", "RomanC", "RomanT", "ItalicC", "ItalicT",
"ScriptS", "ScriptC", "GothGBT", "GothGRT", "GothITT", "GreekC", "Symbol",
@@ -78,34 +35,52 @@ const BUILTIN_FONTS: &[&str] = &[
];
fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
- move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match (active, st) {
- (true, _) => ACTIVE,
- (false, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.26,
- g: 0.26,
- b: 0.26,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- text_color: TEXT,
+ move |theme: &Theme, st| {
+ let palette = theme.extended_palette();
+ let pair = match (active, st) {
+ (true, _) => Some(palette.primary.strong),
+ (false, button::Status::Hovered | button::Status::Pressed) => {
+ Some(palette.background.strong)
+ }
+ _ => None,
+ };
+ button::Style {
+ background: pair.map(|p| Background::Color(p.color)),
+ text_color: pair.map(|p| p.text).unwrap_or(palette.background.base.text),
..Default::default()
+ }
}
}
-fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
+fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
+ let border = match status {
+ text_input::Status::Focused { .. } => palette.primary.base.color,
+ _ => palette.background.neutral.color,
+ };
text_input::Style {
- background: Background::Color(FIELD),
+ background: Background::Color(palette.background.base.color),
border: Border {
- color: BORDER,
+ color: border,
width: 1.0,
radius: 3.0.into(),
},
- icon: TEXT,
- placeholder: DIM,
- value: TEXT,
- selection: ACCENT,
+ icon: palette.background.base.text,
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
+ }
+}
+
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
+fn primary_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
}
}
@@ -113,8 +88,10 @@ fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill))
.width(1)
.height(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
})
.into()
@@ -143,7 +120,7 @@ impl canvas::Program for TextPreviewCanvas {
&self,
_state: &(),
renderer: &iced::Renderer,
- _theme: &Theme,
+ theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec {
@@ -185,7 +162,7 @@ impl canvas::Program for TextPreviewCanvas {
};
let stroke = canvas::Stroke {
width: 1.4,
- style: canvas::Style::Solid(TEXT),
+ style: canvas::Style::Solid(theme.extended_palette().background.base.text),
..Default::default()
};
for s in &strokes {
@@ -237,16 +214,19 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
let font_panel = container(
column![
- text("Font File").size(10).color(DIM),
+ text("Font File").size(10).style(muted_style),
container(scrollable(column(font_items).spacing(1)).height(Fill))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LIST)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
+ }
})
.width(Fill)
.height(Fill)
@@ -275,7 +255,7 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
field: &'static str,
) -> Element<'a, Message> {
row![
- text(label).size(11).color(DIM).width(120),
+ text(label).size(11).style(muted_style).width(120),
text_input(ph, buf)
.on_input(move |v| Message::TextStyleEdit { field, value: v })
.style(field_style)
@@ -312,7 +292,7 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
// ββ Right: Properties βββββββββββββββββββββββββββββββββββββββββββββββββ
let props_panel = container(
column![
- text("Properties").size(11).color(ACCENT),
+ text("Properties").size(11).style(primary_style),
frow("Big Font:", "big-font fileβ¦", bigfont_buf, "bigfont"),
frow("TrueType Font:", "e.g. Arial", ttf_buf, "ttf"),
frow("Fixed Height:", "0 = variable", height_buf, "height"),
@@ -337,16 +317,19 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
.size(15)
.text_size(11),
Space::new().height(8),
- text("Preview").size(10).color(DIM),
+ text("Preview").size(10).style(muted_style),
container(preview)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(FIELD)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 4.0.into()
},
..Default::default()
+ }
})
.padding(8)
.width(Fill),
@@ -382,16 +365,19 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
let ttf_panel = container(
column![
- text("TrueType (system)").size(10).color(DIM),
+ text("TrueType (system)").size(10).style(muted_style),
container(scrollable(column(ttf_items).spacing(1)).height(Fill))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LIST)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
+ }
})
.width(Fill)
.height(Fill)
diff --git a/src/ui/window/about.rs b/src/ui/window/about.rs
index ce170cb3e..53c3d4313 100644
--- a/src/ui/window/about.rs
+++ b/src/ui/window/about.rs
@@ -1,48 +1,16 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, text};
-use iced::{Background, Border, Color, Element, Theme};
-
-const BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const PANEL: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
-const BORDER: Color = Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
-const WHITE: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
+use iced::{Background, Element, Theme};
fn info_row<'a>(label: &'static str, value: String) -> Element<'a, Message> {
row![
- text(label).size(11).color(DIM).width(100),
- text(value).size(11).color(WHITE),
+ text(label)
+ .size(11)
+ .style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ })
+ .width(100),
+ text(value).size(11),
]
.spacing(8)
.align_y(iced::Center)
@@ -57,10 +25,16 @@ pub fn view_window<'a>() -> Element<'a, Message> {
let logo = container(
column![
- text("Open CAD Studio").size(32).color(ACCENT),
+ text("Open CAD Studio")
+ .size(32)
+ .style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
+ }),
text("CAD application for Architecture & Engineering")
.size(11)
- .color(DIM),
+ .style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }),
]
.spacing(4)
.align_x(iced::Center),
@@ -82,35 +56,11 @@ pub fn view_window<'a>() -> Element<'a, Message> {
.spacing(2)
.padding([12, 16]),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL)),
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- });
+ .style(container::bordered_box);
let copy_btn = button(text("Copy Info").size(11))
.on_press(Message::AboutCopyInfo)
- .style(|_: &Theme, st| button::Style {
- background: Some(Background::Color(match st {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.20,
- g: 0.42,
- b: 0.72,
- a: 1.0,
- },
- _ => ACCENT,
- })),
- text_color: WHITE,
- border: Border {
- radius: 4.0.into(),
- ..Default::default()
- },
- ..Default::default()
- })
+ .style(button::primary)
.padding([6, 16]);
let footer = row![copy_btn]
@@ -132,8 +82,10 @@ pub fn view_window<'a>() -> Element<'a, Message> {
left: 20.0,
}),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.into()
diff --git a/src/ui/window/alias_editor.rs b/src/ui/window/alias_editor.rs
index 6309beceb..6f453802e 100644
--- a/src/ui/window/alias_editor.rs
+++ b/src/ui/window/alias_editor.rs
@@ -6,7 +6,7 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
-use iced::{Background, Border, Color, Element, Fill, Length, Theme};
+use iced::{Background, Element, Fill, Length, Theme};
/// Which column of an alias row a text edit targets.
#[derive(Clone, Copy, Debug)]
@@ -18,33 +18,21 @@ pub enum AliasField {
/// Right-hand lane reserved for the scrollbar so it never overlaps the β column.
const GUTTER: f32 = 16.0;
-const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
-const FIELD_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 };
-const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
-const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
-const WHITE: Color = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 };
-const ADD_C: Color = Color { r: 0.20, g: 0.40, b: 0.62, a: 1.0 };
-
-fn field_style(_: &Theme, _s: text_input::Status) -> text_input::Style {
- text_input::Style {
- background: Background::Color(FIELD_BG),
- border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
- icon: WHITE,
- placeholder: DIM,
- value: WHITE,
- selection: Color { r: 0.25, g: 0.40, b: 0.60, a: 1.0 },
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}
}
/// Build the alias editor content. `rows` is the live working buffer.
pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
- let title = text("Command Aliases").size(15).color(WHITE);
+ let title = text("Command Aliases").size(15);
let hint = text(
"Type an alias and the command it runs (e.g. L β LINE). \
Apply to save to ocad.pgp; closing discards unapplied edits.",
)
.size(11)
- .color(DIM);
+ .style(muted_style);
// Right gutter reserved so the scrollbar has its own lane and never sits on
// top of the row delete (β) buttons. Applied to both the header and the
@@ -53,8 +41,8 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
let head = container(
row![
- container(text("Alias").size(11).color(DIM)).width(Length::Fixed(120.0)),
- container(text("Command").size(11).color(DIM)).width(Fill),
+ container(text("Alias").size(11).style(muted_style)).width(Length::Fixed(120.0)),
+ container(text("Command").size(11).style(muted_style)).width(Fill),
Space::new().width(Length::Fixed(30.0)),
]
.spacing(8),
@@ -65,31 +53,18 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
for (idx, (alias, cmd)) in rows.iter().enumerate() {
let alias_box = text_input("alias", alias)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v })
- .style(field_style)
.size(13)
.padding([3, 6])
.width(Length::Fixed(120.0));
let cmd_box = text_input("command", cmd)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v })
- .style(field_style)
.size(13)
.padding([3, 6])
.width(Fill);
- let del = button(crate::ui::icons::tinted(crate::ui::icons::CLOSE, 12.0, WHITE))
+ let del = button(crate::ui::icons::themed_danger(crate::ui::icons::CLOSE, 12.0))
.on_press(Message::AliasEditorRemove(idx))
.padding([2, 6])
- .style(|_: &Theme, status| {
- let bg = if matches!(status, button::Status::Hovered) {
- Color { r: 0.45, g: 0.22, b: 0.22, a: 1.0 }
- } else {
- Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }
- };
- button::Style {
- background: Some(Background::Color(bg)),
- border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
- ..Default::default()
- }
- });
+ .style(button::danger);
list = list.push(
row![alias_box, cmd_box, del]
.spacing(8)
@@ -97,40 +72,16 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
);
}
- let add = button(text("+ Add alias").size(12).color(WHITE))
+ let add = button(text("+ Add alias").size(12))
.on_press(Message::AliasEditorAdd)
.padding([4, 10])
- .style(|_: &Theme, status| {
- let bg = if matches!(status, button::Status::Hovered) {
- Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 }
- } else {
- Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }
- };
- button::Style {
- background: Some(Background::Color(bg)),
- text_color: WHITE,
- border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
- ..Default::default()
- }
- });
+ .style(button::secondary);
// Apply β primary action; commits the rows to ocad.pgp and stays open.
- let apply = button(text("Apply").size(12).color(WHITE))
+ let apply = button(text("Apply").size(12))
.on_press(Message::AliasEditorApply)
.padding([4, 16])
- .style(|_: &Theme, status| {
- let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
- Color { r: 0.24, g: 0.46, b: 0.74, a: 1.0 }
- } else {
- ADD_C
- };
- button::Style {
- background: Some(Background::Color(bg)),
- text_color: WHITE,
- border: Border { radius: 4.0.into(), ..Default::default() },
- ..Default::default()
- }
- });
+ .style(button::primary);
container(
column![
@@ -149,8 +100,10 @@ pub fn view_window(rows: &[(String, String)]) -> Element<'_, Message> {
.padding(12)
.width(Fill)
.height(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.into()
diff --git a/src/ui/window/attribute_editor.rs b/src/ui/window/attribute_editor.rs
index 5c2efae05..ba3c1915c 100644
--- a/src/ui/window/attribute_editor.rs
+++ b/src/ui/window/attribute_editor.rs
@@ -19,23 +19,10 @@ use acadrust::types::{Color as AcadColor, LineWeight};
use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space,
};
-use iced::{Background, Border, Color, Element, Length, Theme};
+use iced::{Background, Border, Element, Length, Theme};
use crate::ui::properties::{lw_options, LwItem};
-// Palette shared with the style-manager windows so the editor reads as one of
-// them: toolbar (TB) on top, BG panels, ACTIVE for the selected tab / row.
-const TB: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 };
-const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
-const WHITE: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
-const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
-const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
-const FIELD_BG: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 };
-const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
-const ACTIVE: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
-const ROW_SEL: Color = ACTIVE;
-const ROW_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 };
-
const LABEL_W: f32 = 120.0;
/// Which tab of the editor is showing.
@@ -146,28 +133,25 @@ pub fn color_from_label(label: &str) -> Option {
})
}
-fn field_style(_t: &Theme, _s: text_input::Status) -> text_input::Style {
- text_input::Style {
- background: Background::Color(FIELD_BG),
- border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
- icon: WHITE,
- placeholder: DIM,
- value: WHITE,
- selection: ACCENT,
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}
}
-fn accent_btn(_t: &Theme, status: button::Status) -> button::Style {
- let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
- Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 }
- } else {
- ACCENT
+fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
+ let border = match status {
+ text_input::Status::Focused { .. } => palette.primary.base.color,
+ _ => palette.background.neutral.color,
};
- button::Style {
- background: Some(Background::Color(bg)),
- text_color: WHITE,
- border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
- ..Default::default()
+ text_input::Style {
+ background: Background::Color(palette.background.base.color),
+ border: Border { color: border, width: 1.0, radius: 3.0.into() },
+ icon: palette.background.base.text,
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
}
}
@@ -176,8 +160,10 @@ fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Length::Fill).height(1))
.width(Length::Fill)
.height(1)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
})
.into()
@@ -186,7 +172,7 @@ fn hdivider<'a>() -> Element<'a, Message> {
/// One `label : widget` row with a fixed-width label column.
fn field_row<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> {
row![
- container(text(label).size(12).color(DIM)).width(LABEL_W),
+ container(text(label).size(12).style(muted_style)).width(LABEL_W),
widget,
]
.spacing(8)
@@ -226,20 +212,28 @@ fn pick_field<'a>(
fn tab_button<'a>(label: &'a str, this: AttrTab, active: AttrTab) -> Element<'a, Message> {
let is_active = this == active;
- button(text(label).size(11).color(WHITE))
+ button(text(label).size(11))
.padding([4, 12])
.on_press(Message::AttrEditorTab(this))
- .style(move |_t: &Theme, status| button::Style {
- background: Some(Background::Color(match (is_active, status) {
- (true, _) => ACTIVE,
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match (is_active, status) {
+ (true, _) => palette.primary.strong,
(false, button::Status::Hovered | button::Status::Pressed) => {
- Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
+ palette.background.strong
}
- _ => Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 },
- })),
- text_color: WHITE,
- border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
+ border: Border {
+ color: palette.background.neutral.color,
+ width: 1.0,
+ radius: 3.0.into(),
+ },
..Default::default()
+ }
})
.into()
}
@@ -259,20 +253,22 @@ pub fn view_window<'a>(
) -> Element<'a, Message> {
// ββ Top toolbar: block name on the left, Apply on the right βββββββββββ
// Mirrors the style-manager windows (actions left, primary action right).
- let apply = button(text("Apply").size(11).color(WHITE))
+ let apply = button(text("Apply").size(11))
.padding([4, 14])
.on_press(Message::AttrEditorApply)
- .style(accent_btn);
+ .style(button::primary);
let toolbar = container(
row![
- text(format!("Block: {block}")).size(12).color(DIM),
+ text(format!("Block: {block}")).size(12).style(muted_style),
Space::new().width(Length::Fill),
apply,
]
.align_y(iced::Center),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TB)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color
+ )),
..Default::default()
})
.width(Length::Fill)
@@ -286,7 +282,7 @@ pub fn view_window<'a>(
.spacing(2);
let body: Element<'_, Message> = if rows.is_empty() {
- text("This block has no attributes.").size(13).color(DIM).into()
+ text("This block has no attributes.").size(13).style(muted_style).into()
} else {
match tab {
AttrTab::Attribute => attribute_tab(rows, selected),
@@ -303,8 +299,10 @@ pub fn view_window<'a>(
.padding(12);
container(column![toolbar, hdivider(), content])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.width(Length::Fill)
@@ -315,9 +313,9 @@ pub fn view_window<'a>(
/// Attribute tab: tag / prompt / value list with row-select, plus a value box.
fn attribute_tab<'a>(rows: &'a [AttrRow], selected: usize) -> Element<'a, Message> {
let head = row![
- container(text("Tag").size(11).color(DIM)).width(130),
- container(text("Prompt").size(11).color(DIM)).width(Length::Fill),
- container(text("Value").size(11).color(DIM)).width(140),
+ container(text("Tag").size(11).style(muted_style)).width(130),
+ container(text("Prompt").size(11).style(muted_style)).width(Length::Fill),
+ container(text("Value").size(11).style(muted_style)).width(140),
]
.spacing(6);
@@ -325,27 +323,28 @@ fn attribute_tab<'a>(rows: &'a [AttrRow], selected: usize) -> Element<'a, Messag
for (idx, r) in rows.iter().enumerate() {
let is_sel = idx == selected;
let line = row![
- container(text(r.tag.as_str()).size(12).color(WHITE)).width(130),
- container(text(r.prompt.as_str()).size(12).color(DIM)).width(Length::Fill),
- container(text(r.value.as_str()).size(12).color(WHITE)).width(140),
+ container(text(r.tag.as_str()).size(12)).width(130),
+ container(text(r.prompt.as_str()).size(12).style(muted_style)).width(Length::Fill),
+ container(text(r.value.as_str()).size(12)).width(140),
]
.spacing(6);
let btn = button(line)
.on_press(Message::AttrEditorSelect(idx))
.padding([3, 4])
.width(Length::Fill)
- .style(move |_t: &Theme, status| {
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
let hovered = matches!(status, button::Status::Hovered);
- let bg = if is_sel {
- ROW_SEL
+ let pair = if is_sel {
+ palette.primary.strong
} else if hovered {
- Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0 }
+ palette.background.strong
} else {
- ROW_BG
+ palette.background.weak
};
button::Style {
- background: Some(Background::Color(bg)),
- text_color: WHITE,
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
border: Border::default(),
..Default::default()
}
diff --git a/src/ui/window/layers.rs b/src/ui/window/layers.rs
index 1fcdc27ba..40583c844 100644
--- a/src/ui/window/layers.rs
+++ b/src/ui/window/layers.rs
@@ -47,6 +47,35 @@ const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0;
/// scrolled into view after it is added (#271).
pub const LAYER_TABLE_SCROLL_ID: &str = "layer-manager-table-scroll";
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
+fn table_input_style(
+ theme: &Theme,
+ status: iced::widget::text_input::Status,
+) -> iced::widget::text_input::Style {
+ let palette = theme.extended_palette();
+ let border = match status {
+ iced::widget::text_input::Status::Focused { .. } => palette.primary.base.color,
+ _ => palette.background.neutral.color,
+ };
+ iced::widget::text_input::Style {
+ background: Background::Color(palette.background.base.color),
+ border: Border {
+ color: border,
+ width: 1.0,
+ radius: 2.0.into(),
+ },
+ icon: palette.background.base.text,
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
+ }
+}
+
// ββ Layer data ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#[derive(Clone, Debug)]
@@ -313,39 +342,15 @@ impl LayerPanel {
.size(FONT_SZ)
.padding([3, 6])
.width(Length::Fixed(180.0))
- .style(|_: &Theme, _| iced::widget::text_input::Style {
- background: Background::Color(Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 1.0,
- }),
- border: Border {
- color: BORDER_COLOR,
- width: 1.0,
- radius: 2.0.into(),
- },
- icon: Color::WHITE,
- placeholder: Color {
- r: 0.45,
- g: 0.45,
- b: 0.45,
- a: 1.0,
- },
- value: Color::WHITE,
- selection: Color {
- r: 0.20,
- g: 0.44,
- b: 0.72,
- a: 0.5,
- },
- }),
+ .style(table_input_style),
]
.spacing(2)
.align_y(iced::Center),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TOOLBAR_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color
+ )),
..Default::default()
})
.width(Fill)
@@ -355,13 +360,15 @@ impl LayerPanel {
let sc = self.sort_col;
let sa = self.sort_asc;
let mut header_row = row![
- text("Status").size(10).color(DIM).width(50),
+ text("Status").size(10).style(muted_style).width(50),
sortable_header("Name", LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa),
// Draggable divider: adjusts the Name column width (#359).
iced::widget::mouse_area(
container(iced::widget::Space::new().width(2).height(14)).style(
- |_: &Theme| container::Style {
- background: Some(Background::Color(BORDER_COLOR)),
+ |theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
},
),
@@ -390,20 +397,23 @@ impl LayerPanel {
header_row = header_row.push(
text(vp.label.as_str())
.size(10)
- .color(DIM)
+ .style(muted_style)
.width(Length::Fixed(COL_ICON)),
);
}
let col_header = container(header_row)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(COL_HEADER_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
+ }
})
.padding([4, 8])
.width(Fill);
@@ -451,8 +461,10 @@ impl LayerPanel {
// ββ Full-window frame βββββββββββββββββββββββββββββββββββββββββββββ
container(column![toolbar, col_header, table].spacing(0))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.width(Fill)
@@ -463,6 +475,48 @@ impl LayerPanel {
// ββ Sorting helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+fn layer_cell_button_style(
+ theme: &Theme,
+ status: button::Status,
+ is_selected: bool,
+ index: usize,
+) -> button::Style {
+ let palette = theme.extended_palette();
+ let highlighted = matches!(status, button::Status::Hovered);
+ let pair = if highlighted {
+ palette.background.strong
+ } else if is_selected {
+ palette.primary.weak
+ } else if index % 2 == 0 {
+ palette.background.base
+ } else {
+ palette.background.weak
+ };
+ button::Style {
+ background: highlighted.then_some(Background::Color(pair.color)),
+ text_color: pair.text,
+ ..Default::default()
+ }
+}
+
+fn layer_header_button_style(theme: &Theme, status: button::Status) -> button::Style {
+ let palette = theme.extended_palette();
+ let highlighted = matches!(
+ status,
+ button::Status::Hovered | button::Status::Pressed
+ );
+ let pair = if highlighted {
+ palette.background.strong
+ } else {
+ palette.background.weak
+ };
+ button::Style {
+ background: highlighted.then_some(Background::Color(pair.color)),
+ text_color: pair.text,
+ ..Default::default()
+ }
+}
+
/// Packed RGB key for ordering colours deterministically by hue-ish bytes.
fn color_sort_key(c: Color) -> u32 {
let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u32;
@@ -478,30 +532,19 @@ fn sortable_header<'a>(
active: Option,
asc: bool,
) -> Element<'a, Message> {
- let mut content = row![text(label).size(10).color(DIM)]
+ let mut content = row![text(label).size(10).style(muted_style)]
.spacing(3)
.align_y(iced::Center);
if active == Some(col) {
content = content.push(if asc {
- crate::ui::icons::arrow_up(8.0, DIM)
+ crate::ui::icons::themed_arrow_up(8.0)
} else {
- crate::ui::icons::arrow_down(8.0, DIM)
+ crate::ui::icons::themed_arrow_down(8.0)
});
}
button(content)
.on_press(Message::LayerSort(col))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
- })
+ .style(layer_header_button_style)
.padding(Padding {
top: 0.0,
bottom: 0.0,
@@ -517,41 +560,31 @@ fn sortable_header<'a>(
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),
+ crate::ui::icons::themed(icon, 12.0),
+ text(label).size(11),
]
.spacing(5)
.align_y(iced::Center),
)
.on_press(msg)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
- },
- button::Status::Pressed => Color {
- r: 0.25,
- g: 0.25,
- b: 0.25,
- a: 1.0,
- },
- _ => Color {
- r: 0.26,
- g: 0.26,
- b: 0.26,
- a: 1.0,
- },
- })),
+ .style(|theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match status {
+ button::Status::Hovered | button::Status::Pressed => {
+ palette.background.strong
+ }
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
border: Border {
radius: 3.0.into(),
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
},
- text_color: Color::WHITE,
+ text_color: pair.text,
..Default::default()
+ }
})
.padding([4, 10])
.into()
@@ -563,46 +596,46 @@ fn toolbar_btn_cond<'a>(
msg: Message,
enabled: bool,
) -> Element<'a, Message> {
- let fg = if enabled {
- Color::WHITE
- } else {
- Color {
- r: 0.45,
- g: 0.45,
- 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),
+ if enabled {
+ crate::ui::icons::themed(icon, 12.0)
+ } else {
+ crate::ui::icons::themed_disabled(icon, 12.0)
+ },
+ if enabled {
+ text(label).size(11)
+ } else {
+ text(label).size(11).style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(
+ theme.extended_palette().background.base.text.scale_alpha(0.42)
+ ),
+ })
+ },
]
.spacing(5)
.align_y(iced::Center),
)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
- },
- _ => Color {
- r: 0.26,
- g: 0.26,
- b: 0.26,
- a: 1.0,
- },
- })),
+ .style(move |theme: &Theme, status| {
+ let palette = theme.extended_palette();
+ let pair = match status {
+ button::Status::Hovered if enabled => palette.background.strong,
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
border: Border {
radius: 3.0.into(),
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
},
- text_color: Color::WHITE,
+ text_color: if enabled {
+ pair.text
+ } else {
+ pair.text.scale_alpha(0.42)
+ },
..Default::default()
+ }
})
.padding([4, 10]);
if enabled {
@@ -616,21 +649,25 @@ fn toolbar_btn_cond<'a>(
#[allow(clippy::too_many_arguments)]
/// Hover popup showing a layer's full name when the cell truncates it.
fn name_tip<'a>(name: &'a str) -> Element<'a, Message> {
- container(text(name).size(FONT_SZ).color(ROW_TEXT))
+ container(text(name).size(FONT_SZ))
.padding(Padding {
top: 3.0,
bottom: 3.0,
left: 7.0,
right: 7.0,
})
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL_BG)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.strong.color)),
border: Border {
- color: BORDER_COLOR,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
+ text_color: Some(palette.background.strong.text),
..Default::default()
+ }
})
.into()
}
@@ -649,23 +686,10 @@ fn layer_row<'a>(
name_col_w: f32,
) -> Element<'a, Message> {
let svg_btn = |bytes: &'static [u8], on_press: Message| -> Element<'a, Message> {
- button(
- iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
- .width(ICON_SZ)
- .height(ICON_SZ),
- )
+ button(crate::ui::icons::semantic(bytes, ICON_SZ))
.on_press(on_press)
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
+ .style(move |theme: &Theme, status| {
+ layer_cell_button_style(theme, status, is_selected, index)
})
.padding(Padding {
top: COMBO_PAD_V,
@@ -682,27 +706,9 @@ fn layer_row<'a>(
let lck_svg = crate::ui::icons::layer_lock(layer.locked);
let status_dot: Element<'_, Message> = if is_current {
- crate::ui::icons::tinted(
- crate::ui::icons::CHECK,
- 13.0,
- Color {
- r: 0.25,
- g: 0.85,
- b: 0.45,
- a: 1.0,
- },
- )
+ crate::ui::icons::themed_success(crate::ui::icons::CHECK, 13.0)
} else {
- crate::ui::icons::tinted(
- crate::ui::icons::DOT,
- 9.0,
- Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
- },
- )
+ crate::ui::icons::themed_secondary(crate::ui::icons::DOT, 9.0)
};
// Name cell
@@ -717,38 +723,7 @@ fn layer_row<'a>(
left: 4.0,
right: 4.0,
})
- .style(|_: &Theme, _| iced::widget::text_input::Style {
- background: iced::Background::Color(Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
- }),
- border: Border {
- radius: 2.0.into(),
- width: 1.0,
- color: Color {
- r: 0.45,
- g: 0.65,
- b: 0.90,
- a: 1.0,
- },
- },
- icon: Color::WHITE,
- placeholder: Color {
- r: 0.4,
- g: 0.4,
- b: 0.4,
- a: 1.0,
- },
- value: Color::WHITE,
- selection: Color {
- r: 0.25,
- g: 0.45,
- b: 0.75,
- a: 0.5,
- },
- })
+ .style(table_input_style)
.width(Length::Fixed(name_col_w))
.into()
} else {
@@ -756,21 +731,11 @@ fn layer_row<'a>(
let name_budget = ((name_col_w / 6.0) as usize).max(8);
let name_btn = button(
text(crate::ui::text_util::elide(&layer.name, name_budget))
- .size(FONT_SZ)
- .color(ROW_TEXT),
+ .size(FONT_SZ),
)
.on_press(Message::LayerRenameStart(index))
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered => Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- ..Default::default()
+ .style(move |theme: &Theme, status| {
+ layer_cell_button_style(theme, status, is_selected, index)
})
.padding(Padding {
top: COMBO_PAD_V,
@@ -837,7 +802,7 @@ fn layer_row<'a>(
} else {
text(layer.linetype.as_str())
.size(FONT_SZ)
- .color(DIM)
+ .style(muted_style)
.width(Length::Fixed(COL_LT))
.into()
};
@@ -861,7 +826,7 @@ fn layer_row<'a>(
} else {
text(cur_lw_item.to_string())
.size(FONT_SZ)
- .color(DIM)
+ .style(muted_style)
.width(Length::Fixed(COL_LW))
.into()
};
@@ -877,33 +842,13 @@ fn layer_row<'a>(
left: 4.0,
right: 4.0,
})
- .style(|_: &Theme, _| iced::widget::text_input::Style {
- background: iced::Background::Color(Color::TRANSPARENT),
- border: Border {
- radius: 2.0.into(),
- width: 1.0,
- color: BORDER_COLOR,
- },
- icon: Color::WHITE,
- placeholder: DIM,
- value: ROW_TEXT,
- selection: Color {
- r: 0.25,
- g: 0.45,
- b: 0.75,
- a: 0.5,
- },
+ .style(|theme: &Theme, status| {
+ let mut style = table_input_style(theme, status);
+ style.background = iced::Background::Color(Color::TRANSPARENT);
+ style
})
.width(Length::Fixed(COL_TRANS));
- let bg = if is_selected {
- ROW_SEL
- } else if index % 2 == 0 {
- ROW_EVEN
- } else {
- ROW_ODD
- };
-
let mut row_content = row![
container(status_dot)
.width(50)
@@ -945,9 +890,20 @@ fn layer_row<'a>(
mouse_area(
container(row_content)
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(bg)),
+ .style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ let pair = if is_selected {
+ palette.primary.weak
+ } else if index % 2 == 0 {
+ palette.background.base
+ } else {
+ palette.background.weak
+ };
+ container::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: Some(pair.text),
..Default::default()
+ }
})
.padding(Padding {
top: 0.0,
@@ -965,31 +921,10 @@ fn layer_row<'a>(
// ββ Combo style ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
fn combo_input_style(
- _theme: &Theme,
- _status: iced::widget::text_input::Status,
+ theme: &Theme,
+ status: iced::widget::text_input::Status,
) -> iced::widget::text_input::Style {
- iced::widget::text_input::Style {
- background: iced::Background::Color(Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 1.0,
- }),
- border: Border {
- radius: 2.0.into(),
- width: 1.0,
- color: BORDER_COLOR,
- },
- icon: Color::WHITE,
- placeholder: DIM,
- value: Color::WHITE,
- selection: Color {
- r: 0.25,
- g: 0.45,
- b: 0.75,
- a: 0.5,
- },
- }
+ table_input_style(theme, status)
}
// ββ Display helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -1054,67 +989,3 @@ const COL_COLOR: f32 = 90.0;
const COL_LT: f32 = 110.0;
const COL_LW: f32 = 90.0;
const COL_TRANS: f32 = 80.0;
-
-// ββ Colors ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-const PANEL_BG: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.18,
- a: 1.0,
-};
-const TOOLBAR_BG: Color = Color {
- r: 0.20,
- g: 0.20,
- b: 0.20,
- a: 1.0,
-};
-const COL_HEADER_BG: Color = Color {
- r: 0.21,
- g: 0.21,
- b: 0.21,
- a: 1.0,
-};
-const ROW_EVEN: Color = Color {
- r: 0.18,
- g: 0.18,
- b: 0.18,
- a: 1.0,
-};
-const ROW_ODD: Color = Color {
- r: 0.21,
- g: 0.21,
- b: 0.21,
- a: 1.0,
-};
-const ROW_SEL: Color = Color {
- r: 0.18,
- g: 0.32,
- b: 0.52,
- a: 1.0,
-};
-const ROW_TEXT: Color = Color {
- r: 0.85,
- g: 0.85,
- b: 0.85,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.50,
- g: 0.50,
- b: 0.50,
- a: 1.0,
-};
-const BORDER_COLOR: Color = Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
-};
-#[allow(dead_code)]
-const ICON_COLOR: Color = Color {
- r: 0.80,
- g: 0.80,
- b: 0.80,
- a: 1.0,
-};
diff --git a/src/ui/window/layout_manager.rs b/src/ui/window/layout_manager.rs
index 1c2316aa2..28a616dc8 100644
--- a/src/ui/window/layout_manager.rs
+++ b/src/ui/window/layout_manager.rs
@@ -2,131 +2,37 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
-use iced::{Background, Border, Color, Element, Fill, Theme};
+use iced::{Background, Element, Fill, Theme};
-const TB: Color = Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 1.0,
-};
-const BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const TEXT: Color = Color {
- r: 0.88,
- g: 0.88,
- b: 0.88,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
-const ACTIVE: Color = Color {
- r: 0.20,
- g: 0.40,
- b: 0.70,
- a: 1.0,
-};
-const FIELD: Color = Color {
- r: 0.10,
- g: 0.10,
- b: 0.10,
- a: 1.0,
-};
-const LIST: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
-const WARN: Color = Color {
- r: 0.80,
- g: 0.35,
- b: 0.25,
- a: 1.0,
-};
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
+fn primary_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
+ }
+}
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
- move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match (accent, st) {
- (true, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.20,
- g: 0.42,
- b: 0.72,
- a: 1.0,
- },
- (false, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.28,
- g: 0.28,
- b: 0.28,
- a: 1.0,
- },
- (true, _) => ACCENT,
- _ => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- },
- })),
- text_color: TEXT,
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
+ move |theme: &Theme, status| {
+ if accent {
+ button::primary(theme, status)
+ } else {
+ button::secondary(theme, status)
+ }
}
}
fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
- move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match (active, st) {
- (true, _) => ACTIVE,
- (false, button::Status::Hovered | button::Status::Pressed) => Color {
- r: 0.26,
- g: 0.26,
- b: 0.26,
- a: 1.0,
- },
- _ => Color::TRANSPARENT,
- })),
- text_color: TEXT,
- ..Default::default()
- }
-}
-
-fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
- text_input::Style {
- background: Background::Color(FIELD),
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 3.0.into(),
- },
- icon: TEXT,
- placeholder: DIM,
- value: TEXT,
- selection: ACCENT,
+ move |theme: &Theme, status| {
+ if active {
+ button::primary(theme, status)
+ } else {
+ button::subtle(theme, status)
+ }
}
}
@@ -134,8 +40,10 @@ fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1))
.width(Fill)
.height(1)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color,
+ )),
..Default::default()
})
.into()
@@ -145,8 +53,10 @@ fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill))
.width(1)
.height(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color,
+ )),
..Default::default()
})
.into()
@@ -169,34 +79,18 @@ pub fn view_window<'a>(
.padding([4, 10]),
button(text("Delete").size(11))
.on_press(Message::LayoutManagerDelete)
- .style(move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match st {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.60,
- g: 0.20,
- b: 0.18,
- a: 1.0
- },
- _ => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0
- },
- })),
- text_color: if is_model { DIM } else { WARN },
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 4.0.into()
- },
- ..Default::default()
+ .style(move |theme: &Theme, status| {
+ if is_model {
+ button::secondary(theme, status)
+ } else {
+ button::danger(theme, status)
+ }
})
.padding([4, 10]),
Space::new().width(Fill),
button(
row![
- crate::ui::icons::tinted(crate::ui::icons::TRI_LEFT_B, 9.0, Color::WHITE),
+ crate::ui::icons::themed_arrow_left(9.0),
text("Move Left").size(11),
]
.spacing(4)
@@ -208,7 +102,7 @@ pub fn view_window<'a>(
button(
row![
text("Move Right").size(11),
- crate::ui::icons::arrow_right(9.0, Color::WHITE),
+ crate::ui::icons::themed_arrow_right(9.0),
]
.spacing(4)
.align_y(iced::Center),
@@ -224,8 +118,10 @@ pub fn view_window<'a>(
.spacing(4)
.align_y(iced::Center),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TB)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weakest.color,
+ )),
..Default::default()
})
.width(Fill)
@@ -241,11 +137,7 @@ pub fn view_window<'a>(
.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,
- ));
+ item_row = item_row.push(crate::ui::icons::themed_arrow_left(8.0));
}
button(item_row)
.on_press(Message::LayoutManagerSelect(name.clone()))
@@ -258,17 +150,9 @@ pub fn view_window<'a>(
let layout_list = container(
column![
- text("Layouts").size(10).color(DIM),
+ text("Layouts").size(10).style(muted_style),
container(scrollable(column(list_items).spacing(2)).height(Fill))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LIST)),
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 3.0.into()
- },
- ..Default::default()
- })
+ .style(container::bordered_box)
.width(Fill)
.height(Fill)
.padding(2),
@@ -293,38 +177,38 @@ pub fn view_window<'a>(
} else {
"Paper Space Layout"
})
- .size(13)
- .color(TEXT),
+ .size(13),
Space::new().height(8),
row![
- text("Name:").size(11).color(DIM).width(80),
+ text("Name:").size(11).style(muted_style).width(80),
text(selected).size(11),
]
.spacing(8)
.align_y(iced::Center),
row![
- text("Status:").size(11).color(DIM).width(80),
+ text("Status:").size(11).style(muted_style).width(80),
text(if selected == current.as_str() {
"Active"
} else {
"Inactive"
})
.size(11)
- .color(if selected == current.as_str() {
- ACCENT
- } else {
- DIM
+ .style(move |theme: &Theme| {
+ if selected == current.as_str() {
+ primary_style(theme)
+ } else {
+ muted_style(theme)
+ }
}),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(16),
- text("Rename").size(10).color(DIM),
+ text("Rename").size(10).style(muted_style),
row![
text_input("New nameβ¦", rename_buf)
.on_input(Message::LayoutManagerRenameBuf)
.on_submit(Message::LayoutManagerRenameCommit)
- .style(field_style)
.size(11)
.padding([4, 8]),
button(text("OK").size(11))
@@ -343,8 +227,10 @@ pub fn view_window<'a>(
let body = row![layout_list, vsep(), details].height(Fill);
container(column![toolbar, hdivider(), body].spacing(0))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.width(Fill)
diff --git a/src/ui/window/open_progress.rs b/src/ui/window/open_progress.rs
index 9bcf36aaa..661687eab 100644
--- a/src/ui/window/open_progress.rs
+++ b/src/ui/window/open_progress.rs
@@ -5,7 +5,7 @@
use iced::time::Instant;
use iced::widget::{button, column, container, row, stack, text, Space};
-use iced::{Background, Border, Color, Element, Fill, Length, Theme};
+use iced::{Background, Border, Element, Fill, Length, Theme};
use std::sync::atomic::Ordering;
use crate::app::{
@@ -60,13 +60,10 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
.width(Length::Fixed(fill_width))
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.30,
- g: 0.62,
- b: 0.95,
- a: 1.0,
- })),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().primary.base.color
+ )),
border: Border {
radius: 3.0.into(),
..Default::default()
@@ -86,13 +83,10 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
let bar_track: Element<'_, Message> = container(
stack![
container(Space::new().width(Length::Fixed(BAR_TRACK_WIDTH)).height(Length::Fixed(BAR_TRACK_HEIGHT)))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.18,
- g: 0.18,
- b: 0.18,
- a: 1.0,
- })),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.strong.color
+ )),
border: Border {
radius: 3.0.into(),
..Default::default()
@@ -107,9 +101,7 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
.into();
// ββ Card body ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- let title = text("Opening file")
- .size(15)
- .color(Color::WHITE);
+ let title = text("Opening file").size(15);
let name_line = text(format!(
"{} ({})",
@@ -117,11 +109,8 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
format_size(progress.size_bytes)
))
.size(13)
- .color(Color {
- r: 0.82,
- g: 0.82,
- b: 0.82,
- a: 1.0,
+ .style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.82)),
});
let phase_line = text(format!(
@@ -130,52 +119,13 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
basis_points as f32 / 100.0
))
.size(12)
- .color(Color {
- r: 0.70,
- g: 0.80,
- b: 0.95,
- a: 1.0,
+ .style(|theme: &Theme| iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
});
- let cancel_btn: Element<'_, Message> = button(text("Cancel").size(12).color(Color::WHITE))
+ let cancel_btn: Element<'_, Message> = button(text("Cancel").size(12))
.on_press(Message::OpenCancel)
- .style(|_: &Theme, status| {
- let bg = match status {
- button::Status::Hovered => Color {
- r: 0.32,
- g: 0.32,
- b: 0.32,
- a: 1.0,
- },
- button::Status::Pressed => Color {
- r: 0.42,
- g: 0.18,
- b: 0.18,
- a: 1.0,
- },
- _ => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- },
- };
- button::Style {
- background: Some(Background::Color(bg)),
- border: Border {
- color: Color {
- r: 0.40,
- g: 0.40,
- b: 0.40,
- a: 1.0,
- },
- width: 1.0,
- radius: 3.0.into(),
- },
- text_color: Color::WHITE,
- ..Default::default()
- }
- })
+ .style(button::danger)
.padding([4, 14])
.into();
@@ -187,35 +137,25 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
.width(Length::Fixed(CARD_WIDTH)),
)
.padding([18, 22])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 0.98,
- })),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: Color {
- r: 0.45,
- g: 0.45,
- b: 0.45,
- a: 1.0,
- },
+ color: palette.background.neutral.color,
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
+ }
});
// ββ Backdrop (click-blocker + dim) ββββββββββββββββββββββββββββββββββββ
let backdrop: Element<'_, Message> = container(Space::new().width(Fill).height(Fill))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.55,
- })),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.strong.color.scale_alpha(0.72)
+ )),
..Default::default()
})
.width(Fill)
diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs
index 1e68316d8..15913622e 100644
--- a/src/ui/window/options.rs
+++ b/src/ui/window/options.rs
@@ -1,96 +1,123 @@
+use crate::app::config::UiThemeConfig;
use crate::app::Message;
-use iced::widget::{button, column, container, pick_list, row, text, Space};
-use iced::{Background, Border, Color, Element, Fill, Theme};
+use iced::widget::{
+ button, column, container, pick_list, row, scrollable, text, text_input, Space,
+};
+use iced::{Background, Border, Element, Fill, Theme};
-pub fn view_window<'a>(default_save_format: &'a str) -> Element<'a, Message> {
- const BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.17,
- a: 1.0,
- };
- const BORDER: Color = Color {
- r: 0.32,
- g: 0.32,
- b: 0.36,
- a: 1.0,
- };
- const TEXT: Color = Color {
- r: 0.90,
- g: 0.90,
- b: 0.90,
- a: 1.0,
- };
- const DIM: Color = Color {
- r: 0.60,
- g: 0.60,
- b: 0.64,
- a: 1.0,
- };
-
- let selected = crate::io::SAVE_FORMAT_OPTIONS
+pub fn view_window<'a>(
+ default_save_format: &'a str,
+ ui_theme: &'a UiThemeConfig,
+ theme_color_inputs: &'a [String; 6],
+) -> Element<'a, Message> {
+ let selected_format = crate::io::SAVE_FORMAT_OPTIONS
.iter()
.copied()
.find(|candidate| *candidate == default_save_format);
- let close = button(text("Close").size(12).color(TEXT))
- .on_press(Message::CloseModal)
- .padding([5, 16])
- .style(|_: &Theme, status| button::Style {
- background: Some(Background::Color(match status {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.34,
- g: 0.34,
- b: 0.38,
- a: 1.0,
- },
- _ => Color {
- r: 0.26,
- g: 0.26,
- b: 0.29,
- a: 1.0,
- },
- })),
- text_color: TEXT,
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- });
+ let theme_options = Theme::ALL
+ .iter()
+ .map(ToString::to_string)
+ .chain(std::iter::once("Custom".to_string()))
+ .collect::>();
+ let selected_theme = Some(ui_theme.name.clone());
- let body = column![
- text("Open and Save").size(15).color(TEXT),
- Space::new().height(16),
+ let palette = ui_theme.palette.to_iced();
+ let colors = [
+ ("Background", palette.background),
+ ("Text", palette.text),
+ ("Primary", palette.primary),
+ ("Success", palette.success),
+ ("Warning", palette.warning),
+ ("Danger", palette.danger),
+ ];
+
+ let mut color_controls = column![].spacing(8);
+ for (index, (label, color)) in colors.into_iter().enumerate() {
+ let swatch = container(Space::new())
+ .width(28)
+ .height(22)
+ .style(move |theme: &Theme| container::Style {
+ background: Some(Background::Color(color)),
+ border: Border {
+ color: theme.extended_palette().background.strong.color,
+ width: 1.0,
+ radius: 3.0.into(),
+ },
+ ..Default::default()
+ });
+ color_controls = color_controls.push(
+ row![
+ text(label).size(12).width(110),
+ swatch,
+ text_input("#RRGGBB", theme_color_inputs[index].as_str())
+ .on_input(move |value| Message::OptionsThemeColorChanged(index, value))
+ .width(130),
+ ]
+ .spacing(10)
+ .align_y(iced::Center),
+ );
+ }
+
+ let close = button(text("Close").size(12))
+ .on_press(Message::CloseModal)
+ .padding([6, 18])
+ .style(button::secondary);
+
+ let content = column![
+ text("Open and Save").size(15),
+ Space::new().height(10),
row![
- text("Default save format:").size(12).color(TEXT).width(150),
- pick_list(crate::io::SAVE_FORMAT_OPTIONS, selected, |format: &str| {
- Message::DefaultSaveFormatChanged(format.to_string())
- })
+ text("Default save format:").size(12).width(150),
+ pick_list(
+ crate::io::SAVE_FORMAT_OPTIONS,
+ selected_format,
+ |format: &str| Message::DefaultSaveFormatChanged(format.to_string())
+ )
.width(Fill),
]
.spacing(12)
.align_y(iced::Center),
- Space::new().height(10),
+ Space::new().height(8),
text(
- "Used when a new drawing is saved for the first time. Existing drawings keep their current file type and version."
+ "Used for the first save of a new drawing. Existing drawings keep their file type and version."
)
- .size(11)
- .color(DIM)
- .width(Fill),
- Space::new().height(Fill),
- row![Space::new().width(Fill), close],
+ .size(11),
+ Space::new().height(22),
+ text("Theme").size(15),
+ Space::new().height(10),
+ row![
+ text("Iced theme:").size(12).width(150),
+ pick_list(
+ theme_options,
+ selected_theme,
+ Message::OptionsThemeChanged,
+ )
+ .width(Fill),
+ ]
+ .spacing(12)
+ .align_y(iced::Center),
+ Space::new().height(8),
+ text(
+ "Changing a base colour switches to Custom. Iced generates every component shade from these six colours."
+ )
+ .size(11),
+ Space::new().height(12),
+ color_controls,
]
.spacing(0)
+ .width(Fill);
+
+ let body = column![
+ scrollable(content).height(Fill),
+ Space::new().height(12),
+ row![Space::new().width(Fill), close],
+ ]
.width(Fill)
.height(Fill);
container(body)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
- ..Default::default()
- })
+ .style(container::rounded_box)
.padding([16, 18])
.width(Fill)
.height(Fill)
diff --git a/src/ui/window/plot.rs b/src/ui/window/plot.rs
index cf0d858c3..133fa54aa 100644
--- a/src/ui/window/plot.rs
+++ b/src/ui/window/plot.rs
@@ -10,17 +10,7 @@ use iced::widget::{
button, checkbox, column, container, mouse_area, pick_list, row, scrollable, text, text_input,
Space,
};
-use iced::{Background, Border, Color, Element, Fill, Length, Theme};
-
-const TB: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 };
-const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
-const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
-const TEXT: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
-const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
-const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
-const FIELD: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 };
-const ACTIVE: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
-const LIST: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 };
+use iced::{Background, Border, Element, Fill, Length, Theme};
/// Sentinel entries in the printer dropdown (not real printer names).
pub const OUT_DEFAULT: &str = "System default printer";
@@ -221,32 +211,49 @@ impl PlotDialogState {
}
fn btn(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
- move |_: &Theme, st| button::Style {
- background: Some(Background::Color(match (accent, st) {
- (true, button::Status::Hovered | button::Status::Pressed) => {
- Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 }
- }
+ move |theme: &Theme, st| {
+ let palette = theme.extended_palette();
+ let pair = match (accent, st) {
+ (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
(false, button::Status::Hovered | button::Status::Pressed) => {
- Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
+ palette.background.strong
}
- (true, _) => ACCENT,
- _ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 },
- })),
- text_color: TEXT,
- border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
+ (true, _) => palette.primary.base,
+ _ => palette.background.weak,
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
+ border: Border {
+ color: palette.background.neutral.color,
+ width: 1.0,
+ radius: 4.0.into(),
+ },
shadow: iced::Shadow::default(),
snap: false,
+ }
}
}
-fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
+fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
+ let border = match status {
+ text_input::Status::Focused { .. } => palette.primary.base.color,
+ _ => palette.background.neutral.color,
+ };
text_input::Style {
- background: Background::Color(FIELD),
- border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
- icon: TEXT,
- placeholder: DIM,
- value: TEXT,
- selection: ACCENT,
+ background: Background::Color(palette.background.base.color),
+ border: Border { color: border, width: 1.0, radius: 3.0.into() },
+ icon: palette.background.base.text,
+ placeholder: palette.background.base.text.scale_alpha(0.48),
+ value: palette.background.base.text,
+ selection: palette.primary.base.color.scale_alpha(0.5),
+ }
+}
+
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
}
}
@@ -254,23 +261,27 @@ fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1))
.width(Fill)
.height(1)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
})
.into()
}
fn section_label<'a>(s: &'static str) -> Element<'a, Message> {
- text(s).size(11).color(DIM).into()
+ text(s).size(11).style(muted_style).into()
}
fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill))
.width(1)
.height(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color
+ )),
..Default::default()
})
.into()
@@ -295,13 +306,16 @@ fn setup_row<'a>(
.into();
}
let is_sel = name == selected;
- let cell = container(text(name.to_string()).size(11).color(TEXT))
+ let cell = container(text(name.to_string()).size(11))
.padding([4, 8])
.width(Fill)
- .style(move |_: &Theme| container::Style {
- background: is_sel.then_some(Background::Color(ACTIVE)),
- text_color: Some(TEXT),
+ .style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: is_sel.then_some(Background::Color(palette.primary.strong.color)),
+ text_color: is_sel.then_some(palette.primary.strong.text),
..Default::default()
+ }
});
mouse_area(cell)
.on_press(Message::PlotDlg(PlotDlgMsg::SelectSetup(name.to_string())))
@@ -321,7 +335,7 @@ fn drop_row<'a>(
.text_size(12)
.padding([3, 6])
.width(Length::Fill);
- row![text(label).size(11).color(DIM).width(92), pl]
+ row![text(label).size(11).style(muted_style).width(92), pl]
.spacing(8)
.align_y(iced::Center)
.into()
@@ -335,7 +349,7 @@ fn field_row<'a>(
width: u16,
) -> Element<'a, Message> {
row![
- text(label).size(11).color(DIM).width(92),
+ text(label).size(11).style(muted_style).width(92),
text_input("", value)
.on_input(move |s| Message::PlotDlg(ctor(s)))
.style(field_style)
@@ -413,8 +427,10 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
]
.align_y(iced::Center),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TB)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weak.color
+ )),
..Default::default()
})
.width(Fill)
@@ -445,7 +461,7 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
.map(|name| setup_row(name, &s.selected_setup, renaming, rename_buf))
.collect();
let list_body: Element<'_, Message> = if rows.is_empty() {
- container(text("(no page setups)").size(11).color(DIM))
+ container(text("(no page setups)").size(11).style(muted_style))
.padding([6, 8])
.into()
} else {
@@ -453,16 +469,19 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
};
let list_panel = container(
column![
- text("Page setups").size(10).color(DIM),
+ text("Page setups").size(10).style(muted_style),
container(list_body)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(LIST)),
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
border: Border {
- color: BORDER,
+ color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
+ }
})
.width(Fill)
.height(Fill)
@@ -550,11 +569,18 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
hdivider(),
section_label("Plot style table (pen assignments)"),
row![
- container(text(style_label).size(12).color(TEXT))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(FIELD)),
- border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
+ container(text(style_label).size(12))
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.base.color)),
+ border: Border {
+ color: palette.background.neutral.color,
+ width: 1.0,
+ radius: 3.0.into(),
+ },
..Default::default()
+ }
})
.padding([4, 8])
.width(Fill),
@@ -583,7 +609,7 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
.width(Length::Fill)
)
.width(Fill),
- text("DPI").size(11).color(DIM),
+ text("DPI").size(11).style(muted_style),
text_input("", &s.dpi)
.on_input(move |v| Message::PlotDlg(PlotDlgMsg::Dpi(v)))
.style(field_style)
@@ -631,8 +657,10 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
let body = row![list_panel, vsep(), detail].height(Fill);
container(column![toolbar, hdivider(), body].spacing(0))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color
+ )),
..Default::default()
})
.width(Fill)
diff --git a/src/ui/window/plugin_manager.rs b/src/ui/window/plugin_manager.rs
index bd8e43d97..3f1b903e0 100644
--- a/src/ui/window/plugin_manager.rs
+++ b/src/ui/window/plugin_manager.rs
@@ -7,7 +7,7 @@
use crate::app::Message;
use crate::plugin::external::{ExternalPlugin, RegistryEntry};
use iced::widget::{button, column, container, pick_list, row, scrollable, text, text_input, Space};
-use iced::{Background, Border, Color, Element, Fill, Theme};
+use iced::{Background, Border, Element, Fill, Theme};
use rustc_hash::{FxHashMap, FxHashSet};
/// Marketplace state passed to the Plugin Manager view.
@@ -25,124 +25,105 @@ inventory::submit!(crate::command::CommandRegistration {
names: &["PLUGINS", "PLUGINMANAGER"]
});
-const BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const CARD: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
-const BORDER: Color = Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.30,
- g: 0.62,
- b: 0.95,
- a: 1.0,
-};
-const WHITE: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
+fn primary_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
+ }
+}
fn badge<'a>(label: String) -> Element<'a, Message> {
- container(text(label).size(11).color(WHITE))
+ container(text(label).size(11))
.padding([2, 8])
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.20,
- g: 0.34,
- b: 0.52,
- a: 1.0,
- })),
+ .style(|theme: &Theme| {
+ let pair = theme.extended_palette().primary.weak;
+ container::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: Some(pair.text),
border: Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
+ }
})
.into()
}
fn toggle_button<'a>(id: &str, disabled: bool) -> Element<'a, Message> {
// Label shows the action the click performs.
- let (label, on, off) = if disabled {
- ("Enable", Color { r: 0.18, g: 0.5, b: 0.25, a: 1.0 }, Color { r: 0.22, g: 0.6, b: 0.3, a: 1.0 })
+ let label = if disabled {
+ "Enable"
} else {
- ("Disable", Color { r: 0.4, g: 0.22, b: 0.22, a: 1.0 }, Color { r: 0.55, g: 0.28, b: 0.28, a: 1.0 })
+ "Disable"
};
let want_enabled = disabled; // clicking flips the state
let id_owned = id.to_string();
- button(text(label).size(12).color(WHITE))
+ button(text(label).size(12))
.padding([3, 12])
.on_press(Message::SetPluginEnabled(id_owned, want_enabled))
- .style(move |_: &Theme, status| {
- let bg = match status {
- button::Status::Hovered | button::Status::Pressed => off,
- _ => on,
- };
- button::Style {
- background: Some(Background::Color(bg)),
- text_color: WHITE,
- border: Border { radius: 4.0.into(), ..Default::default() },
- ..Default::default()
- }
- })
+ .style(if disabled { button::success } else { button::danger })
.into()
}
+#[derive(Clone, Copy)]
+enum StatusKind {
+ Muted,
+ Success,
+ Danger,
+ Warning,
+}
+
/// Coloured status pill for a discovered external package.
-fn status_badge<'a>(label: &str, color: Color) -> Element<'a, Message> {
- container(text(label.to_string()).size(11).color(WHITE))
+fn status_badge<'a>(label: &str, kind: StatusKind) -> Element<'a, Message> {
+ container(text(label.to_string()).size(11))
.padding([2, 8])
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(color)),
+ .style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ let pair = match kind {
+ StatusKind::Muted => palette.background.weak,
+ StatusKind::Success => palette.success.weak,
+ StatusKind::Danger => palette.danger.weak,
+ StatusKind::Warning => palette.warning.weak,
+ };
+ container::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: Some(pair.text),
border: Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
+ }
})
.into()
}
fn external_card<'a>(p: &ExternalPlugin, loaded: bool, disabled: bool) -> Element<'a, Message> {
- let (status, color) = if loaded && disabled {
- ("Disabled", Color { r: 0.45, g: 0.45, b: 0.45, a: 1.0 })
+ let (status, kind) = if loaded && disabled {
+ ("Disabled", StatusKind::Muted)
} else if loaded {
- ("Loaded", Color { r: 0.2, g: 0.5, b: 0.3, a: 1.0 })
+ ("Loaded", StatusKind::Success)
} else if !p.api_compatible() {
- ("API incompatible", Color { r: 0.55, g: 0.28, b: 0.28, a: 1.0 })
+ ("API incompatible", StatusKind::Danger)
} else if !p.lib_present {
- ("No library", Color { r: 0.5, g: 0.42, b: 0.2, a: 1.0 })
+ ("No library", StatusKind::Warning)
} else {
- ("Restart to load", Color { r: 0.5, g: 0.42, b: 0.2, a: 1.0 })
+ ("Restart to load", StatusKind::Warning)
};
let mut header = row![
- text(p.name.clone()).size(15).color(WHITE),
+ text(p.name.clone()).size(15),
Space::new().width(8),
badge(format!("v{}", p.version)),
Space::new().width(8),
badge(format!("API {}", p.api_version)),
Space::new().width(Fill),
- status_badge(status, color),
+ status_badge(status, kind),
]
.align_y(iced::Center);
// A loaded plugin can be turned off (drops its ribbon tab + dispatch).
@@ -154,75 +135,52 @@ fn external_card<'a>(p: &ExternalPlugin, loaded: bool, disabled: bool) -> Elemen
header = header.push(pill_button(
"Uninstall",
Message::PluginUninstall(p.id.clone()),
- Color { r: 0.4, g: 0.25, b: 0.25, a: 1.0 },
+ button::danger,
));
- let id_line = text(p.id.clone()).size(11).color(ACCENT);
+ let id_line = text(p.id.clone()).size(11).style(primary_style);
let mut body = column![header, id_line].spacing(5);
if !p.description.is_empty() {
- body = body.push(text(p.description.clone()).size(12).color(DIM));
+ body = body.push(text(p.description.clone()).size(12).style(muted_style));
}
if !p.command_prefixes.is_empty() {
body = body.push(
text(format!("Commands: {}", p.command_prefixes.join(", ")))
.size(11)
- .color(DIM),
+ .style(muted_style),
);
}
container(body.padding([12, 14]))
.width(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(CARD)),
- border: Border { color: BORDER, width: 1.0, radius: 6.0.into() },
- ..Default::default()
- })
+ .style(container::bordered_box)
.into()
}
-fn pill_button<'a>(label: &str, msg: Message, bg: Color) -> Element<'a, Message> {
- button(text(label.to_string()).size(12).color(WHITE))
+fn pill_button<'a>(
+ label: &str,
+ msg: Message,
+ style: fn(&Theme, button::Status) -> button::Style,
+) -> Element<'a, Message> {
+ button(text(label.to_string()).size(12))
.padding([4, 12])
.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()
- }
- })
+ .style(style)
.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))
+fn pill_icon_button<'a>(
+ icon: &'static [u8],
+ msg: Message,
+ style: fn(&Theme, button::Status) -> button::Style,
+) -> Element<'a, Message> {
+ button(crate::ui::icons::themed(icon, 11.0))
.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()
- }
- })
+ .style(style)
.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 };
-
/// Release dropdown + Install (+ optional unlink) for one repo.
fn install_controls<'a>(
repo: &str,
@@ -232,7 +190,7 @@ fn install_controls<'a>(
) -> Element<'a, Message> {
let repo_s = repo.to_string();
let picker: Element<'_, Message> = if tags.is_empty() {
- text("no releases").size(11).color(DIM).into()
+ text("no releases").size(11).style(muted_style).into()
} else {
let r = repo_s.clone();
pick_list(tags, selected, move |tag| {
@@ -244,7 +202,11 @@ fn install_controls<'a>(
let mut controls = row![
picker,
Space::new().width(8),
- pill_button("Install", Message::PluginInstall(repo_s.clone()), GREEN),
+ pill_button(
+ "Install",
+ Message::PluginInstall(repo_s.clone()),
+ button::success,
+ ),
]
.align_y(iced::Center)
.spacing(4);
@@ -253,7 +215,7 @@ fn install_controls<'a>(
controls = controls.push(pill_icon_button(
crate::ui::icons::CLOSE,
Message::PluginRepoRemove(repo_s),
- RED,
+ button::danger,
));
}
controls.into()
@@ -262,37 +224,33 @@ fn install_controls<'a>(
fn market_card<'a>(body: iced::widget::Column<'a, Message>) -> Element<'a, Message> {
container(body.spacing(4).padding([10, 12]))
.width(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(CARD)),
- border: Border { color: BORDER, width: 1.0, radius: 6.0.into() },
- ..Default::default()
- })
+ .style(container::bordered_box)
.into()
}
fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> {
- let mut col = column![text("Available plugins").size(13).color(ACCENT)].spacing(6);
+ let mut col = column![text("Available plugins").size(13).style(primary_style)].spacing(6);
// Curated registry entries (from the OpenCADStudio repo).
for e in m.registry {
let tags = m.release_tags.get(&e.repo).cloned().unwrap_or_default();
let selected = m.selected_tag.get(&e.repo).cloned();
let header = row![
- text(e.name.clone()).size(14).color(WHITE),
+ text(e.name.clone()).size(14),
Space::new().width(Fill),
install_controls(&e.repo, tags, selected, false),
]
.align_y(iced::Center);
- let mut body = column![header, text(e.repo.clone()).size(11).color(ACCENT)];
+ let mut body = column![header, text(e.repo.clone()).size(11).style(primary_style)];
if !e.description.is_empty() {
- body = body.push(text(e.description.clone()).size(12).color(DIM));
+ body = body.push(text(e.description.clone()).size(12).style(muted_style));
}
col = col.push(market_card(body));
}
// Manual: link any repo by owner/repo.
col = col.push(Space::new().height(6));
- col = col.push(text("Add a repository").size(12).color(DIM));
+ col = col.push(text("Add a repository").size(12).style(muted_style));
col = col.push(
row![
text_input("owner/repo", m.input)
@@ -301,7 +259,7 @@ fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> {
.size(13)
.width(Fill),
Space::new().width(8),
- pill_button("Add", Message::PluginRepoAdd, Color { r: 0.2, g: 0.4, b: 0.62, a: 1.0 }),
+ pill_button("Add", Message::PluginRepoAdd, button::primary),
]
.align_y(iced::Center),
);
@@ -309,7 +267,7 @@ fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> {
let tags = m.release_tags.get(repo).cloned().unwrap_or_default();
let selected = m.selected_tag.get(repo).cloned();
let header = row![
- text(repo.clone()).size(13).color(WHITE),
+ text(repo.clone()).size(13),
Space::new().width(Fill),
install_controls(repo, tags, selected, true),
]
@@ -318,7 +276,7 @@ fn marketplace_section<'a>(m: &MarketView) -> Element<'a, Message> {
}
if !m.status.is_empty() {
- col = col.push(text(m.status.to_string()).size(11).color(DIM));
+ col = col.push(text(m.status.to_string()).size(11).style(muted_style));
}
col.into()
}
@@ -329,17 +287,17 @@ pub fn view_window<'a>(
loaded: &FxHashSet,
market: MarketView,
) -> Element<'a, Message> {
- let title = text("Plugins").size(20).color(WHITE);
+ let title = text("Plugins").size(20);
let subtitle = text("Add-ons load from the plugins folder. Install from a repository below.")
.size(12)
- .color(DIM);
+ .style(muted_style);
let mut list = column![].spacing(10);
// Installed external packages (from the plugins folder).
if externals.is_empty() {
- list = list.push(text("No plugins installed yet.").size(13).color(DIM));
+ list = list.push(text("No plugins installed yet.").size(13).style(muted_style));
} else {
- list = list.push(text("Installed").size(13).color(ACCENT));
+ list = list.push(text("Installed").size(13).style(primary_style));
for p in externals {
list = list.push(external_card(
p,
@@ -360,8 +318,10 @@ pub fn view_window<'a>(
.width(Fill)
.height(Fill),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.width(Fill)
diff --git a/src/ui/window/shortcuts.rs b/src/ui/window/shortcuts.rs
index 48f983cea..2f5cdc693 100644
--- a/src/ui/window/shortcuts.rs
+++ b/src/ui/window/shortcuts.rs
@@ -2,7 +2,7 @@
use crate::app::Message;
use iced::widget::{column, container, row, scrollable, text, Space};
-use iced::{Background, Color, Element, Fill, Theme};
+use iced::{Background, Element, Fill, Theme};
use std::borrow::Cow;
/// Display name of the primary accelerator modifier on this platform.
@@ -14,49 +14,26 @@ const MOD: &str = "Cmd";
#[cfg(not(target_os = "macos"))]
const MOD: &str = "Ctrl";
-const TB: Color = Color {
- r: 0.13,
- g: 0.13,
- b: 0.13,
- a: 1.0,
-};
-const BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const BORDER: Color = Color {
- r: 0.35,
- g: 0.35,
- b: 0.35,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
-const KEY: Color = Color {
- r: 0.40,
- g: 0.70,
- b: 1.00,
- a: 1.0,
-};
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
+fn primary_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
+ }
+}
fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1))
.width(Fill)
.height(1)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BORDER)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.neutral.color,
+ )),
..Default::default()
})
.into()
@@ -69,7 +46,7 @@ fn shortcut_row<'a>(
row![
text(key.into())
.size(11)
- .color(KEY)
+ .style(primary_style)
.font(iced::Font::MONOSPACE)
.width(160),
text(action).size(11),
@@ -81,7 +58,7 @@ fn shortcut_row<'a>(
}
fn section<'a>(title: impl Into>) -> Element<'a, Message> {
- container(text(title.into()).size(11).color(DIM))
+ container(text(title.into()).size(11).style(muted_style))
.padding(iced::Padding {
top: 6.0,
right: 0.0,
@@ -99,12 +76,14 @@ pub fn view_window<'a>(
row![
text("Type SHORTCUTS SET to add custom shortcuts.")
.size(10)
- .color(DIM),
+ .style(muted_style),
]
.align_y(iced::Center),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(TB)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.weakest.color,
+ )),
..Default::default()
})
.width(Fill)
@@ -147,7 +126,7 @@ pub fn view_window<'a>(
rows.push(
text(" (none β use: SHORTCUTS SET )")
.size(11)
- .color(DIM)
+ .style(muted_style)
.into(),
);
} else {
@@ -158,7 +137,7 @@ pub fn view_window<'a>(
row![
text(key.as_str())
.size(11)
- .color(KEY)
+ .style(primary_style)
.font(iced::Font::MONOSPACE)
.width(160),
text(cmd.as_str()).size(11),
@@ -179,26 +158,25 @@ pub fn view_window<'a>(
// ββ Header row with accent ββββββββββββββββββββββββββββββββββββββββββββ
let header = container(
row![
- text("Key").size(10).color(ACCENT).width(160),
- text("Action").size(10).color(ACCENT),
+ text("Key").size(10).style(primary_style).width(160),
+ text("Action").size(10).style(primary_style),
]
.spacing(8)
.padding([4, 16]),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(Color {
- r: 0.13,
- g: 0.13,
- b: 0.18,
- a: 1.0,
- })),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().primary.weak.color,
+ )),
..Default::default()
})
.width(Fill);
container(column![toolbar, hdivider(), header, hdivider(), content].spacing(0))
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.width(Fill)
diff --git a/src/ui/window/update_notice.rs b/src/ui/window/update_notice.rs
index 95e845632..39d7c2273 100644
--- a/src/ui/window/update_notice.rs
+++ b/src/ui/window/update_notice.rs
@@ -1,60 +1,33 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, Space};
-use iced::{Background, Border, Color, Element, Fill, Theme};
+use iced::{Background, Border, Element, Fill, Theme};
-const BG: Color = Color {
- r: 0.15,
- g: 0.15,
- b: 0.15,
- a: 1.0,
-};
-const PANEL: Color = Color {
- r: 0.12,
- g: 0.12,
- b: 0.12,
- a: 1.0,
-};
-const BORDER: Color = Color {
- r: 0.30,
- g: 0.30,
- b: 0.30,
- a: 1.0,
-};
-const DIM: Color = Color {
- r: 0.55,
- g: 0.55,
- b: 0.55,
- a: 1.0,
-};
-const ACCENT: Color = Color {
- r: 0.25,
- g: 0.50,
- b: 0.85,
- a: 1.0,
-};
-const WHITE: Color = Color {
- r: 0.92,
- g: 0.92,
- b: 0.92,
- a: 1.0,
-};
+fn muted_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().background.base.text.scale_alpha(0.68)),
+ }
+}
+
+fn primary_style(theme: &Theme) -> iced::widget::text::Style {
+ iced::widget::text::Style {
+ color: Some(theme.extended_palette().primary.base.color),
+ }
+}
/// Renders one of the two "Installed" / "Latest" cards. The `highlight`
/// flag tints the border + label with the accent colour, making the new
/// version the visual anchor of the row.
fn version_card<'a>(label: &'static str, value: String, highlight: bool) -> Element<'a, Message> {
- let label_color = if highlight { ACCENT } else { DIM };
- let value_color = WHITE;
- let border_color = if highlight { ACCENT } else { BORDER };
- let bg = if highlight {
- Color { r: 0.10, g: 0.16, b: 0.22, a: 1.0 } // accent-tinted dark
- } else {
- PANEL
- };
container(
column![
- text(label).size(10).color(label_color),
- text(value).size(20).color(value_color),
+ text(label)
+ .size(10)
+ .style(move |theme: &Theme| if highlight {
+ primary_style(theme)
+ } else {
+ muted_style(theme)
+ }),
+ text(value).size(20),
]
.spacing(4)
.align_x(iced::Center),
@@ -67,14 +40,26 @@ fn version_card<'a>(label: &'static str, value: String, highlight: bool) -> Elem
left: 12.0,
})
.align_x(iced::Center)
- .style(move |_: &Theme| container::Style {
- background: Some(Background::Color(bg)),
+ .style(move |theme: &Theme| {
+ let palette = theme.extended_palette();
+ let pair = if highlight {
+ palette.primary.weak
+ } else {
+ palette.background.base
+ };
+ container::Style {
+ background: Some(Background::Color(pair.color)),
border: Border {
- color: border_color,
+ color: if highlight {
+ palette.primary.base.color
+ } else {
+ palette.background.neutral.color
+ },
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
+ }
})
.into()
}
@@ -98,24 +83,23 @@ fn render_notes_line<'a>(raw: &str) -> Element<'a, Message> {
if let Some(rest) = trimmed.strip_prefix("## ") {
return text(strip_inline_md(rest))
.size(13)
- .color(ACCENT)
+ .style(primary_style)
.into();
}
if let Some(rest) = trimmed.strip_prefix("### ") {
return text(strip_inline_md(rest))
.size(12)
- .color(WHITE)
.into();
}
if let Some(rest) = trimmed.strip_prefix("- ").or_else(|| trimmed.strip_prefix("* ")) {
return row![
- container(crate::ui::icons::tinted(crate::ui::icons::DOT, 5.0, DIM)).width(14),
- text(strip_inline_md(rest)).size(11).color(WHITE),
+ container(crate::ui::icons::themed_secondary(crate::ui::icons::DOT, 5.0)).width(14),
+ text(strip_inline_md(rest)).size(11),
]
.spacing(4)
.into();
}
- text(strip_inline_md(trimmed)).size(11).color(WHITE).into()
+ text(strip_inline_md(trimmed)).size(11).into()
}
/// Drop `**β¦**` and `` `β¦` `` markers without preserving emphasis (iced 0.14
@@ -139,10 +123,10 @@ fn strip_inline_md(s: &str) -> String {
pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> {
let header = container(
column![
- text("New Release Available").size(20).color(ACCENT),
+ text("New Release Available").size(20).style(primary_style),
text("A newer Open CAD Studio version is published on GitHub.")
.size(11)
- .color(DIM),
+ .style(muted_style),
]
.spacing(4)
.align_x(iced::Center),
@@ -165,10 +149,9 @@ 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(crate::ui::icons::tinted(
+ let arrow = container(crate::ui::icons::themed_secondary(
crate::ui::icons::ARROW_LONG_RIGHT,
20.0,
- DIM,
))
.width(iced::Length::Fixed(32.0))
.height(Fill)
@@ -181,50 +164,12 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> {
let later_btn = button(text("Later").size(11))
.on_press(Message::UpdateNoticeClose)
- .style(|_: &Theme, st| button::Style {
- background: Some(Background::Color(match st {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.22,
- g: 0.22,
- b: 0.22,
- a: 1.0,
- },
- _ => Color {
- r: 0.18,
- g: 0.18,
- b: 0.18,
- a: 1.0,
- },
- })),
- text_color: WHITE,
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- })
+ .style(button::secondary)
.padding([6, 16]);
let open_btn = button(text("Open Release Page").size(11))
.on_press(Message::UpdateNoticeOpenRelease)
- .style(|_: &Theme, st| button::Style {
- background: Some(Background::Color(match st {
- button::Status::Hovered | button::Status::Pressed => Color {
- r: 0.20,
- g: 0.42,
- b: 0.72,
- a: 1.0,
- },
- _ => ACCENT,
- })),
- text_color: WHITE,
- border: Border {
- radius: 4.0.into(),
- ..Default::default()
- },
- ..Default::default()
- })
+ .style(button::primary)
.padding([6, 16]);
let footer = row![Space::new().width(Fill), later_btn, open_btn]
@@ -240,7 +185,7 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> {
// Release notes panel. Rendered as a light-markdown column inside a
// bordered scrollable so long bodies stay contained and don't
// explode the window. Empty body β "No release notes provided."
- let notes_heading = container(text("What's new").size(11).color(DIM))
+ let notes_heading = container(text("What's new").size(11).style(muted_style))
.padding(iced::Padding {
top: 10.0,
right: 0.0,
@@ -251,7 +196,7 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> {
let notes_body: Element<'a, Message> = if body.trim().is_empty() {
text("No release notes provided.")
.size(11)
- .color(DIM)
+ .style(muted_style)
.into()
} else {
let mut col = column![].spacing(4);
@@ -264,15 +209,7 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> {
let notes_block = container(notes_body)
.width(Fill)
.height(Fill)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(PANEL)),
- border: Border {
- color: BORDER,
- width: 1.0,
- radius: 4.0.into(),
- },
- ..Default::default()
- });
+ .style(container::bordered_box);
// Wrap notes_block in a Fill-height container outside the column so it
// greedily claims every pixel left over after the fixed-height rows
@@ -294,8 +231,10 @@ pub fn view_window<'a>(latest: &'a str, body: &'a str) -> Element<'a, Message> {
left: 20.0,
}),
)
- .style(|_: &Theme| container::Style {
- background: Some(Background::Color(BG)),
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(
+ theme.extended_palette().background.base.color,
+ )),
..Default::default()
})
.width(Fill)
diff --git a/src/ui/wrap_bar.rs b/src/ui/wrap_bar.rs
index c088c472e..32dae9b65 100644
--- a/src/ui/wrap_bar.rs
+++ b/src/ui/wrap_bar.rs
@@ -25,7 +25,7 @@ use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, tree, Widget};
use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell};
use iced::{
- Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
+ Background, Border, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
Theme, Vector,
};
@@ -482,6 +482,9 @@ pub struct WrapFlow<'a> {
row_h: f32,
/// Align every wrapped row to the right edge of the available width.
justify_end: bool,
+ /// Receives the natural single-row width (as `f32` bits). Multiple flows
+ /// may share one output; the widest measured value wins.
+ natural_width_out: Option>,
}
impl<'a> WrapFlow<'a> {
@@ -493,6 +496,7 @@ impl<'a> WrapFlow<'a> {
spacing_y: 0.0,
row_h: 28.0,
justify_end: false,
+ natural_width_out: None,
}
}
@@ -517,6 +521,11 @@ impl<'a> WrapFlow<'a> {
self.justify_end = justify;
self
}
+
+ pub fn report_natural_width(mut self, out: Arc) -> Self {
+ self.natural_width_out = Some(out);
+ self
+ }
}
impl<'a> Widget for WrapFlow<'a> {
@@ -557,6 +566,11 @@ impl<'a> Widget for WrapFlow<'a> {
// bit closer before wrapping. Only fires when max_w is a real bound
// (i.e. the flow is already width-constrained, e.g. overflowing tabs).
let n = measured.len();
+ if let Some(out) = &self.natural_width_out {
+ let natural_width =
+ sum_w + n.saturating_sub(1) as f32 * self.spacing_x;
+ out.fetch_max(natural_width.to_bits(), Ordering::Relaxed);
+ }
let mut eff_gap = self.spacing_x;
if self.min_spacing_x < self.spacing_x && max_w.is_finite() && n > 1 {
let line_w = sum_w + (n - 1) as f32 * self.spacing_x;
@@ -1360,12 +1374,7 @@ impl<'a> Widget for ReorderTab<'a> {
shadow: Shadow::default(),
snap: true,
},
- Background::Color(Color {
- r: 0.20,
- g: 0.55,
- b: 0.90,
- a: 1.0,
- }),
+ Background::Color(theme.extended_palette().primary.base.color),
);
}
}
diff --git a/web/discussions.json b/web/discussions.json
new file mode 100644
index 000000000..a08409e91
--- /dev/null
+++ b/web/discussions.json
@@ -0,0 +1,42 @@
+[
+ {
+ "number": 531,
+ "title": "Choose The Default Theme",
+ "url": "https://github.com/HakanSeven12/OpenCADStudio/discussions/531",
+ "author": "HakanSeven12",
+ "updated_at": "2026-07-28T07:21:51Z",
+ "pinned": true
+ },
+ {
+ "number": 518,
+ "title": "Addin ideas",
+ "url": "https://github.com/HakanSeven12/OpenCADStudio/discussions/518",
+ "author": "UserDevtec",
+ "updated_at": "2026-07-27T14:09:44Z",
+ "pinned": false
+ },
+ {
+ "number": 430,
+ "title": "Inline Python REPL",
+ "url": "https://github.com/HakanSeven12/OpenCADStudio/discussions/430",
+ "author": "schoeller",
+ "updated_at": "2026-07-24T16:13:26Z",
+ "pinned": false
+ },
+ {
+ "number": 214,
+ "title": "New improvements",
+ "url": "https://github.com/HakanSeven12/OpenCADStudio/discussions/214",
+ "author": "JuanJoseJimenezdeCisneros",
+ "updated_at": "2026-07-14T14:10:11Z",
+ "pinned": false
+ },
+ {
+ "number": 382,
+ "title": "macOS 27 Golden Gate - Public Beta",
+ "url": "https://github.com/HakanSeven12/OpenCADStudio/discussions/382",
+ "author": "ZoomerHub",
+ "updated_at": "2026-07-14T12:57:50Z",
+ "pinned": false
+ }
+]