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:
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
|
||||
/// of `Em(2.0)` is twice the body size, regardless of viewport).
|
||||
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
|
||||
{
|
||||
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;
|
||||
match self
|
||||
@@ -504,6 +508,7 @@ impl LengthBase
|
||||
}
|
||||
}
|
||||
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
|
||||
/// **logical** pixels — and an `em_base` (the body-text font size that
|
||||
/// `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
|
||||
/// (`Length::vmin( 18.0 )`, `Length::px( 24.0 )`, …) or implicitly from
|
||||
@@ -584,15 +590,22 @@ impl Length
|
||||
}
|
||||
|
||||
/// **Density-independent** pixel (the [`WidgetScaling::Physical`] mode).
|
||||
/// `px` is multiplied by the process [`density`] (derived from the
|
||||
/// output's DPI, or set with [`set_density`]) to yield a **constant
|
||||
/// physical size** across displays — the mainstream `dp` of Android /
|
||||
/// Flutter / CSS. Unlike [`Length::fluid`] it does **not** scale with
|
||||
/// the surface size, only with pixel density. Density defaults to
|
||||
/// `1.0`, so `dp( n )` == `n` px until a density is set.
|
||||
pub fn dp( px: f32 ) -> Self
|
||||
/// `px` is multiplied by the pixel density (derived from the output's
|
||||
/// DPI, or set with [`set_density`]) to yield a **constant physical
|
||||
/// size** across displays — the mainstream `dp` of Android / Flutter /
|
||||
/// CSS. Unlike [`Length::fluid`] it does **not** scale with the
|
||||
/// surface size, only with pixel density. Density defaults to `1.0`,
|
||||
/// so `dp( n )` == `n` px until a density is set.
|
||||
///
|
||||
/// 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
|
||||
@@ -613,9 +626,21 @@ impl Length
|
||||
|
||||
/// Resolve to a concrete logical-pixel value given a viewport and an
|
||||
/// `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
|
||||
{
|
||||
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 hi = self.max_px;
|
||||
// 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 );
|
||||
}
|
||||
|
||||
// Serialised: this is the only test that 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 ]
|
||||
fn dp_resolves_against_explicit_density()
|
||||
{
|
||||
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 ]
|
||||
fn density_and_widget_scaling_modes()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user