zaxis 0.1.0

Style and Theme

Semantic tokens, typed overrides, local inheritance and safe painters.

Style is the effective value consumed by existing layout and paint. Theme is an optional source of palette, metrics, typography, density and explicit overrides. A new Context still uses neutral grey Style::default(); existing examples do not opt into another palette automatically.

use zaxis::{Color, Density, Theme};
context.set_theme(Theme::light().accent(Color::rgb(35, 85, 155)).density(Density::Compact));
let effective = context.style();

Built-ins: Theme::dark(), light(), high_contrast(). Density is independent of palette. Compact scales geometry, spacing and padding by 0.8; comfortable uses base metrics. Font sizes do not change with density.

Tokens and actual consumers

Palette exposes background; surface, raised and control surfaces; foreground, muted and disabled text; accent/on-accent; selected/on-selected; focus; border; success/warning/error with contrasting foregrounds. accent(...) chooses black or white for on-accent by measured contrast. Selection is a separate surface token. Metrics exposes spacing, padding, control height, corner radius, border width, elevation (Shadow), background backdrop blur, opacity and blur opacity. Global blur applies to Root, Window and popup backgrounds. Controls and list rows remain opaque and sharp; a control blur requires an explicit builder or surface override. Typography supplies small/body/heading/title/code sizes (code is 13 px; the Code role is set in the monospace family, see Text) and, in Typography::weights, a FontWeight per role plus control (labels of Button, tabs, menus) and an optional selected (active tab). Use Text::typography(TypographyRole::Heading); ordinary text keeps Style::font_size and the body weight. Every weight defaults to Regular, so a theme looks unchanged until a weight is set. TextStyle also carries family and tabular_numbers for a subtree:

theme.typography.weights.heading = FontWeight::SEMIBOLD;
theme.typography.weights.control = FontWeight::MEDIUM;

Components override their weight through typed styles: TextStyle::weight, ButtonStyle::font_weight (variants and semantic status styles merge into it), TextEditStyle::font_weight, WindowStyle::title_font_weight, TitleBarStyle::font_weight, TableStyle::header_font_weight and DisclosureStyle::font_weight (tree rows and collapsing headers). See Font weights for the order. The family itself is chosen by Context::with_fonts or RunOptions::with_font_family; a theme cannot switch it.

The initial audit found these separate dependencies. Theme now resolves values into the same fields used by the existing public components:

ComponentPreviously separate defaultsEffective consumers
Buttonbutton colors; fixed radius 5/minimum 24legacy colors/padding/font, button.surface, minimum geometry
Checkboxbutton colors, radius 4, text-colored checkaccent/on-accent, checkbox.body/indicator, side/gap/stroke
Sliderheight 28, track 6, thumb 8; accent from textaccent/status colors, slider.track/fill/thumb, metrics
TextEditown fill/selection/placeholder/cursorlegacy editor fields, text_edit.surface, geometry, caret, selection foreground
NumberInput/DragValueNumberStyle's grey fillsresolved number fields, number.surface; editing still uses TextEdit
Window/Rootshared panel/title metricswindow.body/title, padding/title metrics, shadow/blur
TitleBarliteral greys/red and fixed fonttitle_bar.surface/controls, semantic error, caption metrics
Popuppadding 2/gap 4/radius 4popup.surface/geometry; children inherit creating Ui
ComboBoxseparate trigger/popup/text/selection paletteresolved combo_box, trigger/option; scrollbar inherits Ui
ColorPickerfixed row/editor geometry, button-like fieldscolor_picker.body/field and metrics; RGB/HSV colors stay accurate
ScrollAreaseparate grey thumbs/hintsresolved scroll and scroll.thumb states
Grid/Tableseparate fills/stripes/selection/cell metricsresolved grid/table, grid surface, table row/header surfaces
Text/Separatortext/border color; fixed separator thickness; Regular weighttext/ separator typed overrides; text weight from TextStyle or typography role
Loader/Progressmotion size, text-colored progressloader, progress.track/fill, accent, existing scheduler
Tabs/SelectionIndicatorselected Button, text-colored indicatorButton selected layer, independent accent indicator
SplitPaneexisting handle palette/panel surfacesresolved split colors/spacing; explicit SplitSurface remains
CollapsingHeader/TreeViewshared compact disclosure rowcollapsing.header/tree.row ControlStyle, density geometry, typography, motion; optional guides

Shapes, renderer clipping, glyph keys, tessellation cache and draw protocol remain the backend. Colors change affected paint descriptions; metrics rerun immediate layout and choose corresponding size/wrap text keys. Style revisions are not global mesh or glyph cache keys.

Explicit overrides and precedence

None inherits. Some(TRANSPARENT), Some(Border::NONE), zero radius, opacity, shadow or blur are explicit values. No comparison with defaults guesses override presence.

