Theming — guide
Class:
guide· audience: a techie theming a facett component or host. The what is intheming-design.md; the why intheming-reasoning.md. This file is the workflow.
f0 — apply a theme, prove a component follows it (the whole workflow)
Three moves take a host from "no theme" to a themed, coherence-tested app:
- Apply a preset once per frame (cheap) on the
egui::Context:
use facett::look::Theme as LookTheme;
LookTheme::nordisk().apply(ctx); // the flagship default; or ::windows_dark(), ::macos_dark(), ::device(), ...
apply installs the full egui::Style, and publishes the derived flat palette, the KeyMap, the EffectsPolicy, and the NativeFeel — every existing component picks all four up with zero per-component wiring.
- Write components against the published state, never a literal. Custom-painted components pull colour from
facett_core::theme(ui); native-feel cues come fromfacett_core::look::feel::{reveal_on_hover, apply_focus_ring, elevation_shadow}. - Prove it headless.
harness::render_themed(facet, theme)snapshots a component under a specific flatTheme;theme::probe/look::feel::probewitness which palette/platform a component actually consumed during that paint — the same seamfacett/tests/coherence.rssweeps across every component.
f1 — picking and switching a Theme
Pick a rich preset directly:
use facett::look::Theme;
let theme = Theme::nordisk(); // flagship identity, default look
// or: Theme::windows_dark(), Theme::macos_light(), Theme::device(), Theme::skade_vinter(), Theme::neutral()
theme.apply(&ctx);
Detect or override the platform instead of naming a preset directly:
use facett::look::{Theme, Platform};
Theme::for_detected_platform().apply(&ctx); // cfg!(target_os) → Mac/Windows/Neutral
Theme::for_platform(Platform::Windows).apply(&ctx); // force a platform regardless of host OS
Enumerate + fuzzy-match by name (drives a picker or a --theme flag):
Theme::preset_names() // Vec<String>: "windows-dark", "nordisk-dark", ...
Theme::by_name("Nordic Dark") // None (that's not a name) — try "nordisk-dark"
Theme::by_name("NORDISK_DARK") // Some(Theme) — case/`-`/`_`/space-insensitive
The flat six-palette family (sci_fi, nordic_aurora, cyberpunk_neon, amber_crt, deep_space, hugin_noir) is separate and reachable directly on facett::Theme:
use facett::{set_theme, Theme};
set_theme(ctx, Theme::hugin_noir()); // stores the flat palette + a matching egui::Visuals
Use look::Theme presets for a coherent whole-app look (OS-adaptive, effects-aware); use the flat family when a component only needs a standalone accent palette.
f2 — reading NativeFeel in a component's paint
A component gets platform-adaptive chrome with one call each, no branching on Platform itself:
fn view(&self, ui: &mut egui::Ui) -> Vec<Msg> {
let rect = ui.max_rect();
facett::look::feel::elevation_shadow(ui, rect, 8.0); // before the fill
// ... paint the surface fill/content ...
facett::look::feel::reveal_on_hover(ui, rect, 8.0); // no-op off Windows
facett::look::feel::apply_focus_ring(ui, rect, self.focused, 8.0);
vec![]
}
reveal_on_hover and apply_focus_ring read the live pointer / accent role internally — pass only the rect and (for focus) whether this control is focused. Read the raw spec instead of a helper when you need the numbers directly:
let feel = facett::look::feel::native_feel(ui); // NativeFeel for the active theme
let ring = feel.focus_ring; // FocusRing { width, expansion, glow, accent_tinted }
let shadow = facett::look::feel::elevation_shadow_params(feel.elevation);
Drive motion with the theme's platform curve instead of a hard-coded easing:
let motion = /* from the active look::Theme */ facett::look::Motion::neutral();
let tw = facett::effects::Tween::from_motion(&motion, 0.0, 1.0, motion.curve);
let y = tw.value_at(elapsed_secs); // macOS: overshoot-and-settle; Windows: snappy cubic
f3 — glow, ravens, and the deck
Turn on deck-level effects (all off by default, no cost when unused):
let mut deck = facett::FacetDeck::new(facets).with_fx(
facett::deckfx::DeckFx { glow: true, chrome: true, ..Default::default() }
);
deck.set_palette(2); // pin Theme::ALL[2] as the deck's flat-palette override
deck.cycle_palette(); // or step through them
deck.send_raven(row_rect); // launch the signature raven onto any rect; deck drives+paints it
palette_picker(ui) draws a ready-made one-line switcher over the flat palette family:
if let Some(i) = deck.palette_picker(ui) {
// user picked palette i this frame
}
Reach for the raw primitives directly when a component wants its own motion, not the deck's:
use facett::effects::{RavenSprite, glow_rect, easing};
let mut raven = RavenSprite::new().from(launch_pos).fly_to(target_rect);
raven.update(ctx); // per frame: advances + requests repaint while flying
raven.paint(ui.painter());
let pulse = easing::ease_in_out_cubic(((time * 2.4).sin() as f32) * 0.5 + 0.5);
glow_rect(ui.painter(), rect, theme.glow, pulse, 6);
f4 — a themed glass card
chrome::card is the one-call "frosted card" that degrades gracefully under the active EffectsPolicy:
use facett::chrome::{card, ChromeStyle};
use facett::look::effects_policy;
let policy = effects_policy(ui);
card(ui, rect, &facett_theme, policy, ChromeStyle::default().for_policy(policy));
For content painted into the card (so the fill stays behind it), split fill/edge:
use facett::chrome::{fill_shape, edge, ChromeStyle};
let style = ChromeStyle::default().for_policy(policy);
ui.painter().add(fill_shape(rect, &theme, policy, style)); // reserve the slot first
// ... draw content into `rect` here ...
edge(ui.painter(), rect, &theme, policy, style); // border + glow on top
f5 — the harness seam (render_themed)
Snapshot a component under a specific theme with no window, no GPU:
use facett_core::harness;
let mut v = MyView::new();
let report = harness::render_themed(&mut v, facett::Theme::sci_fi());
assert!(report.drew()); // it actually painted something
let sized = harness::render_themed_sized(&mut v, facett::Theme::nordisk().to_legacy_palette(), (400.0, 300.0));
Prove a component reads the active theme rather than a private default, using the probes that back facett/tests/coherence.rs:
use facett::core::theme::probe;
use facett::look::Theme as LookTheme;
let ctx = egui::Context::default();
LookTheme::macos_dark().apply(&ctx);
probe::reset();
let _ = ctx.run(Default::default(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| my_facet.ui(ui));
});
assert_eq!(probe::painted(), Some("macos-dark"));
The look::feel::probe module is the native-feel counterpart — reset()/consumed() prove a component read the published Platform through native_feel(ui) rather than assuming one.