11 KiB
Shared theme infrastructure
The first shared mechanism is craft-ui::theme::WidgetPalette: it applies colors to egui's five
widget states, with a supplied foreground stroke and corner radius. VectorCraft, DesignCraft and
LightCraft use the same operation. Their complete palettes, theme choices, fonts and preferences
remain in their frontends.
This follows the reusable-UI workstream of craft-libs issue 2. The cross-app architecture discussion and base-crate request provide context; this change keeps the existing repository, document and release boundaries.
Source comparison
Reviewed 2026-10-09 at the revisions below. All links point to
crates/ui-egui/src/theme.rs. The 3,454-line total includes font setup, specialized helpers and
tests; it is not a count of duplicate theme code. Token-field counts include non-color fields.
| App | Source revision | Lines | Token fields | Theme model |
|---|---|---|---|---|
| PhotoCraft | e7a83ca342cd |
832 | 37 | Five presets; per-preset metrics and Classic bevels |
| VectorCraft | 825d4fdb8b52 |
345 | 38 | Four brightness presets |
| DesignCraft | 66c7ce7e96ec |
426 | 33 | Four brightness presets plus High Contrast |
| WordCraft | d7d9702bda1f |
231 | 32 | Light/dark, ribbon and page colors |
| PdfCraft | f626db25cca2 |
314 | 22 | System/light/dark preference |
| LightCraft | 88ea1a804b35 |
345 | 35 | Dark photo workflow palette |
| FilmCraft | 7b6c13447228 |
447 | 43 | Three presets, custom accent and accessible contrast |
| EffectCraft | 2a460986ee7a |
514 | 44 | Three presets, brightness and label colors |
The matching widget-state operation is visible in VectorCraft lines 299–312,
DesignCraft lines 336–348 and LightCraft lines 235–247. It assigns
bg_fill, weak_bg_fill, corner_radius and fg_stroke across noninteractive, inactive, hovered,
active and open states. The state colors differ, but the assignments and their meaning agree.
The surrounding Visuals operations differ materially:
| App | Differences that remain local |
|---|---|
| VectorCraft | Selection uses accent at 0.55 gamma with an accent stroke. Popup/window radius is 6; shadows vary with light/dark. Inactive border is absent, hover uses input border and active uses accent. Body/button text is 13 pt. |
| DesignCraft | Selection uses strong accent and strong-text stroke. Radius is 4; window shadow stays egui's default. Interactive borders use field border. Inactive buttons get transparent weak fill and input background after the shared operation. Body/button text is 11 pt. High Contrast keeps black/white/yellow colors. |
| LightCraft | The matching loop uses text_label, intentionally dimmer than the app's main text, and a fixed dark palette. Catalog label helpers depend on its own model. |
| FilmCraft | Custom highlight and contrast preferences feed its palette. Widget states use ordinary text for inactive/noninteractive and brighter text for hovered/active; open foreground remains inherited. |
| EffectCraft | Its widget assignment block matches FilmCraft's, but brightness adjustment, label/gradient helpers, popup radius, shadows, spacing and solid scrollbars differ. |
| PhotoCraft | Noninteractive text is dimmer; interactive states have separate border and expansion policies. Classic styling includes bevels, and font size is preference-driven. |
| PdfCraft | System theme resolution and named context storage are local. Its foreground loop preserves each inherited stroke width; a blanket full-stroke assignment would change that contract. Light text and font subpixel settings differ. |
| WordCraft | Tokens derive from egui's current dark mode. Only three interactive states get explicit radii; other states inherit egui defaults. Ribbon/document colors have separate meaning. |
FilmCraft and EffectCraft's separate matching block is a useful second extraction candidate, but it has a different foreground-state contract. Extending the first helper to cover it immediately would obscure the small invariant being shared. There is no single cleanest complete theme: the first pair offers the clearest common operation, and the other apps preserve different requirements rather than providing interchangeable replacements.
Boundary and API
pub struct WidgetPalette {
pub noninteractive: egui::Color32,
pub inactive: egui::Color32,
pub hovered: egui::Color32,
pub active: egui::Color32,
pub open: egui::Color32,
}
impl WidgetPalette {
pub fn apply_to(
self,
widgets: &mut egui::style::Widgets,
radius: egui::CornerRadius,
foreground: egui::Stroke,
);
}
The implementation lives in crates/ui/src/theme. It changes only
those four properties. Borders, expansion, selection, shadows and the rest of egui's visuals
stay untouched. Apps apply their state-specific exceptions afterward. noninteractive names
egui's state, not a new disabled-control policy.
A separate craft-theme crate adds no useful boundary: this mechanism already depends on egui
and belongs beside the controls it styles. A universal token structure, theme enum, application
identifier or global context key is unnecessary. Constructing another large visual-options
object would mostly rename egui::Visuals fields without simplifying their use. Context lookup
and storage are already small and retain different fallback themes.
The identical packed-RGB helper in VectorCraft and DesignCraft is only a few casts; it remains local in this extraction. A contrast API also needs a second runtime consumer before extraction.
Fonts and styling outside theme modules
Font setup occupies hundreds of the audited lines. It depends on app-specific font catalogs,
locale handling and fallback order: PhotoCraft uses its text and preference crates; VectorCraft
uses vectorcraft_text; DesignCraft and WordCraft use their font crates; PdfCraft adds script
and system fallback; LightCraft uses its engine catalog; FilmCraft and EffectCraft use their text
crates and local font discovery. DesignCraft also configures named Arabic shaping families.
These dependencies and font assets stay outside craft-ui.
Theme references occur throughout panels, menus and tools, with local visual/spacing overrides
in every app. A source search alone cannot turn this into a percentage of correctly themed UI:
many literal colors are document content, swatches, previews or intentional contrast choices.
A search of UI source files outside theme.rs found the following. The columns count files,
including tests, containing token/theme references, local visual/spacing mutation, or literal
Color32 construction. They are navigation aids, not counts of defective controls.
| App | Theme references | Local overrides | Literal colors |
|---|---|---|---|
| PhotoCraft | 115 | 37 | 59 |
| VectorCraft | 149 | 40 | 41 |
| DesignCraft | 26 | 8 | 19 |
| WordCraft | 10 | 4 | 9 |
| PdfCraft | 40 | 11 | 30 |
| LightCraft | 40 | 20 | 27 |
| FilmCraft | 53 | 4 | 46 |
| EffectCraft | 61 | 1 | 55 |
Examples worth preserving or reviewing separately:
- PhotoCraft's
widgets.rsdraws fixed light/dark Classic bevels and removes field chrome;menus.rssupplies its own menu styling. - VectorCraft's
widgets.rsuses white primary-button labels and contrasting fill/stroke wells. - DesignCraft's
widgets.rs:147sets focused numeric-field colors independently of High Contrast; fixed dialog warning/error colors also merit a later accessibility review. - WordCraft's ribbon Share control uses white; PdfCraft's notification surfaces and dialog widgets have local light/dark styling.
- LightCraft's dialog download errors use a fixed hue, while its color helpers also draw actual photo-workflow values.
- FilmCraft's header customizes menu chrome; EffectCraft's effect widgets and ScriptUI controls customize backgrounds and slider widths.
This extraction preserves those choices. It does not certify whole-app accessibility or theme consistency, and it does not move font installation into the shared crate.
Migration and validation
- Capture VectorCraft's four themes and DesignCraft's five, including High Contrast, before replacing either loop. Use synthetic documents, panels/inspectors, menus and preferences at 1× and 2× scales. Save the complete egui Style and app Tokens for comparison.
- Add the shared helper with a test that preserves custom borders and expansion while replacing all state fills, including reapplying a theme after a local override.
- Replace only each app's five-state loop. Keep its existing colors, strokes, post-loop exceptions, fonts, spacing, context state, commands and saved preference schema.
- Repeat the renders and compare complete styles/tokens and images. Exercise theme commands, menu check states and an in-memory saved-preference round trip. A process restart or native display acceptance remains a separate check.
- Run shared fmt, strict Clippy, tests and wasm, then each app's own gates. Each app pins the shared revision independently and can revert its adapter/pin without migrating preferences.
The main risks are accidentally resetting inherited egui fields, losing DesignCraft's inactive outline exception or High Contrast colors, and confusing font/locale behavior with theme application. Exact style comparison plus visual checks covers the first two; keeping font code unchanged limits the third. Shared code depends only on egui, and layer checks keep it above engines and document models.
The suite trial also adopts LightCraft's identical loop. FilmCraft/EffectCraft's widget-state matrix and consistent focused numeric fields remain candidates for separate work. Theme adapters for the shared tabs and buttons now also serve PdfCraft, WordCraft, GridCraft, DeckCraft, CADCraft and SoundCraft; their full palettes stay local. Further extractions need two real consumers and their own visual and interaction checks.