use zaxis::{Border, Button, ButtonStyle, ControlStyle, SurfaceStyle, Theme};
let mut theme = Theme::dark();
theme.overrides.button.surface.idle.border = Some(Border::NONE);
theme.overrides.number.height = Some(40.0);
context.set_theme(theme.clone().density(zaxis::Density::Compact));
// Explicit border and numeric height survive accent/density changes.
ui.add(Button::new("Save").style(ButtonStyle {
    surface: ControlStyle {
        idle: SurfaceStyle::fill(zaxis::Color::rgb(70, 95, 125)),
        ..Default::default()
    },
    ..Default::default()
}));

Order: tokens → theme component overrides → outer/inner Ui overrides → instance style/builders → state layers. Existing NumberStyle, GridStyle, TableStyle, ComboBoxStyle, ScrollStyle, SplitStyle and their builders remain. A complete specialized .style(...) replaces that component's defaults. Their *StyleOverride variants expose individual optional fields in theme/scoped patches. Nested surface/state patches merge property by property.

Style::collapsing and Style::tree are optional-field typed styles. Their StyleOverrides fields merge individual properties, including local with_style / with_theme and instance .style(...). Shared DisclosureStyle exposes height, padding, chevron size/stroke, icon size/gap, font size, motion and ControlStyle. CollapsingStyle adds content padding/spacing and optional height animation; TreeStyle adds indent, guide Border and scroll style. Explicit transparency, zero and Border::NONE remain overrides. Fixed tree row height must stay positive.

ControlState has independent enabled, hovered, pressed, selected, focus and semantic-status flags. Layers: idle, selected, semantic status, then disabled or pressed or hover, then focus. Disabled never runs hover rules. Focus border is an immediate layer over selected/pointer surfaces; focus.border = Some(Border::NONE) explicitly removes it. SurfaceStyle provides solid fill (SurfaceStyle::fill), gradients, foreground, border, radius, shadow, opacity and optional blur in each state. Missing state properties retain the base/selected appearance.

Scalar instance geometry builders (size, width, height, padding, font size, rounding) stay invariant during interaction. Button/Checkbox .border(...) is invariant except for focus; .blur(...) fixes the instance blur. Slider .color(...) fixes its filled track/thumb accent. TextEdit .text_color(...) is invariant. Typed surface.idle properties set the base and may be changed by state rules. Layout/hit bounds do not follow animated surface radius/shadow/border.

HoverStyle and Hover use this same resolver. Per-widget hover preset beats wrapper; wrapper beats theme defaults. Explicit presets override their listed properties on a typed hover patch. HoverStyle::NONE suppresses preset effects while retaining explicit typed state rules. Hover::on_hover remains an enabled/unpressed overlay.

Local subtrees and popups

use zaxis::{Color, StyleOverrides, Theme};
ui.with_theme(&Theme::light(), |ui| {
    ui.button("Local light button");
    ui.with_style(&StyleOverrides {
        text_color: Some(Color::rgb(70, 40, 100)), ..Default::default()
    }, |ui| { ui.vertical(|ui| { ui.label("Nested override"); }); });
});
ui.button("Original theme again");

Scopes restore after normal return, early closure return and unwinding. Styles live on Ui, never in temporarily modified global Context. Horizontal/vertical, sized/fill, Grid/Table cells, ScrollArea rows, Presence, pages, SplitPane panels and nested children inherit. Popup inherits its creating subtree despite drawing above other windows; floating ColorPicker carries the originating style too. with_theme supplies a new subtree base; with_style merges into the current value. Local background never changes Context's native clear color. A scope does not paint a backing panel automatically; supply a container/shape where needed.

Safe control painting

Button, Checkbox, Slider expose .painter(PaintMode, callback). Replace skips standard part painting including its blur; Before/After explicitly compose. Checkbox calls for body/indicator; Slider calls for track/thumb, with standard fill included in its track part. ControlPaint supplies real bounds, resolved surface, all state flags and a value snapshot.

use zaxis::{Button, PaintMode, Shape};
ui.add(Button::new("Custom").painter(PaintMode::Replace, |p, info| {
    p.paint(Shape::rect(info.bounds, info.style.fill.unwrap().start)
        .corner_radius(info.style.rounding.unwrap())
        .border(info.style.border.unwrap()));
}));

Painter::paint/text/clip_rect expose safe commands without Ui, mutable model, layout or hit registration. The component owns capture, focus, Id and mutation. Use size builders for a different allocation. Commands use normal caching and clipping. Callbacks may borrow and are never retained in Context; closure addresses are never keys. They run per UI pass, so do not mutate the model through captured interior mutability.

Scheduling and retained state

