docs overhaul, orientation API, fluid-sizing fixes, examples made honest
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

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:
2026-07-30 19:28:26 +02:00
parent 14572ebfb6
commit 1fd697aa6d
33 changed files with 1131 additions and 661 deletions

View File

@@ -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.