Theming¶
This page describes how FlashBang’s theming engine works—the data model, resolution step, and draw pipeline—as implemented in src/style/theme.odin, src/style/resolve.odin, and src/widgets/common.odin. Any concrete theme (flat, skeuomorphic, game HUD, etc.) is just values + optional procedure hooks on Theme; the core does not assume a particular skin or third-party theme package.
Design paradigm¶
- Theme is data — A
Themevalue holds colors, numeric parameters, and optional callback slots. Widgets do not mutate the theme; they read it (and copies on the stack are fine forpush_theme). - State drives resolution — Widget code decides
WidgetVisualState(hover, pressed, etc.) from input/focus/disabled rules, then asks the engine for paint parameters. - Resolution → surface —
resolve_surface(or a customtheme.resolve) produces aResolvedSurface: a renderer-neutral bundle of colors, border, corner radius, focus-ring flags, and optional depth metadata. That struct is the contract between “what the theme meant” and “whatemit_surfacewill draw.” - Drawing is centralized —
emit_surfaceimplements the default composition (optional pre-hook, body fill, border, focus ring). Widgets share one path so behavior stays consistent. - Overrides are explicit — Per-widget
Maybe(style.Color)fields patch the resolved surface viaapply_color_overrideafter resolution, without editing the global theme. - No selector language — There is no CSS-like matching. Rules are procedural: your
resolveproc (or the built-in fallback) implements whatever policy you want.
Theme: capability surface¶
The struct is the full list of what the engine can consult. Rough groups:
- Optional render hooks (
nil= use built-in behavior for that concern): resolve— Replace defaultresolve_surfacelogic entirely.geometry— Emit extra draw ops before the standard body (e.g. layered rects for depth cues).check_indicator,window_chrome,separator_geometry,group_box_label— Widget-specific extension points used only by the widgets that call them.- Scalar / color fields — Used by the default resolver and by custom
resolveimplementations however you define (e.g.surface_color,corner_radius, focus ring fields, window chrome sizing, spacing).
The authoritative field list and comments are in src/style/theme.odin.
WidgetVisualState¶
Enumeration in src/style/resolve.odin: NORMAL, HOVERED, PRESSED, FOCUSED, DISABLED. Widgets classify interaction state; the theme maps that enum (plus context) to a ResolvedSurface.
The extruded parameter¶
resolve_surface(theme, state, extruded) takes a boolean hint from the widget author: “this control is logically raised vs inset for styling purposes.” The built-in flat fallback largely ignores depth; a custom theme.resolve may use extruded to branch (e.g. invert depth sign, different border treatment). It is not a guarantee of 3D visuals—that depends entirely on whether your theme supplies depth-related fields and hooks.
Built-in resolution (no custom resolve)¶
If theme.resolve == nil, resolve_surface applies a minimal flat mapping: theme colors, simple border, disabled text color. Highlight/shadow/depth fields on ResolvedSurface are set to neutral values suitable for flat UI. This is a reference implementation, not a prescription for how custom themes should look.
Per-widget color overrides¶
After resolution, widgets may call apply_color_override so optional Maybe(style.Color) fields on configs replace fill/border/text on the ResolvedSurface (and adjust derived edge colors when fill changes). This is engine-level patching, not theme editing.
emit_surface pipeline¶
Regardless of theme “style,” the default emitter:
- Runs
theme.geometryfirst if set (extension point). - Draws the body from
resolved.surface_color. - Draws border when
border_widthis positive. - Draws focus ring if enabled on the resolved surface.
So the pipeline is fixed; the numbers and optional pre-layer come from resolution + theme hooks.
Active theme in immediate mode¶
set_immediate_context ensures a default theme exists on first use and resets the per-frame theme stack. Application code uses set_theme, push_theme, pop_theme, and widgets read active_theme(ctx). Scoped pushes are per frame in the current implementation—reapply each frame if you rely on local overrides.
Built-in theme constructors¶
default_theme() and basic_theme() return a small flat theme with no custom hooks (see src/style/theme.odin). They exist so the engine runs out of the box; your game or tool replaces or wraps them with a Theme built however you like.
Separators¶
emit_separator consults theme.separator_geometry when non-nil; otherwise the engine draws a simple line using resolved border color. Same pattern: hook optional, fallback generic.
Example in practice¶
Membrane is the bundled theme that exercises resolve, geometry, indicators, separators, group-box label, and window chrome. Reading it alongside this page shows how a full visual system maps onto the engine’s hooks without changing widget code.