Identical set_style/set_theme do not request another redraw. Theme equality is checked before resolution. Local equality preserves stable animation channels. Default theme application snaps and preserves focus, capture, popup, scroll, editor selection/undo, panel geometry and bound application values. set_theme_animated(theme, TweenOptions::new(duration)) explicitly uses the existing scheduler. Forward finite tweens only; reduced motion snaps. All metrics and geometry use the target immediately. Colors interpolate in linear-light sRGB. Optional surface colors interpolate when both endpoints explicitly contain them; a change in inheritance of that property applies immediately. Completed palette transitions stop repaint.

Contrast and limits

Tests check ordinary/muted text on background/surface/raised/control/hover/pressed at 4.5:1; on-accent/on-selected and semantic foregrounds at 4.5:1; focus on surface/control/hover/selected at 3:1. Dark Theme uses brighter muted text than legacy Style to pass the hover-surface threshold. Disabled text is intentionally dim and excluded from the normal text criterion. Picker markers intentionally use black/white against arbitrary actual RGB/HSV colors. Custom alpha, gradients, opacity and blur may reduce contrast. Composite over the actual backdrop and test worst-case endpoints with contrast_ratio. Choosing on-accent alone cannot guarantee accent/focus contrast against all backgrounds. Local themes need appropriate backing surfaces; shadows stay inside active clips.

Migration

Style::default, existing public fields, Context::style/set_style, Ui::style, struct updates (..Style::default()) and old builders remain. set_style installs exactly its supplied Style and clears the Theme source; it does not rederive component palettes. Use Theme for changes that should propagate across defaults. Editing resolved legacy fields is not a token edit. Explicit typed component patches take priority over their legacy fallback fields. Source incompatibility: exhaustive Style/specialized-style literals need the new fields; prefer ..Default::default(). Button's default generic parameter preserves ordinary Button type annotations; a painted button carries its callback type. Checkbox/Slider retain their borrowed lifetimes. Compatibility exports remain.

Run cargo run --example themes to compare actual controls, density, overrides, paint hook, selection, focus, overlays and the font weight scale (SemiBold headings, Medium captions, Cyrillic and emoji in every weight).

Glass

An iOS-style glass look for interactive controls: a translucent body over a backdrop that is blurred, made more vivid and bent inside a narrow edge band like a convex lens, with a thin specular rim lit from the top left. It is one backdrop effect (BackdropEffect::glass), so it is drawn by the renderer from the control's real rounded shape, not painted from layers.

use zaxis::{Button, ButtonVariant, Theme};

ctx.set_theme(Theme::dark().glass(1.0));            // every glass-capable control
ui.add(Button::new("Play").variant(ButtonVariant::Glass));
ui.add(Button::new("Buy").variant(ButtonVariant::Solid).glass(1.0)); // tinted glass
ui.add(Button::new("Flat").glass(0.0));              // opt out of a glass theme

Theme::glass(strength) (Metrics::glass) sets SurfaceStyle::glass on buttons, checkboxes, switch and slider thumbs, the segmented plate and its selection, and combo box triggers; tracks and wells become faint veils so the backdrop shows through the whole control. Ghost buttons stay flat. SurfaceStyle::glass sets it for one surface of any ControlStyle. A glass surface blurs by 10 logical pixels unless blur is set, and its fill only tints the glass.

Glass shows only over something drawn behind it (an image, a gradient, other content); over a flat window background it is a subtle tinted rim. Limits: radio buttons, text fields, number inputs, tab bars and menus stay flat; the lens bends toward content inside the control's own bounds and never reads from outside it; there is no chromatic dispersion.

Dock tokens

Style::dock and ThemeOverrides::dock contain DockStyle; local/builder patches merge optional values and nested SurfaceStyle / TabsStyle fields. Unspecified colors inherit the active palette. Density changes the inherited spacing/padding and tab metrics.

FieldDefault Tiled / Flat
preset, motionTiled, Snappy
gaptheme spacing (8 px comfortable) / 1 px
panel_radius10 px / 0 px; arbitrary per-corner radii supported
surface, inactive_bordertheme window surface and border
padding, minimumtheme spacing; 120 × 80 logical px
tabsinherited tab strip controls and header metrics
ring_width, ring_gap2 px and 1 px / 1 px and 0 px
ring_active, ring_inactiveaccent; muted foreground at 45% alpha when inactive
ring_glow5 px / 0 px; faint external contour strokes
ring_gradientfalse; optional six-second rotating two-color contour
preview_coloraccent; translucent fill and border
springmotion preset; critical damping, mass 1, 0.2 px / 0.8 px/s thresholds

DockMotion::Snappy is 6 Hz; Smooth is 4.5 Hz; Off skips all Dock motion. Global reduced motion also disables motion and the rotating contour. A custom spring overrides the motion preset's spring, with invalid options falling back to the preset. surface.rounding supplies the radius unless panel_radius is set. Surface gradients, borders, opacity, shadows and backdrop blur use the existing Appearance painter. The contour uses ordinary gradient-border geometry; no custom shader/material is necessary.

Edit on GitHub

On this page