render, types, ci: resolution-time dp with per-canvas density, bounded GLES image cache, clippy gate, backend capability matrix
Length::dp no longer collapses to absolute pixels at construction: the design value travels in a new LengthBase::Dp variant and the density multiplication happens when the length is resolved. Previously dp( n ) baked in whatever density() returned at view-build time, so correctness across output changes depended on the view being rebuilt after set_density and in that order; now a density change is picked up by the very next paint with no reconstruction. Length::resolve keeps its signature (process density), and the new Length::resolve_with_density takes an explicit factor. dp becomes const in the bargain. Density also becomes overridable per canvas, the first step towards surface-local responsive state. SoftwareCanvas and GlesCanvas carry a density: Option<f32> analogous to the layout_viewport introduced for sub-canvas fluid resolution: None means "use the process global", Canvas::set_density pins a local factor, and sub-canvases inherit it. All canvas-routed resolution honours it — geom_px / font_px for stock-widget design pixels, and the new Canvas::resolve_geom / resolve_font for explicit Length values, which every widget now uses in place of the raw l.resolve( canvas.viewport_layout(), EM ) pattern (row, column, wrap_grid, spacer, container, separator, button, text, rich_text, text_edit, list_item, vslider, image, and the container draw path). Overlay sizing keeps resolving against the main surface with the global density, which is what it describes. New tests cover explicit-density resolution, resolution-time application, the local-over-global override and sub-canvas inheritance. The GLES image texture cache is now bounded. It was content-keyed but unbounded and never evicted, so a stream of distinct buffers — a photo carousel, video thumbnails — grew GPU memory for the lifetime of the canvas. The cache now tracks an estimated byte total (RGBA8, w × h × 4) against a 32 MiB budget and evicts least-recently-drawn textures on insert; the most recent entry is never evicted, so a single texture larger than the whole budget still draws and simply owns the cache until replaced. Drop-time cleanup is unchanged: drain deletes whatever the map holds. CI gains a Clippy step (workspace, all targets, test-support, -D warnings) sharing the build cache of the test job, with make clippy mirroring the invocation locally and CONTRIBUTING listing it. Run make clippy locally before pushing the first time — the gate has not seen the tree yet and pre-existing lints will fail CI until addressed. docs/backends.md formalises the software/GLES capability matrix that was previously scattered across per-method rustdoc: parity set (fills, strokes, text, images, paths, path clips), graceful degradations on software (flat-fill gradients, no shadows, no backdrop blur, hard bottom edge), GPU-only features (external textures), the shared Oklab-fallback limitation, and the cross-backend blit panic. Linked from README, onboarding and architecture's known-gaps list, which now states the parity gaps explicitly. The dp/density prose in architecture.md, lib.rs and the Length rustdoc is updated for resolution-time semantics and the per-canvas override.
This commit is contained in:
@@ -3,14 +3,16 @@
|
|||||||
# Runs on every push and pull request against `main`. Two independent
|
# Runs on every push and pull request against `main`. Two independent
|
||||||
# jobs share the cache layer but otherwise execute in parallel:
|
# jobs share the cache layer but otherwise execute in parallel:
|
||||||
#
|
#
|
||||||
# - `test` builds the workspace and runs the full test corpus.
|
# - `test` builds the workspace, runs the full test corpus and lints
|
||||||
|
# with Clippy (`-D warnings`, sharing the build cache).
|
||||||
# - `audit` runs `cargo audit` against the RustSec advisory database to
|
# - `audit` runs `cargo audit` against the RustSec advisory database to
|
||||||
# catch dependency CVEs without waiting for a human to remember to run
|
# catch dependency CVEs without waiting for a human to remember to run
|
||||||
# it locally.
|
# it locally.
|
||||||
#
|
#
|
||||||
# Note: ltk uses Modified Allman style (not rustfmt's default), so there
|
# Note: ltk uses Modified Allman style (not rustfmt's default), so there
|
||||||
# is no `cargo fmt --check` step here on purpose. If rustfmt ever ships
|
# is no `cargo fmt --check` step here on purpose. If rustfmt ever ships
|
||||||
# a stable Allman config, add a third `fmt` job.
|
# a stable Allman config, add a third `fmt` job. Clippy is unaffected by
|
||||||
|
# the formatting style — it lints semantics, not layout.
|
||||||
#
|
#
|
||||||
# Compatible syntax with GitHub Actions, so swapping host providers does
|
# Compatible syntax with GitHub Actions, so swapping host providers does
|
||||||
# not require touching this file. Gitea also reads `.github/workflows/`
|
# not require touching this file. Gitea also reads `.github/workflows/`
|
||||||
@@ -51,6 +53,7 @@ jobs:
|
|||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||||
|
components: clippy
|
||||||
|
|
||||||
- name: Cache cargo registry and target
|
- name: Cache cargo registry and target
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -61,6 +64,13 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --workspace --all-targets --features test-support
|
run: cargo test --workspace --all-targets --features test-support
|
||||||
|
|
||||||
|
- name: Clippy
|
||||||
|
# Same flags as the build so the artifact cache is reused and
|
||||||
|
# test code is linted too. Warnings are errors: an allow with a
|
||||||
|
# justification at the offending site is the escape hatch, not
|
||||||
|
# a CI-level mute.
|
||||||
|
run: cargo clippy --workspace --all-targets --features test-support -- -D warnings
|
||||||
|
|
||||||
# External-Markdown doctests. `cargo test --doc` only sees doctests
|
# External-Markdown doctests. `cargo test --doc` only sees doctests
|
||||||
# inside `src/`; the cookbook and widget reference under `docs/`
|
# inside `src/`; the cookbook and widget reference under `docs/`
|
||||||
# are stand-alone Markdown files that rustdoc has to be invoked
|
# are stand-alone Markdown files that rustdoc has to be invoked
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **Per-canvas pixel density** — `Canvas::set_density` pins a canvas (and the sub-canvases derived from it) to its own density factor for `Length::dp` resolution, overriding the process `set_density` global; `Canvas::density` reads the effective value. New `Canvas::resolve_geom` / `Canvas::resolve_font` resolve an explicit `Length` in geometry / font space with the canvas' density — widgets now route caller-supplied lengths through them, so a `dp` override follows the canvas it draws on. The hook for surfaces on outputs whose DPI differs from the process-wide one (an overlay on a second monitor, an embedder with several `UiSurface`s).
|
||||||
|
- **`Length::resolve_with_density`** — `Length::resolve` with an explicit density for `LengthBase::Dp`, instead of the process `density()`.
|
||||||
|
- **`docs/backends.md`** — the canonical software/GLES capability matrix: what renders identically, what degrades gracefully on software (gradients, shadows, backdrop blur, bottom fade) and what is GPU-only, replacing per-method rustdoc archaeology. Linked from README, onboarding and the architecture known-gaps list.
|
||||||
|
- **Clippy in CI** — the `test` job lints with `cargo clippy --workspace --all-targets --features test-support -- -D warnings`; `make clippy` mirrors the invocation locally.
|
||||||
|
|
||||||
- **`ListItem::trailing_icon( rgba, w, h )`** — right-aligned icon slot (disclosure arrow) drawn at `TRAILING_ICON_SIZE` (21 px) and vertically centered, alongside the existing leading `icon`. Coexists with `trailing` text, which shifts to the icon's left. Symbolic icons should be pre-tinted by the caller (`tint_symbolic`), matching the leading-icon contract.
|
- **`ListItem::trailing_icon( rgba, w, h )`** — right-aligned icon slot (disclosure arrow) drawn at `TRAILING_ICON_SIZE` (21 px) and vertically centered, alongside the existing leading `icon`. Coexists with `trailing` text, which shifts to the icon's left. Symbolic icons should be pre-tinted by the caller (`tint_symbolic`), matching the leading-icon contract.
|
||||||
- **`ListItem::pad_h( impl Into<Length> )`** — per-item override of the horizontal inset between the row edge and its content; without it the theme default (16 px) applies.
|
- **`ListItem::pad_h( impl Into<Length> )`** — per-item override of the horizontal inset between the row edge and its content; without it the theme default (16 px) applies.
|
||||||
- **`Row::align_top()` / `Row::fill_height()`** — pin children to the row's top edge instead of the default vertical centering, or stretch every non-spacer child to the row's inner height (the row itself is still sized by its tallest child), for siblings whose natural heights differ by a few font-metric pixels.
|
- **`Row::align_top()` / `Row::fill_height()`** — pin children to the row's top edge instead of the default vertical centering, or stretch every non-spacer child to the row's inner height (the row itself is still sized by its tallest child), for siblings whose natural heights differ by a few font-metric pixels.
|
||||||
@@ -22,6 +27,8 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- **`Length::dp` now applies the density at resolution time, not at construction.** The value carries its design pixels in a new `LengthBase::Dp` variant and `resolve` multiplies by the density in effect when it runs, so a `set_density` change takes effect on the next paint without rebuilding the view's lengths — previously a `dp` value was frozen to the density read when it was constructed. Behaviour is unchanged for code that sets density once at startup.
|
||||||
|
- **The GLES image texture cache is now bounded** to 32 MiB of estimated GPU memory with least-recently-drawn eviction (the in-use entry is never evicted). Previously it grew without limit for the canvas' lifetime, so a stream of distinct buffers (photo carousel, video thumbnails) could exhaust GPU memory.
|
||||||
- **`OverlaySpec::size`** is now `( Length, Length )` (was `( u32, u32 )`), resolved against the main surface when the overlay is materialized; wrap existing sizes in `Length::px( … )` for the old fixed behaviour.
|
- **`OverlaySpec::size`** is now `( Length, Length )` (was `( u32, u32 )`), resolved against the main surface when the overlay is materialized; wrap existing sizes in `Length::px( … )` for the old fixed behaviour.
|
||||||
- **`TextEdit::font_size`**, **`TextEdit::fixed_width`** and **`Separator::thickness`** now take `impl Into<Length>` (were `f32`), resolved like the button label (font space) / geometry space; the `f32` sentinels are gone (`f32` call sites still compile via `Into<Length>`).
|
- **`TextEdit::font_size`**, **`TextEdit::fixed_width`** and **`Separator::thickness`** now take `impl Into<Length>` (were `f32`), resolved like the button label (font space) / geometry space; the `f32` sentinels are gone (`f32` call sites still compile via `Into<Length>`).
|
||||||
- **Dependency pins**: `fontdue = "=0.9.3"` and `ignore = "=0.4.23"` are pinned exactly because newer releases require a rustc newer than the declared Rust 1.85 MSRV (Debian stable's toolchain).
|
- **Dependency pins**: `fontdue = "=0.9.3"` and `ignore = "=0.4.23"` are pinned exactly because newer releases require a rustc newer than the declared Rust 1.85 MSRV (Debian stable's toolchain).
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ make all # cargo build --release
|
|||||||
make test # cargo test --features test-support
|
make test # cargo test --features test-support
|
||||||
make doctest-md # typecheck the Rust snippets in docs/*.md
|
make doctest-md # typecheck the Rust snippets in docs/*.md
|
||||||
make stylecheck # mechanical style checks (tabs, attribute spacing)
|
make stylecheck # mechanical style checks (tabs, attribute spacing)
|
||||||
|
make clippy # semantic lints, warnings as errors (mirrors CI)
|
||||||
make audit # cargo audit (installs cargo-audit on first run)
|
make audit # cargo audit (installs cargo-audit on first run)
|
||||||
make doc # cargo doc --no-deps
|
make doc # cargo doc --no-deps
|
||||||
make examples # run every example under examples/ in turn
|
make examples # run every example under examples/ in turn
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -5,7 +5,7 @@ DOCDIR ?= /usr/share/doc/libltk-doc/html
|
|||||||
# ignore filesystem entries with the same name — without this `examples`
|
# ignore filesystem entries with the same name — without this `examples`
|
||||||
# silently no-ops because the `examples/` directory exists, and `doc`
|
# silently no-ops because the `examples/` directory exists, and `doc`
|
||||||
# would do the same once `target/doc` is around.
|
# would do the same once `target/doc` is around.
|
||||||
.PHONY: all test doctest-md stylecheck audit doc install examples clean distclean
|
.PHONY: all test doctest-md stylecheck clippy audit doc install examples clean distclean
|
||||||
|
|
||||||
all:
|
all:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
@@ -25,6 +25,11 @@ doctest-md:
|
|||||||
stylecheck:
|
stylecheck:
|
||||||
./scripts/style-check.sh
|
./scripts/style-check.sh
|
||||||
|
|
||||||
|
# Semantic lints, mirroring the CI invocation. Warnings are errors so a
|
||||||
|
# local run predicts the CI verdict.
|
||||||
|
clippy:
|
||||||
|
cargo clippy --workspace --all-targets --features test-support -- -D warnings
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
@command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit --locked
|
@command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit --locked
|
||||||
cargo audit
|
cargo audit
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ on the roadmap.
|
|||||||
| [`docs/architecture.md`](docs/architecture.md) | Runtime model, overlays, animation, theming, performance and where the cost of a frame lives. |
|
| [`docs/architecture.md`](docs/architecture.md) | Runtime model, overlays, animation, theming, performance and where the cost of a frame lives. |
|
||||||
| [`docs/widgets.md`](docs/widgets.md) | Per-widget catalogue: what each one is, when to use it, minimal example, see-also. |
|
| [`docs/widgets.md`](docs/widgets.md) | Per-widget catalogue: what each one is, when to use it, minimal example, see-also. |
|
||||||
| [`docs/theming.md`](docs/theming.md) | JSON theme schema, slot conventions, runtime APIs. |
|
| [`docs/theming.md`](docs/theming.md) | JSON theme schema, slot conventions, runtime APIs. |
|
||||||
|
| [`docs/backends.md`](docs/backends.md) | Software/GLES capability matrix — what renders identically, what degrades, what is GPU-only. |
|
||||||
| [`docs/cookbook.md`](docs/cookbook.md) | Concrete recipes — slide-in panels, password fields, runtime theme toggle, channel-driven state, embedding without `ltk::run`. |
|
| [`docs/cookbook.md`](docs/cookbook.md) | Concrete recipes — slide-in panels, password fields, runtime theme toggle, channel-driven state, embedding without `ltk::run`. |
|
||||||
| `cargo doc --open` | Per-item rustdoc for the public API. |
|
| `cargo doc --open` | Per-item rustdoc for the public API. |
|
||||||
| [`CHANGELOG.md`](CHANGELOG.md) | What changed in each release, and what is pending unreleased. |
|
| [`CHANGELOG.md`](CHANGELOG.md) | What changed in each release, and what is pending unreleased. |
|
||||||
|
|||||||
@@ -193,11 +193,11 @@ Every size in a widget tree is a `Length`, resolved to concrete pixels at layout
|
|||||||
`ltk` offers two adaptation strategies, and both live in the same `Length` type so an app can mix them per value:
|
`ltk` offers two adaptation strategies, and both live in the same `Length` type so an app can mix them per value:
|
||||||
|
|
||||||
- **Fluid** (`Length::fluid(n)`, and the raw `vmin` / `vmax` / `vw` / `vh` / `orient` units): surface-proportional. `fluid(n)` reads a single design pixel `n` as `vmin(n / fluid_reference() * 100).clamp(n * FLUID_MIN, n * FLUID_MAX)` — at a surface whose short side equals the reference (412 px by default) it is exactly `n`, and it scales with the short side elsewhere, auto-clamped to `[0.7n, 1.5n]`. This tracks the width in portrait and the height in landscape, because the short side *is* the width in portrait and the height in landscape. `orient(portrait, landscape)` is the escape hatch for a different percentage per orientation.
|
- **Fluid** (`Length::fluid(n)`, and the raw `vmin` / `vmax` / `vw` / `vh` / `orient` units): surface-proportional. `fluid(n)` reads a single design pixel `n` as `vmin(n / fluid_reference() * 100).clamp(n * FLUID_MIN, n * FLUID_MAX)` — at a surface whose short side equals the reference (412 px by default) it is exactly `n`, and it scales with the short side elsewhere, auto-clamped to `[0.7n, 1.5n]`. This tracks the width in portrait and the height in landscape, because the short side *is* the width in portrait and the height in landscape. `orient(portrait, landscape)` is the escape hatch for a different percentage per orientation.
|
||||||
- **Physical** (`Length::dp(n)`): constant physical size. `dp(n)` is `n × density()`, where `density()` is a process-wide factor (default `1.0`, typically set from the output DPI via `set_density`). It does not scale with the surface, only with pixel density — the mainstream HiDPI `dp`.
|
- **Physical** (`Length::dp(n)`): constant physical size. `dp(n)` resolves to `n ×` the pixel density (default `1.0`, typically set from the output DPI via `set_density`). It does not scale with the surface, only with pixel density — the mainstream HiDPI `dp`. The multiplication happens at **resolution time**, not when the `Length` is constructed: a `dp` value carries its design pixels, so a density change takes effect on the next paint without rebuilding the view's lengths.
|
||||||
|
|
||||||
Stock widgets do not hard-code either strategy. Each carries a design pixel per dimension (e.g. `button` height 48, font 16) and resolves it through the process-wide `WidgetScaling` mode: `Length::widget(n)` returns `fluid(n)` under `WidgetScaling::Fluid` (the default) or `dp(n)` under `WidgetScaling::Physical`. `set_widget_scaling(mode)` flips it once for the whole app. An explicit `Length` on an individual widget (`button.height(...)`, `text_edit.height(...)`, `font_size(...)`) bypasses the mode entirely — the mode only decides the meaning of the *default* design pixels, never an override the app wrote on purpose.
|
Stock widgets do not hard-code either strategy. Each carries a design pixel per dimension (e.g. `button` height 48, font 16) and resolves it through the process-wide `WidgetScaling` mode: `Length::widget(n)` returns `fluid(n)` under `WidgetScaling::Fluid` (the default) or `dp(n)` under `WidgetScaling::Physical`. `set_widget_scaling(mode)` flips it once for the whole app. An explicit `Length` on an individual widget (`button.height(...)`, `text_edit.height(...)`, `font_size(...)`) bypasses the mode entirely — the mode only decides the meaning of the *default* design pixels, never an override the app wrote on purpose.
|
||||||
|
|
||||||
Both `density()` and `widget_scaling()` are process globals read during layout; set them at startup (or, for density, whenever the surface moves to an output with a different DPI). Because they are global, ltk's own test suite serialises the tests that touch them.
|
Both `density()` and `widget_scaling()` are process globals read during layout; set them at startup (or, for density, whenever the surface moves to an output with a different DPI). Because they are global, ltk's own test suite serialises the tests that touch them. Density is also overridable **per canvas**: `Canvas::set_density` pins a canvas (and every sub-canvas derived from it) to its own factor, and all canvas-routed resolution — `geom_px` / `font_px` for stock-widget design pixels, `Canvas::resolve_geom` / `resolve_font` for explicit `Length` values — uses the local density when one is pinned and the process global otherwise. This is the hook for a surface sitting on an output whose DPI differs from the one the global was derived from (an overlay on a second monitor, an embedder with several `UiSurface`s).
|
||||||
|
|
||||||
`Length` adapts *sizes* to the orientation; to adapt the *structure* of a layout (a row of panels in landscape, the same panels stacked in portrait), branch the view on `ltk::orientation()`. The runtime records the main surface's physical dimensions on every configure (also readable as `ltk::viewport_size()`) and rebuilds the view after each resize, so a `match ltk::orientation() { Landscape => row()…, Portrait => column()… }` follows the window live. The portrait/landscape rule matches `Length::orient` (a square surface counts as portrait). `examples/clip_path.rs` shows the pattern.
|
`Length` adapts *sizes* to the orientation; to adapt the *structure* of a layout (a row of panels in landscape, the same panels stacked in portrait), branch the view on `ltk::orientation()`. The runtime records the main surface's physical dimensions on every configure (also readable as `ltk::viewport_size()`) and rebuilds the view after each resize, so a `match ltk::orientation() { Landscape => row()…, Portrait => column()… }` follows the window live. The portrait/landscape rule matches `Length::orient` (a square surface counts as portrait). `examples/clip_path.rs` shows the pattern.
|
||||||
|
|
||||||
@@ -322,3 +322,5 @@ Downstream consumers shipping into regulated environments (EN 301 549, WCAG 2.1
|
|||||||
**xdg-activation-v1 — wired in.** Both directions work: a token found in `$XDG_ACTIVATION_TOKEN` at startup is used to activate the app's own window once it maps (so an external launcher can raise an ltk window with focus), and an app that spawns children requests fresh tokens through `App::take_activation_requests` and receives them via `App::on_activation_token` to place in the child's environment.
|
**xdg-activation-v1 — wired in.** Both directions work: a token found in `$XDG_ACTIVATION_TOKEN` at startup is used to activate the app's own window once it maps (so an external launcher can raise an ltk window with focus), and an app that spawns children requests fresh tokens through `App::take_activation_requests` and receives them via `App::on_activation_token` to place in the child's environment.
|
||||||
|
|
||||||
**Fractional scale — deferred.** `wp_fractional_scale_v1` (so 125 % / 150 % outputs render natively instead of via compositor downscale) remains tracked as upcoming protocol work.
|
**Fractional scale — deferred.** `wp_fractional_scale_v1` (so 125 % / 150 % outputs render natively instead of via compositor downscale) remains tracked as upcoming protocol work.
|
||||||
|
|
||||||
|
**Software/GLES parity gaps — see [`docs/backends.md`](./backends.md).** The software backend renders gradients as a flat fill from the first stop, skips outer and inset shadows and backdrop blur, and hard-cuts the bottom-edge fade; `oklab` gradient interpolation falls back to linear-light on both backends. The capability matrix is the canonical per-feature table and must be updated in the same patch that closes any of these gaps.
|
||||||
|
|||||||
33
docs/backends.md
Normal file
33
docs/backends.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Backend capability matrix
|
||||||
|
|
||||||
|
ltk renders through one of two interchangeable backends behind the same `Canvas` API: **software** (CPU rasterisation with tiny-skia + fontdue into a `wl_shm` buffer) and **GLES** (GPU rasterisation via EGL + OpenGL ES 2/3). `ltk::run()` selects GLES when an EGL context can be created and falls back to software; an embedder driving `core::UiSurface` can force either. `Canvas::is_software()` lets a caller branch on the active backend at draw time.
|
||||||
|
|
||||||
|
The two backends are kept at visual parity for the common primitives — solid fills, strokes, lines, text, images, vector paths. This page is the canonical statement of where they differ, so the information does not have to be reassembled from per-method rustdoc. When a difference listed here is closed, update this table in the same patch.
|
||||||
|
|
||||||
|
## Matrix
|
||||||
|
|
||||||
|
| Capability | Software | GLES | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Solid fills, strokes, lines (`fill_rect`, `stroke_rect`, `draw_line`) | Yes | Yes | Visual parity. |
|
||||||
|
| Text (`draw_text`, `draw_text_with_font`) | Yes | Yes | Both snap pen positions to integer pixels; glyph caches differ (per-canvas hashmap with eviction vs. shared atlas texture) but output is at parity. |
|
||||||
|
| Images (`draw_image_data`) | Yes | Yes | Both snap destinations to integer pixels. GLES caches uploaded textures content-keyed, bounded to 32 MiB with LRU eviction; software draws directly from the buffer. |
|
||||||
|
| External GL texture (`draw_external_texture`, `External::texture`) | No-op | Yes | Software has no GL state to sample from. Use `External::cpu` for a backend-independent immediate-mode draw hook. |
|
||||||
|
| Vector paths (`fill_path`, `stroke_path`) | Yes | Yes | Both rasterise through the same tiny-skia code, so shapes are at parity. GLES uploads the rasterised path as a texture per draw — costly for large animated vectors. |
|
||||||
|
| Rect clipping (`set_clip_rects`) | Exact multi-rect mask | Bounding-box scissor | On GLES several dirty rects clip to their union; software masks each rect exactly. Branch on `is_software()` when exactness matters. |
|
||||||
|
| Path clipping (`set_clip_path`) | Yes (coverage mask) | Yes (offscreen layer + anti-aliased coverage composite) | Visual parity; different mechanisms. |
|
||||||
|
| Gradients (`fill_paint_rect` with `Linear` / `Radial`) | Flat fill from the first stop | Dedicated shaders | Software gradient rendering is a known gap; tiny-skia could render them natively but is not wired up yet. |
|
||||||
|
| Gradient interpolation spaces | `srgb`, `linear-rgb` | `srgb`, `linear-rgb` | `oklab` is declared in the schema but currently falls back to linear-light **on both backends** (shared LUT builder). |
|
||||||
|
| Outer drop shadows (`fill_shadow_outer`) | No-op | Analytic soft-shadow shader | Software shadow rendering is a known gap. |
|
||||||
|
| Inset shadows (`fill_shadow_inset`) | No-op | Yes, with `normal` / `plus-lighter` / `multiply` / `screen` / `overlay` blends | `overlay` routes through an FBO snapshot on GLES. |
|
||||||
|
| Backdrop blur (Glass surfaces with a `backdrop` block) | Not rendered | Yes (aux-FBO Gaussian pipeline) | Themes ship `-flat` surface variants without `backdrop`; nothing selects them per backend automatically — pass one explicitly when targeting software. |
|
||||||
|
| Bottom-edge fade (`blit_fade_bottom`, `viewport.fade_bottom`) | Hard edge | Feathered | Software ignores `fade_bottom_px`. |
|
||||||
|
| Cross-backend blit (`blit` between a software and a GLES canvas) | — | — | Unimplemented (panics). Never occurs in practice: sub-canvases always share their parent's backend. |
|
||||||
|
| Pixel readback (`read_rgba_pixels`) | Yes (un-premultiplies) | Yes | Both return tightly packed straight-alpha RGBA8. |
|
||||||
|
| Presentation | `write_to_wayland_buf` into `wl_shm` | `eglSwapBuffers` (with damage when available) | `write_to_wayland_buf` is a no-op on GLES; `present` is the GLES path. |
|
||||||
|
| Animation frame-rate cap | ~30 Hz default (overridable via `App::cap_software_animation`) | Uncapped (compositor frame callbacks) | The cap protects the CPU path from sustained full-rate rasterisation. |
|
||||||
|
|
||||||
|
## Practical guidance
|
||||||
|
|
||||||
|
For a theme or app that must look identical on both backends, stay within the parity set: solid fills, strokes, text, images, paths, path clips. The visible divergences are gradients, shadows, backdrop blur and the bottom fade — all degrade gracefully on software (flat fill, no shadow, no blur, hard edge) rather than failing, so a software rendering of a GLES-designed theme is flatter but functional.
|
||||||
|
|
||||||
|
The pixel-level test suite runs on the software backend; the GLES paths that need a live GL context (layer composites, shaders) are exercised by the examples. There are currently no golden tests comparing the two backends' output — parity for the common primitives is maintained by review and by the shared rasterisation code where it exists (paths).
|
||||||
@@ -446,6 +446,7 @@ then the cookbook, then architecture:
|
|||||||
theming, animation and performance
|
theming, animation and performance
|
||||||
- [`docs/theming.md`](./theming.md) — JSON theme schema, slot conventions,
|
- [`docs/theming.md`](./theming.md) — JSON theme schema, slot conventions,
|
||||||
runtime APIs
|
runtime APIs
|
||||||
|
- [`docs/backends.md`](./backends.md) — software/GLES capability matrix
|
||||||
- [`examples/showcase.rs`](../examples/showcase.rs) — smallest visual tour
|
- [`examples/showcase.rs`](../examples/showcase.rs) — smallest visual tour
|
||||||
- [`examples/widgets.rs`](../examples/widgets.rs) — broader widget coverage
|
- [`examples/widgets.rs`](../examples/widgets.rs) — broader widget coverage
|
||||||
- [`examples/mini_shell.rs`](../examples/mini_shell.rs) — overlays and shell
|
- [`examples/mini_shell.rs`](../examples/mini_shell.rs) — overlays and shell
|
||||||
|
|||||||
@@ -212,12 +212,10 @@ pub( crate ) fn layout_and_draw<Msg: Clone>(
|
|||||||
{
|
{
|
||||||
canvas.stroke_rect( rect, color, width, c.corners );
|
canvas.stroke_rect( rect, color, width, c.corners );
|
||||||
}
|
}
|
||||||
let vp = canvas.viewport_layout();
|
let pad_l = canvas.resolve_geom( c.pad_left );
|
||||||
let em = crate::types::Length::EM_BASE_DEFAULT;
|
let pad_r = canvas.resolve_geom( c.pad_right );
|
||||||
let pad_l = c.pad_left.resolve( vp, em );
|
let pad_t = canvas.resolve_geom( c.pad_top );
|
||||||
let pad_r = c.pad_right.resolve( vp, em );
|
let pad_b = canvas.resolve_geom( c.pad_bottom );
|
||||||
let pad_t = c.pad_top.resolve( vp, em );
|
|
||||||
let pad_b = c.pad_bottom.resolve( vp, em );
|
|
||||||
let inner = crate::types::Rect
|
let inner = crate::types::Rect
|
||||||
{
|
{
|
||||||
x: rect.x + pad_l,
|
x: rect.x + pad_l,
|
||||||
|
|||||||
@@ -15,6 +15,14 @@
|
|||||||
//! serve the stale texture for the new content. Content-keying makes
|
//! serve the stale texture for the new content. Content-keying makes
|
||||||
//! that impossible: identical bytes → identical key, regardless of
|
//! that impossible: identical bytes → identical key, regardless of
|
||||||
//! where they live in memory.
|
//! where they live in memory.
|
||||||
|
//!
|
||||||
|
//! The cache is bounded to [`IMAGE_CACHE_MAX_BYTES`] of estimated GPU
|
||||||
|
//! memory (RGBA8: `w × h × 4` per texture) with least-recently-drawn
|
||||||
|
//! eviction, so a stream of distinct buffers (a photo carousel, video
|
||||||
|
//! thumbnails) recycles textures instead of growing GPU memory for the
|
||||||
|
//! canvas' lifetime. The most recent entry is never evicted, so a
|
||||||
|
//! single texture larger than the whole budget still draws — the cache
|
||||||
|
//! then holds that one entry until something replaces it.
|
||||||
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::hash::{ Hash, Hasher };
|
use std::hash::{ Hash, Hasher };
|
||||||
@@ -26,6 +34,12 @@ use crate::types::Rect;
|
|||||||
use super::helpers::{ ortho_rect, upload_rgba_texture };
|
use super::helpers::{ ortho_rect, upload_rgba_texture };
|
||||||
use super::GlesCanvas;
|
use super::GlesCanvas;
|
||||||
|
|
||||||
|
/// Byte budget for the image texture cache. 32 MiB holds a phone-sized
|
||||||
|
/// wallpaper (~10 MB at 1080×2400) plus a working set of icons and
|
||||||
|
/// thumbnails; sized for mobile GPUs where this memory competes with
|
||||||
|
/// the compositor.
|
||||||
|
pub const IMAGE_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
/// Compute a 64-bit fingerprint of an RGBA buffer for the texture
|
/// Compute a 64-bit fingerprint of an RGBA buffer for the texture
|
||||||
/// cache. Hashes the full byte slice for small buffers (icons,
|
/// cache. Hashes the full byte slice for small buffers (icons,
|
||||||
/// thumbnails — below 16 KB ≈ 64×64 RGBA), and falls back to a
|
/// thumbnails — below 16 KB ≈ 64×64 RGBA), and falls back to a
|
||||||
@@ -85,6 +99,13 @@ impl GlesCanvas
|
|||||||
{
|
{
|
||||||
let tex = upload_rgba_texture( &self.gl, self.version, rgba_data, img_w as i32, img_h as i32 );
|
let tex = upload_rgba_texture( &self.gl, self.version, rgba_data, img_w as i32, img_h as i32 );
|
||||||
self.image_cache.insert( cache_key, ( tex, img_w, img_h ) );
|
self.image_cache.insert( cache_key, ( tex, img_w, img_h ) );
|
||||||
|
self.image_cache_bytes += ( img_w as usize ) * ( img_h as usize ) * 4;
|
||||||
|
self.image_cache_lru.push_back( cache_key );
|
||||||
|
self.evict_image_cache_overflow();
|
||||||
|
} else if let Some( pos ) = self.image_cache_lru.iter().position( |k| *k == cache_key )
|
||||||
|
{
|
||||||
|
self.image_cache_lru.remove( pos );
|
||||||
|
self.image_cache_lru.push_back( cache_key );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snap to integer pixels. With GL_LINEAR sampling, a
|
// Snap to integer pixels. With GL_LINEAR sampling, a
|
||||||
@@ -107,9 +128,9 @@ impl GlesCanvas
|
|||||||
let mvp = ortho_rect( self.width, self.height, dest );
|
let mvp = ortho_rect( self.width, self.height, dest );
|
||||||
let alpha = opacity * self.global_alpha;
|
let alpha = opacity * self.global_alpha;
|
||||||
// SAFETY: see `primitives.rs` module doc. `*tex` is owned by
|
// SAFETY: see `primitives.rs` module doc. `*tex` is owned by
|
||||||
// `self.image_cache` so it outlives the call. The image cache
|
// `self.image_cache` so it outlives the call. Eviction ran
|
||||||
// stays valid as long as `&mut self` is held — no eviction
|
// before this borrow and never removes the most-recent key,
|
||||||
// path runs concurrently with the draw.
|
// which is `cache_key`.
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
self.gl.use_program( Some( self.tex_program ) );
|
self.gl.use_program( Some( self.tex_program ) );
|
||||||
@@ -126,6 +147,27 @@ impl GlesCanvas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete least-recently-drawn cached textures until the estimated
|
||||||
|
/// byte total fits [`IMAGE_CACHE_MAX_BYTES`]. The back of the LRU —
|
||||||
|
/// the entry the current draw is about to use — is never removed.
|
||||||
|
fn evict_image_cache_overflow( &mut self )
|
||||||
|
{
|
||||||
|
while self.image_cache_bytes > IMAGE_CACHE_MAX_BYTES && self.image_cache_lru.len() > 1
|
||||||
|
{
|
||||||
|
let Some( key ) = self.image_cache_lru.pop_front() else { break };
|
||||||
|
if let Some( ( tex, w, h ) ) = self.image_cache.remove( &key )
|
||||||
|
{
|
||||||
|
self.image_cache_bytes = self.image_cache_bytes
|
||||||
|
.saturating_sub( ( w as usize ) * ( h as usize ) * 4 );
|
||||||
|
// SAFETY: `tex` was created through `self.gl` in the insert
|
||||||
|
// path above and just left the map, so it is deleted exactly
|
||||||
|
// once. Deleting a bound texture is defined in GLES (the
|
||||||
|
// binding reverts to 0); no draw is in flight here.
|
||||||
|
unsafe { self.gl.delete_texture( tex ); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw an externally-owned GL texture into `dest`.
|
/// Draw an externally-owned GL texture into `dest`.
|
||||||
///
|
///
|
||||||
/// The caller owns the texture and is responsible for keeping it valid
|
/// The caller owns the texture and is responsible for keeping it valid
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
//! method — see the module's own doc for when to use the guards
|
//! method — see the module's own doc for when to use the guards
|
||||||
//! and when not to.
|
//! and when not to.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{ HashMap, VecDeque };
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::Font;
|
use fontdue::Font;
|
||||||
@@ -157,6 +157,11 @@ pub struct GlesCanvas
|
|||||||
/// the same inside offscreen content (scroll viewports, clip layers)
|
/// the same inside offscreen content (scroll viewports, clip layers)
|
||||||
/// as outside it.
|
/// as outside it.
|
||||||
pub( crate ) layout_viewport: Option<( f32, f32 )>,
|
pub( crate ) layout_viewport: Option<( f32, f32 )>,
|
||||||
|
/// Canvas-local pixel density for `Dp` resolution, when the owning
|
||||||
|
/// surface sits on an output whose density differs from the process
|
||||||
|
/// [`crate::density`]. `None` falls back to the global. Inherited by
|
||||||
|
/// sub-canvases.
|
||||||
|
pub( crate ) density: Option<f32>,
|
||||||
pub global_alpha: f32,
|
pub global_alpha: f32,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
@@ -349,8 +354,13 @@ pub struct GlesCanvas
|
|||||||
// frame (the cache would happily serve the stale texture).
|
// frame (the cache would happily serve the stale texture).
|
||||||
// Content-keying tolerates that case at the cost of one
|
// Content-keying tolerates that case at the cost of one
|
||||||
// `DefaultHasher` pass over the bytes per draw call — fast for
|
// `DefaultHasher` pass over the bytes per draw call — fast for
|
||||||
// any reasonable icon size.
|
// any reasonable icon size. Bounded by `image.rs`'s
|
||||||
|
// `IMAGE_CACHE_MAX_BYTES` with LRU eviction.
|
||||||
image_cache: HashMap<(u32, u32, u64), (glow::Texture, u32, u32)>,
|
image_cache: HashMap<(u32, u32, u64), (glow::Texture, u32, u32)>,
|
||||||
|
// LRU order of `image_cache` keys, least-recent at the front.
|
||||||
|
image_cache_lru: VecDeque<(u32, u32, u64)>,
|
||||||
|
// Estimated GPU bytes held by `image_cache` (RGBA8: w × h × 4).
|
||||||
|
image_cache_bytes: usize,
|
||||||
|
|
||||||
// Gradient LUT cache: FNV-ish hash of the 512×RGBA8 LUT bytes → texture.
|
// Gradient LUT cache: FNV-ish hash of the 512×RGBA8 LUT bytes → texture.
|
||||||
// Gradients are theme-derived and constant across frames; caching avoids
|
// Gradients are theme-derived and constant across frames; caching avoids
|
||||||
|
|||||||
@@ -337,6 +337,7 @@ impl GlesCanvas
|
|||||||
font_registry: None,
|
font_registry: None,
|
||||||
dpi_scale: 1.0,
|
dpi_scale: 1.0,
|
||||||
layout_viewport: None,
|
layout_viewport: None,
|
||||||
|
density: None,
|
||||||
global_alpha: 1.0,
|
global_alpha: 1.0,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -459,6 +460,8 @@ impl GlesCanvas
|
|||||||
atlas_row_height: 0,
|
atlas_row_height: 0,
|
||||||
glyph_cache: HashMap::new(),
|
glyph_cache: HashMap::new(),
|
||||||
image_cache: HashMap::new(),
|
image_cache: HashMap::new(),
|
||||||
|
image_cache_lru: std::collections::VecDeque::new(),
|
||||||
|
image_cache_bytes: 0,
|
||||||
gradient_lut_cache: HashMap::new(),
|
gradient_lut_cache: HashMap::new(),
|
||||||
clip_scissor: None,
|
clip_scissor: None,
|
||||||
clip_layer: None,
|
clip_layer: None,
|
||||||
@@ -550,6 +553,7 @@ impl GlesCanvas
|
|||||||
dpi_scale: self.dpi_scale,
|
dpi_scale: self.dpi_scale,
|
||||||
layout_viewport: Some( self.layout_viewport.unwrap_or(
|
layout_viewport: Some( self.layout_viewport.unwrap_or(
|
||||||
( self.width as f32, self.height as f32 ) ) ),
|
( self.width as f32, self.height as f32 ) ) ),
|
||||||
|
density: self.density,
|
||||||
global_alpha: self.global_alpha,
|
global_alpha: self.global_alpha,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -672,6 +676,8 @@ impl GlesCanvas
|
|||||||
atlas_row_height: 0,
|
atlas_row_height: 0,
|
||||||
glyph_cache: HashMap::new(),
|
glyph_cache: HashMap::new(),
|
||||||
image_cache: HashMap::new(),
|
image_cache: HashMap::new(),
|
||||||
|
image_cache_lru: std::collections::VecDeque::new(),
|
||||||
|
image_cache_bytes: 0,
|
||||||
gradient_lut_cache: HashMap::new(),
|
gradient_lut_cache: HashMap::new(),
|
||||||
clip_scissor: None,
|
clip_scissor: None,
|
||||||
clip_layer: None,
|
clip_layer: None,
|
||||||
|
|||||||
@@ -122,19 +122,19 @@ impl<Msg: Clone> Column<Msg>
|
|||||||
#[ inline ]
|
#[ inline ]
|
||||||
fn resolved_spacing( &self, canvas: &Canvas ) -> f32
|
fn resolved_spacing( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.spacing.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT )
|
canvas.resolve_geom( self.spacing )
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ inline ]
|
#[ inline ]
|
||||||
fn resolved_padding( &self, canvas: &Canvas ) -> f32
|
fn resolved_padding( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.padding.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT )
|
canvas.resolve_geom( self.padding )
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ inline ]
|
#[ inline ]
|
||||||
fn resolved_max_width( &self, canvas: &Canvas ) -> Option<f32>
|
fn resolved_max_width( &self, canvas: &Canvas ) -> Option<f32>
|
||||||
{
|
{
|
||||||
self.max_width.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
|
self.max_width.map( |l| canvas.resolve_geom( l ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Report the intrinsic content width as preferred width instead of filling
|
/// Report the intrinsic content width as preferred width instead of filling
|
||||||
|
|||||||
@@ -92,13 +92,13 @@ impl<Msg: Clone> Row<Msg>
|
|||||||
#[ inline ]
|
#[ inline ]
|
||||||
fn resolved_spacing( &self, canvas: &Canvas ) -> f32
|
fn resolved_spacing( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.spacing.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT )
|
canvas.resolve_geom( self.spacing )
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ inline ]
|
#[ inline ]
|
||||||
fn resolved_padding( &self, canvas: &Canvas ) -> f32
|
fn resolved_padding( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.padding.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT )
|
canvas.resolve_geom( self.padding )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push the content block to the right edge of the available width.
|
/// Push the content block to the right edge of the available width.
|
||||||
|
|||||||
@@ -115,11 +115,9 @@ impl Spacer
|
|||||||
/// weighted by `weight`.
|
/// weighted by `weight`.
|
||||||
pub fn preferred_size( &self, canvas: &Canvas ) -> ( f32, f32 )
|
pub fn preferred_size( &self, canvas: &Canvas ) -> ( f32, f32 )
|
||||||
{
|
{
|
||||||
let vp = canvas.viewport_layout();
|
|
||||||
let em = Length::EM_BASE_DEFAULT;
|
|
||||||
(
|
(
|
||||||
self.fixed_width .map( |l| l.resolve( vp, em ) ).unwrap_or( 0.0 ),
|
self.fixed_width .map( |l| canvas.resolve_geom( l ) ).unwrap_or( 0.0 ),
|
||||||
self.fixed_height.map( |l| l.resolve( vp, em ) ).unwrap_or( 0.0 ),
|
self.fixed_height.map( |l| canvas.resolve_geom( l ) ).unwrap_or( 0.0 ),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,12 +126,12 @@ impl Spacer
|
|||||||
/// layout only needs the main-axis size for one orientation.
|
/// layout only needs the main-axis size for one orientation.
|
||||||
pub fn resolved_height( &self, canvas: &Canvas ) -> Option<f32>
|
pub fn resolved_height( &self, canvas: &Canvas ) -> Option<f32>
|
||||||
{
|
{
|
||||||
self.fixed_height.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
|
self.fixed_height.map( |l| canvas.resolve_geom( l ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolved_width( &self, canvas: &Canvas ) -> Option<f32>
|
pub fn resolved_width( &self, canvas: &Canvas ) -> Option<f32>
|
||||||
{
|
{
|
||||||
self.fixed_width.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
|
self.fixed_width.map( |l| canvas.resolve_geom( l ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No-op — spacers are invisible.
|
/// No-op — spacers are invisible.
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ impl<Msg: Clone> WrapGrid<Msg>
|
|||||||
{
|
{
|
||||||
Some( m ) =>
|
Some( m ) =>
|
||||||
{
|
{
|
||||||
let m = m.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ).max( 1.0 );
|
let m = canvas.resolve_geom( m ).max( 1.0 );
|
||||||
let cols = ( ( ( inner_w + sx ) / ( m + sx ) ).floor() as usize ).max( 1 );
|
let cols = ( ( ( inner_w + sx ) / ( m + sx ) ).floor() as usize ).max( 1 );
|
||||||
match self.max_columns
|
match self.max_columns
|
||||||
{
|
{
|
||||||
@@ -130,12 +130,10 @@ impl<Msg: Clone> WrapGrid<Msg>
|
|||||||
|
|
||||||
fn resolved( &self, canvas: &Canvas ) -> ( f32, f32, f32 )
|
fn resolved( &self, canvas: &Canvas ) -> ( f32, f32, f32 )
|
||||||
{
|
{
|
||||||
let vp = canvas.viewport_layout();
|
|
||||||
let em = Length::EM_BASE_DEFAULT;
|
|
||||||
(
|
(
|
||||||
self.spacing_x.resolve( vp, em ),
|
canvas.resolve_geom( self.spacing_x ),
|
||||||
self.spacing_y.resolve( vp, em ),
|
canvas.resolve_geom( self.spacing_y ),
|
||||||
self.padding.resolve( vp, em ),
|
canvas.resolve_geom( self.padding ),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/lib.rs
10
src/lib.rs
@@ -115,8 +115,9 @@
|
|||||||
//! - [`Length`] — a size/distance that may be absolute pixels
|
//! - [`Length`] — a size/distance that may be absolute pixels
|
||||||
//! ([`LengthBase::Px`]), relative to the surface viewport
|
//! ([`LengthBase::Px`]), relative to the surface viewport
|
||||||
//! ([`LengthBase::Vw`] / [`LengthBase::Vh`] / [`LengthBase::Vmin`] /
|
//! ([`LengthBase::Vw`] / [`LengthBase::Vh`] / [`LengthBase::Vmin`] /
|
||||||
//! [`LengthBase::Vmax`] / [`LengthBase::Orient`]) or to the root font
|
//! [`LengthBase::Vmax`] / [`LengthBase::Orient`]), to the root font
|
||||||
//! size ([`LengthBase::Em`]). Every setter that takes a size, padding,
|
//! size ([`LengthBase::Em`]) or to the pixel density
|
||||||
|
//! ([`LengthBase::Dp`]). Every setter that takes a size, padding,
|
||||||
//! spacing or font height now accepts `impl Into<Length>`, so legacy
|
//! spacing or font height now accepts `impl Into<Length>`, so legacy
|
||||||
//! `.size( 24.0 )` keeps working while new code can write
|
//! `.size( 24.0 )` keeps working while new code can write
|
||||||
//! `.size( Length::vmin( 4.0 ).clamp( 16.0, 32.0 ) )` for a typeface
|
//! `.size( Length::vmin( 4.0 ).clamp( 16.0, 32.0 ) )` for a typeface
|
||||||
@@ -188,8 +189,9 @@
|
|||||||
//! millimetres — and legibility is a function of physical (angular) size,
|
//! millimetres — and legibility is a function of physical (angular) size,
|
||||||
//! not of what fraction of the screen a glyph fills. When a size must stay
|
//! not of what fraction of the screen a glyph fills. When a size must stay
|
||||||
//! a **constant physical size** across very different displays, use the
|
//! a **constant physical size** across very different displays, use the
|
||||||
//! other mode: [`Length::dp`] (a density-independent pixel — `n ×`
|
//! other mode: [`Length::dp`] (a density-independent pixel — `n ×` the
|
||||||
//! [`density`], the mainstream HiDPI unit), or [`LengthBase::Em`] for text
|
//! pixel density, applied when the value is resolved — the mainstream
|
||||||
|
//! HiDPI unit), or [`LengthBase::Em`] for text
|
||||||
//! relative to the root font size. The pre-calibrated
|
//! relative to the root font size. The pre-calibrated
|
||||||
//! [`theme::typography`] scale
|
//! [`theme::typography`] scale
|
||||||
//! ([`theme::typography::h0`]…[`theme::typography::body_xs`]) is built on
|
//! ([`theme::typography::h0`]…[`theme::typography::body_xs`]) is built on
|
||||||
|
|||||||
@@ -143,6 +143,11 @@ pub struct SoftwareCanvas
|
|||||||
/// the same inside offscreen content (scroll viewports, clip layers)
|
/// the same inside offscreen content (scroll viewports, clip layers)
|
||||||
/// as outside it.
|
/// as outside it.
|
||||||
pub( crate ) layout_viewport: Option<( f32, f32 )>,
|
pub( crate ) layout_viewport: Option<( f32, f32 )>,
|
||||||
|
/// Canvas-local pixel density for `Dp` resolution, when the owning
|
||||||
|
/// surface sits on an output whose density differs from the process
|
||||||
|
/// [`crate::density`]. `None` falls back to the global. Inherited by
|
||||||
|
/// sub-canvases.
|
||||||
|
pub( crate ) density: Option<f32>,
|
||||||
/// Global alpha multiplier for all drawing operations (0.0 =
|
/// Global alpha multiplier for all drawing operations (0.0 =
|
||||||
/// transparent, 1.0 = opaque).
|
/// transparent, 1.0 = opaque).
|
||||||
pub global_alpha: f32,
|
pub global_alpha: f32,
|
||||||
@@ -264,6 +269,51 @@ impl Canvas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pixel density used to resolve [`crate::LengthBase::Dp`] values on
|
||||||
|
/// this canvas: the canvas-local density when one was pinned with
|
||||||
|
/// [`Self::set_density`], the process [`crate::density`] otherwise.
|
||||||
|
pub fn density( &self ) -> f32
|
||||||
|
{
|
||||||
|
let local = match self
|
||||||
|
{
|
||||||
|
Canvas::Software( c ) => c.density,
|
||||||
|
Canvas::Gles( c ) => c.density,
|
||||||
|
};
|
||||||
|
local.unwrap_or_else( crate::types::density )
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pin this canvas — and every sub-canvas later derived from it — to
|
||||||
|
/// a pixel density, overriding the process [`crate::density`] for
|
||||||
|
/// `Dp` resolution. For a surface sitting on an output whose DPI
|
||||||
|
/// differs from the one the process global was derived from.
|
||||||
|
pub fn set_density( &mut self, d: f32 )
|
||||||
|
{
|
||||||
|
let d = d.max( 0.0 );
|
||||||
|
match self
|
||||||
|
{
|
||||||
|
Canvas::Software( c ) => c.density = Some( d ),
|
||||||
|
Canvas::Gles( c ) => c.density = Some( d ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve an explicit [`Length`] in **geometry** space: against
|
||||||
|
/// [`Self::viewport_layout`], with this canvas' [`Self::density`].
|
||||||
|
/// Widgets resolve caller-supplied geometry lengths through this so
|
||||||
|
/// a `Length::dp` override follows the canvas the widget draws on.
|
||||||
|
pub fn resolve_geom( &self, l: Length ) -> f32
|
||||||
|
{
|
||||||
|
l.resolve_with_density( self.viewport_layout(), Length::EM_BASE_DEFAULT, self.density() )
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve an explicit [`Length`] in **font** space: against
|
||||||
|
/// [`Self::viewport_logical`], with this canvas' [`Self::density`].
|
||||||
|
/// Counterpart of [`Self::resolve_geom`] for font sizes, which are
|
||||||
|
/// handed to the raster path pre-`dpi_scale`.
|
||||||
|
pub fn resolve_font( &self, l: Length ) -> f32
|
||||||
|
{
|
||||||
|
l.resolve_with_density( self.viewport_logical(), Length::EM_BASE_DEFAULT, self.density() )
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a stock-widget **geometry** design pixel (height, padding,
|
/// Resolve a stock-widget **geometry** design pixel (height, padding,
|
||||||
/// box size, gap…) through the process-wide [`crate::WidgetScaling`]
|
/// box size, gap…) through the process-wide [`crate::WidgetScaling`]
|
||||||
/// mode, into a concrete physical-pixel value for the layout tree.
|
/// mode, into a concrete physical-pixel value for the layout tree.
|
||||||
@@ -275,7 +325,7 @@ impl Canvas
|
|||||||
/// [`Self::viewport_layout`].
|
/// [`Self::viewport_layout`].
|
||||||
pub fn geom_px( &self, design_px: f32 ) -> f32
|
pub fn geom_px( &self, design_px: f32 ) -> f32
|
||||||
{
|
{
|
||||||
Length::widget( design_px ).resolve( self.viewport_layout(), Length::EM_BASE_DEFAULT )
|
self.resolve_geom( Length::widget( design_px ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a stock-widget **font** design pixel through the process-wide
|
/// Resolve a stock-widget **font** design pixel through the process-wide
|
||||||
@@ -292,13 +342,13 @@ impl Canvas
|
|||||||
{
|
{
|
||||||
WidgetScaling::Fluid =>
|
WidgetScaling::Fluid =>
|
||||||
{
|
{
|
||||||
Length::fluid( design_px ).resolve( self.viewport_logical(), Length::EM_BASE_DEFAULT )
|
self.resolve_font( Length::fluid( design_px ) )
|
||||||
}
|
}
|
||||||
WidgetScaling::Physical =>
|
WidgetScaling::Physical =>
|
||||||
{
|
{
|
||||||
let scale = self.dpi_scale();
|
let scale = self.dpi_scale();
|
||||||
let scale = if scale > 0.0 { scale } else { 1.0 };
|
let scale = if scale > 0.0 { scale } else { 1.0 };
|
||||||
design_px * crate::types::density() / scale
|
design_px * self.density() / scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -890,6 +940,40 @@ mod viewport_tests
|
|||||||
set_widget_scaling( WidgetScaling::Fluid );
|
set_widget_scaling( WidgetScaling::Fluid );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn canvas_density_overrides_process_density()
|
||||||
|
{
|
||||||
|
use crate::types::{ set_widget_scaling, set_density, WidgetScaling, Length };
|
||||||
|
let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
|
||||||
|
|
||||||
|
set_widget_scaling( WidgetScaling::Physical );
|
||||||
|
set_density( 1.0 );
|
||||||
|
let mut c = Canvas::new( 412, 900 );
|
||||||
|
|
||||||
|
// No local density → the process global applies.
|
||||||
|
assert_eq!( c.geom_px( 48.0 ), 48.0 );
|
||||||
|
|
||||||
|
// A pinned canvas density wins over the global, for stock-widget
|
||||||
|
// design pixels and explicit dp lengths alike.
|
||||||
|
c.set_density( 2.0 );
|
||||||
|
assert_eq!( c.density(), 2.0 );
|
||||||
|
assert_eq!( c.geom_px( 48.0 ), 96.0 );
|
||||||
|
assert_eq!( c.resolve_geom( Length::dp( 10.0 ) ), 20.0 );
|
||||||
|
|
||||||
|
set_widget_scaling( WidgetScaling::Fluid );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn sub_canvas_inherits_density()
|
||||||
|
{
|
||||||
|
let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
|
||||||
|
|
||||||
|
let mut c = Canvas::new( 412, 900 );
|
||||||
|
c.set_density( 3.0 );
|
||||||
|
let sub = c.sub_canvas( 100, 100 );
|
||||||
|
assert_eq!( sub.density(), 3.0 );
|
||||||
|
}
|
||||||
|
|
||||||
#[ test ]
|
#[ test ]
|
||||||
fn font_px_is_constant_physical_in_physical_mode()
|
fn font_px_is_constant_physical_in_physical_mode()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ impl SoftwareCanvas
|
|||||||
font_registry: None,
|
font_registry: None,
|
||||||
dpi_scale: 1.0,
|
dpi_scale: 1.0,
|
||||||
layout_viewport: None,
|
layout_viewport: None,
|
||||||
|
density: None,
|
||||||
global_alpha: 1.0,
|
global_alpha: 1.0,
|
||||||
glyph_cache: HashMap::new(),
|
glyph_cache: HashMap::new(),
|
||||||
clip_mask: None,
|
clip_mask: None,
|
||||||
@@ -57,6 +58,7 @@ impl SoftwareCanvas
|
|||||||
dpi_scale: self.dpi_scale,
|
dpi_scale: self.dpi_scale,
|
||||||
layout_viewport: Some( self.layout_viewport.unwrap_or(
|
layout_viewport: Some( self.layout_viewport.unwrap_or(
|
||||||
( self.pixmap.width() as f32, self.pixmap.height() as f32 ) ) ),
|
( self.pixmap.width() as f32, self.pixmap.height() as f32 ) ) ),
|
||||||
|
density: self.density,
|
||||||
global_alpha: self.global_alpha,
|
global_alpha: self.global_alpha,
|
||||||
glyph_cache: HashMap::new(),
|
glyph_cache: HashMap::new(),
|
||||||
clip_mask: None,
|
clip_mask: None,
|
||||||
|
|||||||
77
src/types.rs
77
src/types.rs
@@ -480,11 +480,15 @@ pub enum LengthBase
|
|||||||
/// Multiple of the root font size (typographic hierarchy: a heading
|
/// Multiple of the root font size (typographic hierarchy: a heading
|
||||||
/// of `Em(2.0)` is twice the body size, regardless of viewport).
|
/// of `Em(2.0)` is twice the body size, regardless of viewport).
|
||||||
Em( f32 ),
|
Em( f32 ),
|
||||||
|
/// Density-independent pixel: the design value times the pixel
|
||||||
|
/// density in effect **when the length is resolved** (the canvas'
|
||||||
|
/// own density, or the process [`density`]). See [`Length::dp`].
|
||||||
|
Dp( f32 ),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LengthBase
|
impl LengthBase
|
||||||
{
|
{
|
||||||
fn resolve( &self, viewport: ( f32, f32 ), em_base: f32 ) -> f32
|
fn resolve( &self, viewport: ( f32, f32 ), em_base: f32, density: f32 ) -> f32
|
||||||
{
|
{
|
||||||
let ( vw, vh ) = viewport;
|
let ( vw, vh ) = viewport;
|
||||||
match self
|
match self
|
||||||
@@ -504,6 +508,7 @@ impl LengthBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LengthBase::Em( mul ) => em_base * mul,
|
LengthBase::Em( mul ) => em_base * mul,
|
||||||
|
LengthBase::Dp( v ) => *v * density,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -519,7 +524,8 @@ impl LengthBase
|
|||||||
/// Resolution requires a viewport — passed in as `(width, height)` in
|
/// Resolution requires a viewport — passed in as `(width, height)` in
|
||||||
/// **logical** pixels — and an `em_base` (the body-text font size that
|
/// **logical** pixels — and an `em_base` (the body-text font size that
|
||||||
/// `Em` is a multiple of). All resolution funnels through
|
/// `Em` is a multiple of). All resolution funnels through
|
||||||
/// [`Length::resolve`], so widgets can stay backend-agnostic.
|
/// [`Length::resolve`] (or [`Length::resolve_with_density`] where a
|
||||||
|
/// canvas-local density applies), so widgets can stay backend-agnostic.
|
||||||
///
|
///
|
||||||
/// Construct directly via the [`LengthBase`] variants
|
/// Construct directly via the [`LengthBase`] variants
|
||||||
/// (`Length::vmin( 18.0 )`, `Length::px( 24.0 )`, …) or implicitly from
|
/// (`Length::vmin( 18.0 )`, `Length::px( 24.0 )`, …) or implicitly from
|
||||||
@@ -584,15 +590,22 @@ impl Length
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// **Density-independent** pixel (the [`WidgetScaling::Physical`] mode).
|
/// **Density-independent** pixel (the [`WidgetScaling::Physical`] mode).
|
||||||
/// `px` is multiplied by the process [`density`] (derived from the
|
/// `px` is multiplied by the pixel density (derived from the output's
|
||||||
/// output's DPI, or set with [`set_density`]) to yield a **constant
|
/// DPI, or set with [`set_density`]) to yield a **constant physical
|
||||||
/// physical size** across displays — the mainstream `dp` of Android /
|
/// size** across displays — the mainstream `dp` of Android / Flutter /
|
||||||
/// Flutter / CSS. Unlike [`Length::fluid`] it does **not** scale with
|
/// CSS. Unlike [`Length::fluid`] it does **not** scale with the
|
||||||
/// the surface size, only with pixel density. Density defaults to
|
/// surface size, only with pixel density. Density defaults to `1.0`,
|
||||||
/// `1.0`, so `dp( n )` == `n` px until a density is set.
|
/// so `dp( n )` == `n` px until a density is set.
|
||||||
pub fn dp( px: f32 ) -> Self
|
///
|
||||||
|
/// The multiplication happens at **resolution time**, not here: the
|
||||||
|
/// value carries its design pixels, and [`Length::resolve`] applies
|
||||||
|
/// the process [`density`] — or the canvas' own density
|
||||||
|
/// ([`crate::Canvas::set_density`]) on canvas-routed resolution — so
|
||||||
|
/// a density change takes effect on the next paint without
|
||||||
|
/// reconstructing the view's lengths.
|
||||||
|
pub const fn dp( px: f32 ) -> Self
|
||||||
{
|
{
|
||||||
Length::px( px * density() )
|
Self::from_base( LengthBase::Dp( px ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a stock-widget design pixel through the process-wide
|
/// Resolve a stock-widget design pixel through the process-wide
|
||||||
@@ -613,9 +626,21 @@ impl Length
|
|||||||
|
|
||||||
/// Resolve to a concrete logical-pixel value given a viewport and an
|
/// Resolve to a concrete logical-pixel value given a viewport and an
|
||||||
/// `em_base` (the root font size that `Em` is a fraction of).
|
/// `em_base` (the root font size that `Em` is a fraction of).
|
||||||
|
/// [`LengthBase::Dp`] values use the process [`density`]; resolution
|
||||||
|
/// paths that know a more local density (a canvas tied to a specific
|
||||||
|
/// output) go through [`Self::resolve_with_density`] instead.
|
||||||
pub fn resolve( &self, viewport: ( f32, f32 ), em_base: f32 ) -> f32
|
pub fn resolve( &self, viewport: ( f32, f32 ), em_base: f32 ) -> f32
|
||||||
{
|
{
|
||||||
let raw = self.base.resolve( viewport, em_base );
|
self.resolve_with_density( viewport, em_base, density() )
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::resolve`] with an explicit pixel density for
|
||||||
|
/// [`LengthBase::Dp`], instead of the process [`density`]. This is
|
||||||
|
/// what [`crate::Canvas`]-routed resolution calls with the canvas'
|
||||||
|
/// own density.
|
||||||
|
pub fn resolve_with_density( &self, viewport: ( f32, f32 ), em_base: f32, density: f32 ) -> f32
|
||||||
|
{
|
||||||
|
let raw = self.base.resolve( viewport, em_base, density );
|
||||||
let lo = self.min_px;
|
let lo = self.min_px;
|
||||||
let hi = self.max_px;
|
let hi = self.max_px;
|
||||||
// If both bounds present, normalise their order so swapped args
|
// If both bounds present, normalise their order so swapped args
|
||||||
@@ -912,9 +937,33 @@ mod length_tests
|
|||||||
assert_eq!( Length::dp( 48.0 ).resolve( ( 3840.0, 2160.0 ), 16.0 ), 48.0 );
|
assert_eq!( Length::dp( 48.0 ).resolve( ( 3840.0, 2160.0 ), 16.0 ), 48.0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialised: this is the only test that mutates the process-wide density
|
#[ test ]
|
||||||
// and widget-scaling globals, so it owns them start-to-finish and restores
|
fn dp_resolves_against_explicit_density()
|
||||||
// the defaults, keeping the other (read-only-default) tests deterministic.
|
{
|
||||||
|
let l = Length::dp( 48.0 );
|
||||||
|
assert_eq!( l.resolve_with_density( ( 412.0, 900.0 ), 16.0, 2.0 ), 96.0 );
|
||||||
|
assert_eq!( l.resolve_with_density( ( 412.0, 900.0 ), 16.0, 1.0 ), 48.0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn dp_is_applied_at_resolution_time_not_construction()
|
||||||
|
{
|
||||||
|
use super::set_density;
|
||||||
|
|
||||||
|
let _g = GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
|
||||||
|
|
||||||
|
// Construct while density is 1.0, resolve after it changes: the
|
||||||
|
// length must follow the new density.
|
||||||
|
let l = Length::dp( 48.0 );
|
||||||
|
set_density( 2.0 );
|
||||||
|
assert_eq!( l.resolve( ( 412.0, 900.0 ), 16.0 ), 96.0 );
|
||||||
|
set_density( 1.0 );
|
||||||
|
assert_eq!( l.resolve( ( 412.0, 900.0 ), 16.0 ), 48.0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialised: mutates the process-wide density and widget-scaling
|
||||||
|
// globals, so it owns them start-to-finish and restores the defaults,
|
||||||
|
// keeping the other (read-only-default) tests deterministic.
|
||||||
#[ test ]
|
#[ test ]
|
||||||
fn density_and_widget_scaling_modes()
|
fn density_and_widget_scaling_modes()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
fn label_font_size( &self, canvas: &Canvas ) -> f32
|
fn label_font_size( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.font_size
|
self.font_size
|
||||||
.map( |l| l.resolve( canvas.viewport_logical(), Length::EM_BASE_DEFAULT ) )
|
.map( |l| canvas.resolve_font( l ) )
|
||||||
.unwrap_or_else( || canvas.font_px( theme::FONT_SIZE ) )
|
.unwrap_or_else( || canvas.font_px( theme::FONT_SIZE ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
fn resolved_height( &self, canvas: &Canvas ) -> f32
|
fn resolved_height( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.height
|
self.height
|
||||||
.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
|
.map( |l| canvas.resolve_geom( l ) )
|
||||||
.unwrap_or_else( || canvas.geom_px( theme::HEIGHT ) )
|
.unwrap_or_else( || canvas.geom_px( theme::HEIGHT ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ impl<Msg: Clone> Button<Msg>
|
|||||||
{
|
{
|
||||||
let w = match self.width
|
let w = match self.width
|
||||||
{
|
{
|
||||||
Some( l ) => l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ).min( max_width ),
|
Some( l ) => canvas.resolve_geom( l ).min( max_width ),
|
||||||
None =>
|
None =>
|
||||||
{
|
{
|
||||||
let text_w = canvas.measure_text( label, self.label_font_size( canvas ) );
|
let text_w = canvas.measure_text( label, self.label_font_size( canvas ) );
|
||||||
|
|||||||
@@ -275,12 +275,10 @@ impl<Msg: Clone> Container<Msg>
|
|||||||
/// Return the preferred `(width, height)` accounting for padding.
|
/// Return the preferred `(width, height)` accounting for padding.
|
||||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
|
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
|
||||||
{
|
{
|
||||||
let vp = canvas.viewport_layout();
|
let pad_l = canvas.resolve_geom( self.pad_left );
|
||||||
let em = Length::EM_BASE_DEFAULT;
|
let pad_r = canvas.resolve_geom( self.pad_right );
|
||||||
let pad_l = self.pad_left.resolve( vp, em );
|
let pad_t = canvas.resolve_geom( self.pad_top );
|
||||||
let pad_r = self.pad_right.resolve( vp, em );
|
let pad_b = canvas.resolve_geom( self.pad_bottom );
|
||||||
let pad_t = self.pad_top.resolve( vp, em );
|
|
||||||
let pad_b = self.pad_bottom.resolve( vp, em );
|
|
||||||
let avail = self.max_width.map( |m| max_width.min( m ) ).unwrap_or( max_width );
|
let avail = self.max_width.map( |m| max_width.min( m ) ).unwrap_or( max_width );
|
||||||
let pad_x = pad_l + pad_r;
|
let pad_x = pad_l + pad_r;
|
||||||
let pad_y = pad_t + pad_b;
|
let pad_y = pad_t + pad_b;
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ impl Image
|
|||||||
if let Some( extent ) = &self.short_side
|
if let Some( extent ) = &self.short_side
|
||||||
{
|
{
|
||||||
let ( vw, vh ) = canvas.viewport_layout();
|
let ( vw, vh ) = canvas.viewport_layout();
|
||||||
let s = extent.resolve( ( vw, vh ), Length::EM_BASE_DEFAULT ).max( 0.0 );
|
let s = canvas.resolve_geom( *extent ).max( 0.0 );
|
||||||
let sw = self.width as f32;
|
let sw = self.width as f32;
|
||||||
let sh = self.height as f32;
|
let sh = self.height as f32;
|
||||||
if sw <= 0.0 || sh <= 0.0 { return ( s, s ); }
|
if sw <= 0.0 || sh <= 0.0 { return ( s, s ); }
|
||||||
@@ -123,10 +123,8 @@ impl Image
|
|||||||
}
|
}
|
||||||
if let Some( ( w, h ) ) = &self.display_size
|
if let Some( ( w, h ) ) = &self.display_size
|
||||||
{
|
{
|
||||||
let vp = canvas.viewport_layout();
|
let rw = canvas.resolve_geom( *w ).max( 0.0 );
|
||||||
let em = Length::EM_BASE_DEFAULT;
|
let rh = canvas.resolve_geom( *h ).max( 0.0 );
|
||||||
let rw = w.resolve( vp, em ).max( 0.0 );
|
|
||||||
let rh = h.resolve( vp, em ).max( 0.0 );
|
|
||||||
return ( rw, rh );
|
return ( rw, rh );
|
||||||
}
|
}
|
||||||
if self.cover
|
if self.cover
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ impl<Msg: Clone> ListItem<Msg>
|
|||||||
|
|
||||||
let label_size = canvas.font_px( theme::LABEL_SIZE );
|
let label_size = canvas.font_px( theme::LABEL_SIZE );
|
||||||
let pad_h = self.pad_h
|
let pad_h = self.pad_h
|
||||||
.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
|
.map( |l| canvas.resolve_geom( l ) )
|
||||||
.unwrap_or_else( || canvas.geom_px( theme::PAD_H ) );
|
.unwrap_or_else( || canvas.geom_px( theme::PAD_H ) );
|
||||||
let has_sub = self.subtitle.is_some();
|
let has_sub = self.subtitle.is_some();
|
||||||
let label_y = if has_sub
|
let label_y = if has_sub
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ impl<Msg: Clone> RichText<Msg>
|
|||||||
#[ inline ]
|
#[ inline ]
|
||||||
fn resolved_size( &self, canvas: &Canvas ) -> f32
|
fn resolved_size( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.size.resolve( canvas.viewport_logical(), Length::EM_BASE_DEFAULT )
|
canvas.resolve_font( self.size )
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_font( &self, canvas: &Canvas ) -> Option<Arc<Font>>
|
fn resolve_font( &self, canvas: &Canvas ) -> Option<Arc<Font>>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ impl Separator
|
|||||||
fn resolved_thickness( &self, canvas: &Canvas ) -> f32
|
fn resolved_thickness( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.thickness
|
self.thickness
|
||||||
.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
|
.map( |l| canvas.resolve_geom( l ) )
|
||||||
.unwrap_or_else( || canvas.geom_px( theme::THICKNESS ) )
|
.unwrap_or_else( || canvas.geom_px( theme::THICKNESS ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ impl Separator
|
|||||||
fn resolved_pad_v( &self, canvas: &Canvas ) -> f32
|
fn resolved_pad_v( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.pad_v
|
self.pad_v
|
||||||
.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
|
.map( |l| canvas.resolve_geom( l ) )
|
||||||
.unwrap_or_else( || canvas.geom_px( theme::PAD_V ) )
|
.unwrap_or_else( || canvas.geom_px( theme::PAD_V ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ impl Text
|
|||||||
#[ inline ]
|
#[ inline ]
|
||||||
fn resolved_size( &self, canvas: &Canvas ) -> f32
|
fn resolved_size( &self, canvas: &Canvas ) -> f32
|
||||||
{
|
{
|
||||||
self.size.resolve( canvas.viewport_logical(), Length::EM_BASE_DEFAULT )
|
canvas.resolve_font( self.size )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint the full string even when it overflows, instead of truncating
|
/// Paint the full string even when it overflows, instead of truncating
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub( crate ) use cursor::{ cursor_visual_down, cursor_visual_end, cursor_visual_
|
|||||||
pub( crate ) fn resolve_font_size( canvas: &Canvas, fs: Option<Length> ) -> f32
|
pub( crate ) fn resolve_font_size( canvas: &Canvas, fs: Option<Length> ) -> f32
|
||||||
{
|
{
|
||||||
fs
|
fs
|
||||||
.map( |l| l.resolve( canvas.viewport_logical(), Length::EM_BASE_DEFAULT ) )
|
.map( |l| canvas.resolve_font( l ) )
|
||||||
.unwrap_or_else( || canvas.font_px( theme::FONT_SIZE ) )
|
.unwrap_or_else( || canvas.font_px( theme::FONT_SIZE ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,10 +444,10 @@ impl<Msg: Clone> TextEdit<Msg>
|
|||||||
( max_width, h )
|
( max_width, h )
|
||||||
} else {
|
} else {
|
||||||
let w = self.fixed_width
|
let w = self.fixed_width
|
||||||
.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ).min( max_width ) )
|
.map( |l| canvas.resolve_geom( l ).min( max_width ) )
|
||||||
.unwrap_or( max_width );
|
.unwrap_or( max_width );
|
||||||
let h = self.height
|
let h = self.height
|
||||||
.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
|
.map( |l| canvas.resolve_geom( l ) )
|
||||||
.unwrap_or_else( || canvas.geom_px( theme::HEIGHT ) );
|
.unwrap_or_else( || canvas.geom_px( theme::HEIGHT ) );
|
||||||
( w, h )
|
( w, h )
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,10 +139,8 @@ impl<Msg: Clone> VSlider<Msg>
|
|||||||
/// the type-level docs on intrinsic sizing.
|
/// the type-level docs on intrinsic sizing.
|
||||||
pub fn preferred_size( &self, _max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
pub fn preferred_size( &self, _max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||||
{
|
{
|
||||||
let vp = canvas.viewport_layout();
|
let w = canvas.resolve_geom( self.width ).max( 2.0 );
|
||||||
let em = Length::EM_BASE_DEFAULT;
|
let h = canvas.resolve_geom( self.height ).max( 2.0 );
|
||||||
let w = self.width.resolve( vp, em ).max( 2.0 );
|
|
||||||
let h = self.height.resolve( vp, em ).max( 2.0 );
|
|
||||||
( w, h )
|
( w, h )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user