Example theme: Membrane¶
Membrane is a concrete style.Theme shipped with FlashBang under themes/membrane/ in the library repository. It exists to show how the theming engine’s hooks work end-to-end: custom resolve, geometry, and widget-specific procs, not just palette swaps.
This page describes Membrane as an instance of the general model on Styling & themes.
What Membrane looks like (design intent)¶
Source comments in themes/membrane/theme.odin describe it as a dark, organic neumorphic skin:
base_colorandsurface_colorare the same (MAIN_COLOR), so widgets sit in the same material as the background; separation comes from shading (depth layers), not a hard outline.border_widthis driven to zero inmembrane_resolve—edges read as soft gradients frommembrane_geometry, not stroked borders.- Custom window chrome (
window_decoration = .CUSTOM) so title bar and buttons match the same language.
Using Membrane in an app¶
You build a Theme value once (or when switching skins) and hand it to FlashBang. The Membrane sources live under themes/membrane/ in the FlashBang tree (package name membrane). Point an import at that directory using whatever layout you use—relative path, a collection: root, a git submodule, etc.:
import membrane "../path/to/flashbang/themes/membrane" // adjust to your layout
flashbang.set_theme(membrane.create())
Scoped overrides still use push_theme / pop_theme like any other theme. Membrane does not change the immediate-mode stack rules—only the Theme contents.
How the engine maps to Membrane¶
FlashBang’s contract is: widgets call style.resolve_surface (which delegates to theme.resolve when set), optionally apply_color_override, then emit_surface. emit_surface always runs theme.geometry first (if non-nil), then the standard body / border / focus ring.
Membrane wires every optional hook on Theme in membrane.create():
Theme field |
Membrane proc | Role |
|---|---|---|
resolve |
membrane_resolve |
State + extruded → ResolvedSurface with depth, soft edges, highlight/shadow colors, no hard border. |
geometry |
membrane_geometry |
Layered semi-transparent rects before the body; uses ResolvedSurface.depth, edge_softness, corner_radius. |
check_indicator |
membrane_check_indicator |
Checkbox checkmark from overlapping circular blocks, each passed through membrane_geometry. |
separator_geometry |
membrane_separator_geometry |
Groove: paired shadow/highlight 1px lines. |
group_box_label |
membrane_group_box_label |
Pill label: resolve + geometry + body fill. |
window_chrome |
membrane_window_chrome |
Min/max/close (and related) as membrane blocks—see themes/membrane/window_chrome.odin. |
So: resolution picks numbers and colors; geometry (and the specialized procs) turn those into extra draw ops the flat engine never emits by itself.
membrane_resolve (custom ResolveSurfaceProc)¶
Implemented in themes/membrane/resolve.odin.
- Depth sign — Starts from
extrusion_depthifextruded, else negativeindentation_depth, so raised vs inset controls get opposite shading semantics without separate widget code paths in the resolver. WidgetVisualState— Adjusts depth, edge softness, and surface tint (e.g. hover sharpens edges and lightens slightly; press usespress_depth_scaleto invert depth; disabled flattens and desaturates).- Tint from depth — Small lighten/darken from signed depth so extruded vs recessed read clearly.
- Highlight / shadow colors —
style.compute_edge_colorscombinessurface, signeddepth,light_angle, and strength fields fromTheme. ResolvedSurface— Setsborder_width = 0on purpose: Membrane relies onmembrane_geometryfor edge read, notemit_surface’s stroke pass.
This is the clearest example of extruded as a hint: the same resolver runs for many widgets; extruded chooses which depth convention applies.
membrane_geometry (custom SurfaceGeometryProc)¶
Implemented in themes/membrane/geometry.odin.
- Casts
draw_list_rawto^rendering.DrawListand emitsMEMBRANE_LAYERS(10) stacked shadow rects on one diagonal and highlight rects on the other, with increasing offset, slight grow, and falling alpha—soft neumorphic rim. resolved.depthnonnegative vs negative flips which side is “lit,” so recessed widgets invert the direction.push_z_layer(list, list.current_z - 1)so these layers sit under the main body neighbors paint over—documented as important for clean overlap (see also comments insplit_pane.odinin the library).spreadis clamped by widget size so tiny indicators do not get huge halos.
emit_surface calls this before the flat body fill, so the body sits on top of the gradient stack.
Other Membrane-specific pieces (short)¶
- Checkmark — Seven circular blocks along a polyline path; pass 1 all
membrane_geometry, pass 2 all bodies so interiors stay clean. - Separators — Two thin fills (shadow + highlight) for a crease; no shared “separator widget” magic beyond
SeparatorGeometryProc. - Group box label — Resolves
NORMAL+ extruded, rounds into a pill radius, runsmembrane_geometry, then pillRectFill. - Window chrome — Composes
membrane_geometryand small rects/circles for controls; full detail inwindow_chrome.odin.
Contrasting with default_theme¶
style.default_theme() leaves resolve, geometry, and the other hooks nil. The engine uses the built-in flat resolve_surface path and emit_surface without pre-layers. Membrane is the same pipeline—only the Theme value fills in behavior through hooks.
Source layout (FlashBang repo)¶
| File | Contents |
|---|---|
themes/membrane/theme.odin |
create() — palette, lighting, spacing, hook assignments. |
themes/membrane/resolve.odin |
membrane_resolve. |
themes/membrane/geometry.odin |
membrane_geometry, indicator, separator, group box label. |
themes/membrane/window_chrome.odin |
membrane_window_chrome. |
Membrane’s own .odin files import FlashBang subpackages (e.g. style, rendering) using paths wired for this repository’s build. Your project must resolve those imports consistently (same FlashBang checkout, matching odin root or collection paths). The important API surface for apps remains membrane.create() -> style.Theme and flashbang.set_theme. When you integrate FlashBang (submodule, fork layout, or a future packaged layout), align Membrane’s imports with that setup—the theming engine does not depend on any one import prefix.