docs overhaul, orientation API, fluid-sizing fixes, examples made honest
Documentation pass: every claim in docs/ and the meta files was audited against the source and the drift fixed — around ninety corrections. CONTRIBUTING and the CI workflow now run cargo test with --features test-support (the gated test_support module made both the documented commands and the CI build fail to compile), make example becomes make examples, make doctest-md and the debhelper requirement of make clean are documented, and patch shape asks for a CHANGELOG entry. theming.md loses the nonexistent surface.backdrop, gains the real gradient defaults (linear-rgb, oklab), the six slot variants including typography, the ten-field palette, a truthful effects-consumer table, the ThemePreference/from_hour API and a responsive-sizing note; the stale docstrings in src/theme that fed the drift are fixed too. architecture.md's "Known gaps" section is rewritten against reality (multi-touch slots, xdg-activation, a11y live regions and SetValue/Increment/Decrement are implemented), gains a module map, subsurfaces and window-lifecycle coverage, and correct crustace/loginmanager paths. widgets.md fixes the ten factual errors (stateless spinner, toast/combo via overlays(), tooltip hover contract, row has no max_width, scroll axes, multiline text_edit, dialog panic wording) and now states the column() 16 px default padding — the recurring ambush — plus row's differing 0 default and dialog's max_width. onboarding, README and cookbook get the remaining sweep: build/test instructions, complete example lists, img_widget, clipping-parity honesty, ~30 Hz software cap, read_rgba_pixels signature, tab indentation in snippets, and rustdoc-style links that rendered literally are gone everywhere. CHANGELOG is restructured per Keep a Changelog with the missing entries (window_resizable, claims_raw_touch, Row::align_top/fill_height, caret fixes, dependency pins) and the pad_v Added/Changed contradiction resolved. New adaptive-layout API: ltk::orientation() with the Orientation enum, backed by viewport_size()/set_viewport_size — the runtime records the main surface's physical dimensions on every configure, before App::on_resize, so view() can branch a layout on portrait vs landscape without hand-tracking resizes. The portrait rule matches Length::orient (square counts as portrait); embedders driving core::UiSurface call set_viewport_size themselves. Documented in the crate root's responsive-design section and architecture.md. Fluid-vs-fixed sizing fixes in widgets, all the same disease — fluid content inside a fixed-pixel box. TextEdit::fixed_width takes impl Into<Length> (f32 call sites keep compiling as px) and the time picker's digit fields move to Length::fluid( 72.0 ), matching their fluid font so digits can no longer outgrow the box. Dialog::max_width takes impl Into<Length> with a Length::fluid( 480.0 ) default so the card scales with the stock buttons inside it, and the card's interior no longer stacks the column() default 16 px padding on top of CARD_PADDING — that double inset squeezed the action row until its buttons clipped on narrow windows. App::on_pointer_axis now triggers a view rebuild and repaint; previously state mutated in the hook did not paint until the next unrelated event. Examples reworked to be honest demos: responsive's mode/density controls become stock buttons in a grid/column so they follow the modes they demonstrate instead of overflowing; dialog's openers stack vertically, and the example gains the app-level ESC handler so the ESC chain closes an open dialog first and quits second; widgets' tab strip now switches real per-tab pages; carousel gains pointer/touch drag through the horizontal-swipe hooks (crustace's pager pattern), one-tile-per-detent mouse wheel, and snap math driven by the real surface width from on_resize instead of a hardcoded 800; clip_path arranges its cells by ltk::orientation() and sizes them from the counter-axis of the flow.
This commit is contained in:
245
docs/theming.md
245
docs/theming.md
@@ -1,7 +1,8 @@
|
||||
# ltk theming
|
||||
|
||||
`ltk` reads a JSON theme document at startup and exposes a process-wide
|
||||
active state for widgets and applications to query. This document
|
||||
`ltk` loads a JSON theme document lazily — on the first theme accessor
|
||||
call, not at startup — and exposes a process-wide active state for
|
||||
widgets and applications to query. This document
|
||||
describes the on-disk format, the runtime APIs, and the slot conventions
|
||||
that built-in widgets expect.
|
||||
|
||||
@@ -20,6 +21,8 @@ A theme is a directory under one of:
|
||||
3. `/usr/share/ltk/themes/<id>/` — system-wide install path
|
||||
(`ltk-theme-default` Debian package).
|
||||
|
||||
The ordered list is queryable at runtime via `ltk::theme::search_paths()`.
|
||||
|
||||
The directory tree:
|
||||
|
||||
```text
|
||||
@@ -36,15 +39,22 @@ The directory tree:
|
||||
│ │ └── horizontal.svg wordmark (header / sign-in bars)
|
||||
│ └── dark/
|
||||
│ └── (same set, dark variants)
|
||||
└── icons/ symbolic + app icons
|
||||
├── app-default.svg fallback icon for unknown app ids
|
||||
├── apps/ per-application icons (firefox.svg, …)
|
||||
└── catalogue/ symbolic glyph catalogue
|
||||
├── filled/ solid silhouettes — preferred by default
|
||||
│ └── <category>/ general, system, window, …
|
||||
└── line/ outlined variants (same names)
|
||||
├── icons/ symbolic + app icons
|
||||
│ ├── apps/ per-application icons (firefox.svg, …)
|
||||
│ │ └── default.svg generic app icon shipped with the set
|
||||
│ └── catalogue/ symbolic glyph catalogue
|
||||
│ ├── filled/ solid silhouettes — preferred by default
|
||||
│ │ └── <category>/ general, system, window, …
|
||||
│ └── line/ outlined variants (same names)
|
||||
├── cursors/ X cursor files (consumed by the
|
||||
└── cursor.theme compositor, not parsed by ltk itself)
|
||||
```
|
||||
|
||||
Note a known mismatch: the `theme_app_default_icon()` fallback looks
|
||||
for `icons/app-default.svg`, a path the shipped default theme does not
|
||||
provide (its generic icon lives at `icons/apps/default.svg`), so that
|
||||
fallback never resolves against the default theme.
|
||||
|
||||
Branded assets in `branding/` and icons in `icons/` are picked up by
|
||||
convention — see [Branding assets](#branding-assets) and
|
||||
[Icons](#icons) below. Asset paths declared inside `theme.json` (e.g. a
|
||||
@@ -68,12 +78,18 @@ absolute paths work for system fonts but break relocatable installs.
|
||||
}
|
||||
```
|
||||
|
||||
`theme.id` must match the directory name. `theme.name` is shown in any
|
||||
theme-picker UI a shell builds on top of `ltk`.
|
||||
`theme.id` should match the directory name by convention — lookup goes
|
||||
by directory name and the id inside the JSON is not checked against it.
|
||||
`theme.name` is shown in any theme-picker UI a shell builds on top of
|
||||
`ltk`.
|
||||
|
||||
The other six sections are described below in order. The parser is strict
|
||||
(`deny_unknown_fields`): unknown keys at any level are an error so typos
|
||||
surface immediately.
|
||||
The other five sections are described below in order. The parser is
|
||||
strict (`deny_unknown_fields`) at most levels — the document root,
|
||||
`fonts`, `modes`, gradient stops, shadow entries — so typos there are
|
||||
an error. It is *not* strict everywhere: unknown keys directly inside a
|
||||
slot entry or inside a `meta` block are silently ignored, and the
|
||||
`surface` / `typography` slot bodies lose the check to serde's
|
||||
`#[serde(flatten)]`.
|
||||
|
||||
## `fonts`
|
||||
|
||||
@@ -100,9 +116,14 @@ weight. The map key (`"sora"`) is the family alias used inside the theme
|
||||
files at render time. The chain is walked in order; the first family that
|
||||
can rasterise the codepoint wins.
|
||||
|
||||
If none of the listed `sources` exist on disk, `ltk` falls back to its
|
||||
embedded Sora Regular (~50 KB, OFL 1.1) and stamps a red banner on every
|
||||
frame pointing at the missing-theme problem.
|
||||
Sources that fail to read are skipped with a stderr warning; when the
|
||||
whole block yields nothing, text falls back to the embedded Sora
|
||||
Regular (~50 KB, OFL 1.1). Missing fonts never trigger the red warning
|
||||
banner — that banner is reserved for the case where the `default`
|
||||
theme document itself cannot be found (see [Common
|
||||
errors](#common-errors)). The registry that backs this is built by
|
||||
`ltk::theme::build_font_registry()`, which the draw loop already calls
|
||||
at canvas creation time.
|
||||
|
||||
## `colors`
|
||||
|
||||
@@ -125,12 +146,20 @@ kebab-case. There is no distinction between "palette" colours (used in
|
||||
many places) and "raw" colours (single-use) — any hex can live here, and
|
||||
single-use literals are equally valid inline.
|
||||
|
||||
Inline colour values elsewhere in the document (slot values, gradient
|
||||
stops, shadow colours, window-controls tokens) additionally accept the
|
||||
functional forms `rgb(R, G, B)` and `rgba(R, G, B, A)` — R/G/B as
|
||||
integers or floats in `0..=255`, A as a float in `0.0..=1.0`. The
|
||||
top-level `colors` map is the asymmetric exception: its entries must
|
||||
be hex literals; a functional form there is rejected.
|
||||
|
||||
## `gradients`
|
||||
|
||||
Named paints. Two variants: `linear` and `radial`. Used both as fills for
|
||||
surfaces and as values referenced from a slot. Stops carry a `pos` (in
|
||||
`[0, 1]`, but stop positions outside that range are accepted and used for
|
||||
extrapolation) and a `color` (literal hex or `@reference`).
|
||||
surfaces and as values referenced from a slot. Stops carry a `pos`
|
||||
(alias: `position`; in `[0, 1]`, but stop positions outside that range
|
||||
are accepted and used for extrapolation) and a `color` (literal hex or
|
||||
`@reference`).
|
||||
|
||||
```json
|
||||
"gradients": {
|
||||
@@ -146,10 +175,13 @@ extrapolation) and a `color` (literal hex or `@reference`).
|
||||
}
|
||||
```
|
||||
|
||||
`space` is either `"srgb"` (perceptually quick, the default) or
|
||||
`"linear-rgb"` (interpolate in linear-light space, more uniform mid-tones
|
||||
for high-saturation gradients). `angle_deg` is the conventional CSS
|
||||
gradient angle (`0` = up, `90` = right, `180` = down, `270` = left).
|
||||
`space` is `"linear-rgb"` (interpolate in linear-light space — the
|
||||
default, physically correct and fast), `"srgb"` (interpolate in
|
||||
gamma-encoded sRGB; reproduces designs authored against it, but
|
||||
midpoints of saturated gradients look muddy) or `"oklab"` (perceptual;
|
||||
best for high-chroma brand gradients, slightly more expensive).
|
||||
`angle_deg` is the conventional CSS gradient angle (`0` = up, `90` =
|
||||
right, `180` = down, `270` = left).
|
||||
|
||||
Radial gradients use `center: [x, y]` (relative to the painted rect, both
|
||||
in `[0, 1]`) and `radius: r` (also relative).
|
||||
@@ -174,12 +206,24 @@ Named lists of inset shadows reused across surfaces. Convenient for the
|
||||
|
||||
Each entry has `offset: [x, y]` (logical pixels), `blur` (Gaussian sigma
|
||||
× 2, matching the CSS convention), optional `spread` (default `0`),
|
||||
`color` (literal or `@ref`), and `blend` (`normal`, `plus-lighter`, or
|
||||
`overlay`).
|
||||
`color` (literal or `@ref`), and `blend` (`normal`, `plus-lighter`,
|
||||
`overlay`, `multiply`, or `screen`).
|
||||
|
||||
Reference an entire stack from a slot with `"inset_shadows":
|
||||
"@glass-insets"`, or inline the array if you only use it once.
|
||||
|
||||
### Responsive sizing
|
||||
|
||||
Pixel values in a theme are *logical* design pixels: they resolve
|
||||
through the `Length` / `WidgetScaling` system (`Length::dp` for
|
||||
constant-physical sizes, `Length::fluid` for surface-proportional
|
||||
ones, `set_density`, and `Canvas::geom_px` / `Canvas::font_px` at draw
|
||||
time) — see
|
||||
[`docs/architecture.md#responsive-sizing`](./architecture.md#responsive-sizing).
|
||||
For text, the `theme::typography` module ships the `H0`…`BODY_XS` px
|
||||
constants plus the fluid `h0()`…`body_xs()` `Length` builders that
|
||||
scale with the surface's smaller dimension.
|
||||
|
||||
## `modes`
|
||||
|
||||
Two required entries: `light` and `dark`. Each carries the look the
|
||||
@@ -202,7 +246,7 @@ Both are optional. The runtime resolves them in two steps:
|
||||
```
|
||||
|
||||
`path` resolves against the theme directory. `fit` is one of `"cover"`,
|
||||
`"contain"`, `"stretch"`, `"center"` (default `"cover"`). The wallpaper
|
||||
`"contain"`, `"stretch"`, `"center"`, `"tile"` (default `"cover"`). The wallpaper
|
||||
bundle helper ([`ltk::WallpaperBundle::for_size`]) returns the right
|
||||
crop for landscape or portrait surfaces, so a single landscape SVG / PNG
|
||||
covers both.
|
||||
@@ -222,6 +266,7 @@ Per-mode tokens for the title-bar control buttons:
|
||||
|
||||
```json
|
||||
"window_controls": {
|
||||
"bar_bg": "@off-white",
|
||||
"icon": "#5F5F68",
|
||||
"hover_bg": "@navy/14",
|
||||
"pressed_bg": "@navy/24",
|
||||
@@ -231,7 +276,11 @@ Per-mode tokens for the title-bar control buttons:
|
||||
}
|
||||
```
|
||||
|
||||
Consumed by the [`window_button`](../src/widget/window_button.rs) widget
|
||||
`bar_bg` is the background fill of the server-side-decoration title
|
||||
bar strip the buttons sit on; the six remaining keys style the buttons
|
||||
themselves.
|
||||
|
||||
Consumed by the [`window_button`](../src/widget/window_button/) widget
|
||||
through [`theme_window_controls()`].
|
||||
|
||||
The actual SVG glyphs (`close`, `maximize`, `minimize`, `restore`) live
|
||||
@@ -249,7 +298,8 @@ The mode's slot table. Each entry is keyed by a stable id; widgets look
|
||||
their slot up by id and the slot's `meta.semantic` field supplies a
|
||||
human-readable hint that's useful in theme inspectors.
|
||||
|
||||
Three slot variants:
|
||||
Six slot variants: `color`, `linear`, `radial`, `shadows`, `surface`
|
||||
and `typography`.
|
||||
|
||||
#### `color`
|
||||
|
||||
@@ -263,7 +313,51 @@ Three slot variants:
|
||||
|
||||
Plain colour values. `value` is a literal hex or `@reference`. `meta` is
|
||||
optional but conventional: `palette/<role>` for the palette layer and
|
||||
`effect/<group>/<name>` for everything else.
|
||||
`effect/<group>/<name>` for everything else. Besides `semantic`, `meta`
|
||||
also carries three optional free-form fields the runtime ignores:
|
||||
`fluent` (equivalent token name in another design system), `usage`
|
||||
(guidance on where to use the slot) and `note`.
|
||||
|
||||
#### `linear` / `radial`
|
||||
|
||||
```json
|
||||
"chip-active": {
|
||||
"type": "linear",
|
||||
"angle_deg": 90,
|
||||
"stops": [
|
||||
{ "pos": 0, "color": "@cyan" },
|
||||
{ "pos": 1, "color": "@teal" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Inline gradient slots — the same fields as a [`gradients`](#gradients)
|
||||
entry (`angle_deg` + `stops` + optional `space` for `linear`; `center` +
|
||||
`radius` + `stops` + optional `space` for `radial`), declared directly
|
||||
in the slot table instead of being named and referenced. Resolved by
|
||||
widgets via [`theme_paint( id )`], which also promotes plain `color`
|
||||
slots to a solid paint.
|
||||
|
||||
#### `typography`
|
||||
|
||||
```json
|
||||
"body-m": {
|
||||
"type": "typography",
|
||||
"family": { "ref": "sora" },
|
||||
"weight": 400,
|
||||
"size": 16,
|
||||
"line_height": { "px": 24 }
|
||||
}
|
||||
```
|
||||
|
||||
A resolved text style, looked up via [`theme_text_style( id )`].
|
||||
`family.ref` names an entry of the top-level [`fonts`](#fonts) block;
|
||||
`weight` and `size` are required. `line_height` is either `{ "px": n }`
|
||||
(absolute) or `{ "mul": n }` (multiplier of the size). Optional fields
|
||||
with defaults: `style` (`normal` / `italic`), `letter_spacing`
|
||||
(`0`), `transform` (`none` / `uppercase` / `lowercase` /
|
||||
`capitalize`), `decoration` (`none` / `underline` / `strikethrough`)
|
||||
and `color` (a slot-id string).
|
||||
|
||||
#### `shadows`
|
||||
|
||||
@@ -287,7 +381,6 @@ Outer drop shadows applied via [`theme_shadows(id)`]. Same field shape as
|
||||
"fill": "@surface-glass-dark",
|
||||
"shadows": "shadows-glass",
|
||||
"inset_shadows": "@glass-insets",
|
||||
"backdrop": { "blur_px": 22.5 },
|
||||
"meta": { "semantic": "effect/glass/card" }
|
||||
}
|
||||
```
|
||||
@@ -299,18 +392,18 @@ The most expressive slot kind. Composes:
|
||||
- `shadows` — id of a `shadows` slot or an inline list. Optional.
|
||||
- `inset_shadows` — id of an `inset_stacks` entry (`@glass-insets`) or
|
||||
inline list. Optional.
|
||||
- `backdrop` — `{ "blur_px": <σ × 2> }` for backdrop blur. Optional;
|
||||
GLES backend renders it, software backend ignores it (documented
|
||||
parity gap).
|
||||
|
||||
## References
|
||||
|
||||
The `@name` syntax substitutes a palette / gradient / inset-stack value
|
||||
in place of an inline literal:
|
||||
|
||||
- `@cyan` — looks up `colors.cyan`, `gradients.cyan` or
|
||||
`inset_stacks.cyan` (collisions across sections are an error). When the
|
||||
resolved value is a colour, the alpha channel comes from the original.
|
||||
- `@cyan` — looks up `gradients.cyan` / `inset_stacks.cyan` first,
|
||||
then `colors.cyan`. A name defined in both `gradients` and
|
||||
`inset_stacks` is an error; a name defined in both `colors` and one
|
||||
of the token sections is *not* — the token silently wins, so avoid
|
||||
reusing names across sections. When the resolved value is a colour,
|
||||
the alpha channel comes from the original.
|
||||
- `@cyan/80` — the `/AA` suffix is a colour-only alpha override (two hex
|
||||
digits). Lets a single base `@navy` serve `@navy/14`, `@navy/24`,
|
||||
`@navy/99`, `@navy/D9` etc. without a separate entry per alpha.
|
||||
@@ -339,23 +432,28 @@ of them falls back to embedded defaults.
|
||||
| `accent` | toggle on, slider fill, focus ring | color |
|
||||
| `divider` | separator, toggle off, list item border | color |
|
||||
| `icon` | icon-button glyph colour | color |
|
||||
| `danger` | destructive / error foreground | color |
|
||||
| `danger-bg` | soft fill behind error states | color |
|
||||
|
||||
**Effects (optional but used by built-in widgets):**
|
||||
The default theme defines `danger` but not `danger-bg`, which
|
||||
therefore resolves to the built-in pink-wash default from the palette
|
||||
projection.
|
||||
|
||||
**Effects (optional):**
|
||||
|
||||
| id | consumer | type |
|
||||
| --- | --- | --- |
|
||||
| `shadows-glass` | every surface that opts into elevation | shadows |
|
||||
| `surface-card` | `Container::surface("surface-card")` | surface |
|
||||
| `surface-card-flat` | flat variant for software backend | surface |
|
||||
| `surface-panel` | overlay panels | surface |
|
||||
| `surface-slider-track` | `Slider` track background | surface |
|
||||
| `surface-slider-fill` | `Slider` filled portion | surface |
|
||||
| `surface-slider-track-flat` | software-backend slider track | surface |
|
||||
| `surface-slider-fill-flat` | software-backend slider fill | surface |
|
||||
| `surface-toggle-active` | `Toggle` on-state surface | surface |
|
||||
| `surface-slider-track` | `Slider` / `VSlider` track background | surface |
|
||||
| `surface-slider-fill` | `Slider` / `VSlider` filled portion | surface |
|
||||
| `surface-card` | no built-in widget reads it automatically — apps opt in via `Container::surface("surface-card")` | surface |
|
||||
| `shadows-glass` | referenced from the theme's own surface slots; no direct widget consumer | shadows |
|
||||
| `surface-panel`, `surface-toggle-active`, `surface-card-flat`, `surface-slider-track-flat`, `surface-slider-fill-flat` | reserved — currently unconsumed by any built-in widget | surface |
|
||||
|
||||
The `-flat` variants are used by the software backend, which lacks
|
||||
backdrop blur; the GLES backend uses the non-flat ones.
|
||||
Only the slider track / fill pair is looked up by widgets on their
|
||||
own. The `-flat` variants are *not* wired to the software backend
|
||||
automatically; a caller can pass one explicitly (e.g.
|
||||
`Slider::track_surface("surface-slider-track-flat")`), but nothing in
|
||||
`ltk` selects them per backend.
|
||||
|
||||
## Using the theme from app code
|
||||
|
||||
@@ -403,7 +501,7 @@ fn view( &self ) -> Element<Msg>
|
||||
| `theme_color( id )` | `Option<Color>` | Pull a single colour slot by id when it's not in the palette (`"surface-card-border"`, custom theme tokens). |
|
||||
| `theme_color_or( id, fallback )` | `Color` | Same, with a baked-in default — ergonomic in widget defaults so missing slots don't return `None`. |
|
||||
| `theme_paint( id )` | `Option<Paint>` | Slot may be a colour or a gradient — promotes a colour to `Paint::Solid` automatically. |
|
||||
| `theme_surface( id )` | `Option<Surface>` | Surface slot (fill + shadows + insets + backdrop). |
|
||||
| `theme_surface( id )` | `Option<Surface>` | Surface slot (fill + shadows + insets). |
|
||||
| `theme_resolve_surface( id )` | `Option<( Surface, Vec<Shadow> )>` | Same, but pre-resolves a `ShadowsRef::Named` reference to a flat `Vec`. Use this when you call `canvas.fill_surface` directly. |
|
||||
| `theme_shadows( id )` | `Option<Vec<Shadow>>` | Outer shadow stack. |
|
||||
| `theme_text_style( id )` | `Option<TextStyle>` | Typography slot (size, weight, line-height). |
|
||||
@@ -419,6 +517,10 @@ fn view( &self ) -> Element<Msg>
|
||||
| `theme_app_icon( name )` / `theme_app_default_icon()` | `Option<PathBuf>` | Per-app icons under `icons/apps/`. |
|
||||
| `theme_icon_path( "category/name" )` | `Option<PathBuf>` | Catalogue icon path (filled-then-line lookup). |
|
||||
| `theme_icon_rgba( "category/name", size )` | `Option<( Arc<Vec<u8>>, u32, u32 )>` | Rasterised + cached RGBA. Pair with `theme::tint_symbolic` for chrome glyphs. |
|
||||
| `theme_icon_tinted( name, size, tint )` | `Option<ImageData>` | One-call rasterise + tint (`theme_icon_rgba` + `tint_symbolic`). |
|
||||
| `decode_svg_bytes( bytes, size )` | `Option<( Arc<Vec<u8>>, u32, u32 )>` | Rasterise arbitrary SVG bytes (uncached, no path resolution); the primitive under `theme_icon_rgba`. |
|
||||
| `active_document()` | `Arc<ThemeDocument>` | The whole loaded document, for slot-typed lookups the per-slot helpers do not cover (e.g. iterating slots). |
|
||||
| `active_theme_id()` | `String` | Id of the active theme. |
|
||||
| `is_fallback_active()` | `bool` | `true` when the embedded B/W fallback theme is in force (no theme on disk). Useful to disable a theme-picker UI or warn the user. |
|
||||
|
||||
### Switching mode or document at runtime
|
||||
@@ -436,6 +538,18 @@ let doc = ltk::ThemeDocument::find( "midnight" )
|
||||
ltk::set_active_document( doc );
|
||||
```
|
||||
|
||||
`ThemeDocument::find( id )` walks the standard search paths;
|
||||
`ThemeDocument::load_from_dir( dir )` loads a specific directory
|
||||
(useful for previews or tests). Once loaded, `doc.mode( ThemeMode::Dark
|
||||
)` projects the per-mode content without installing the document.
|
||||
|
||||
For the phone-style "auto" setting, `ThemePreference` (`Light` /
|
||||
`Dark` / `Auto`) is the shell-side persisted preference:
|
||||
`pref.resolve( hour )` maps it to a concrete `ThemeMode`, with `Auto`
|
||||
delegating to `ThemeMode::from_hour( hour )` — dark between 19:00 and
|
||||
06:59 local time, light otherwise. The toolkit itself only ever sees
|
||||
the resolved `ThemeMode`.
|
||||
|
||||
The conventional wiring is to dispatch a message from the UI:
|
||||
|
||||
```rust,no_run
|
||||
@@ -472,7 +586,7 @@ story is "ltk re-runs `view()` every frame and you read fresh values".
|
||||
- **Do** read palette / surfaces / icons inside `view()`, every frame.
|
||||
- **Do** use `theme_color_or( id, fallback )` for non-palette slots so
|
||||
a custom theme that omits a slot still paints something sane.
|
||||
- **Do** use `theme_palette().<field>` for the eight common roles —
|
||||
- **Do** use `theme_palette().<field>` for the ten common roles —
|
||||
the palette is precomputed and cheap; named slot lookups only beat
|
||||
it when you genuinely need a non-canonical token.
|
||||
- **Don't** store `Color`, `Surface`, `Paint`, or icon `PathBuf`s in
|
||||
@@ -622,7 +736,10 @@ Categories under `catalogue/{filled,line}/`: `accessibility`, `actions`,
|
||||
`<g fill="#000000">`). The rasteriser keeps only the alpha channel
|
||||
for symbolic tinting, so the actual RGB of the source doesn't
|
||||
matter — pick `#000000` for consistency with the rest of the
|
||||
catalogue.
|
||||
catalogue. Note the catalogue SVGs' viewBoxes are cropped to the
|
||||
ink, not to a uniform grid: the rasteriser scales the *longest*
|
||||
edge to the requested size preserving aspect ratio, so a non-square
|
||||
glyph comes out smaller than `size` on its other axis.
|
||||
2. Optionally ship the `line/` variant in
|
||||
`catalogue/line/<category>/<name>.svg`.
|
||||
3. Reference it from widget code by its slash-separated stem, no
|
||||
@@ -645,6 +762,9 @@ let ( rgba, w, h ) = ltk::theme::icon_rgba( "window/close", 16 )?;
|
||||
// `ltk::theme_window_controls().icon` instead.
|
||||
let palette = ltk::theme_palette();
|
||||
let tinted = ltk::theme::tint_symbolic( &rgba, palette.icon );
|
||||
|
||||
// Or the one-call form combining both steps:
|
||||
let image = ltk::theme_icon_tinted( "window/close", 16, palette.icon );
|
||||
# Some( () )
|
||||
# }
|
||||
```
|
||||
@@ -652,7 +772,9 @@ let tinted = ltk::theme::tint_symbolic( &rgba, palette.icon );
|
||||
The `icon_rgba` + `tint_symbolic` pair is the standard pipeline for
|
||||
catalogue and chrome icons: rasterise once, recolour per mode via
|
||||
palette tokens. Themes ship a single SVG per glyph and the per-mode
|
||||
look comes from code, not from duplicated assets.
|
||||
look comes from code, not from duplicated assets. `tint_symbolic`
|
||||
multiplies the source alpha by `tint.a`, so a translucent tint
|
||||
attenuates the whole glyph rather than just recolouring it.
|
||||
|
||||
## Localisation
|
||||
|
||||
@@ -679,10 +801,12 @@ date_picker:
|
||||
dow_short_6: "S"
|
||||
```
|
||||
|
||||
`dow_short_<n>` is indexed from each locale's `first_dow` (Sunday-first
|
||||
in `en`, Monday-first in `es` / `fr` / `it` / `de` / `pt` / `pt_BR`).
|
||||
The `Locale` struct ships `first_dow: u8` and the date picker indexes
|
||||
`dow_short_*` accordingly.
|
||||
`dow_short_<n>` is indexed from each locale's first day of week
|
||||
(Sunday-first in `en`, Monday-first in `es` / `fr` / `it` / `de` /
|
||||
`pt` / `pt_BR`). The `Locale` struct's `first_dow` field is
|
||||
crate-private; apps pick a start day through the public
|
||||
`Locale::MONDAY_FIRST` / `Locale::SUNDAY_FIRST` constants and the date
|
||||
picker indexes `dow_short_*` accordingly.
|
||||
|
||||
Built-in widgets read these via `rust_i18n::t!( "context_menu.copy" )`
|
||||
at render time, so switching locale at runtime via
|
||||
@@ -729,9 +853,10 @@ A custom theme directory can live anywhere under
|
||||
`icons/catalogue/filled/<category>/<name>.svg`. The `icon_path`
|
||||
lookup resolves against whichever theme is active, so a partial
|
||||
catalogue overlays the default cleanly without forking the rest.
|
||||
- **Build a flat-only theme**: drop every `backdrop` block from the
|
||||
slots and route every `surface-*-flat` slot to a solid `colors`
|
||||
reference. Visual parity with the software backend is automatic.
|
||||
- **Build a flat theme**: route the `surface-*` slots to solid
|
||||
`colors` references and drop the shadow / inset decorations. A
|
||||
`color` slot promotes to a plain surface automatically, so the
|
||||
built-in widgets keep working with the same ids.
|
||||
|
||||
All three recipes keep the slot ids and reference shapes intact, so the
|
||||
built-in widgets continue to work without code changes.
|
||||
|
||||
Reference in New Issue
Block a user