render, types, ci: resolution-time dp with per-canvas density, bounded GLES image cache, clippy gate, backend capability matrix
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

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:
2026-08-01 10:17:00 +02:00
parent a7f953ca42
commit 806dee5167
29 changed files with 323 additions and 80 deletions

View File

@@ -279,7 +279,7 @@ impl<Msg: Clone> Button<Msg>
fn label_font_size( &self, canvas: &Canvas ) -> f32
{
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 ) )
}
@@ -289,7 +289,7 @@ impl<Msg: Clone> Button<Msg>
fn resolved_height( &self, canvas: &Canvas ) -> f32
{
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 ) )
}
@@ -348,7 +348,7 @@ impl<Msg: Clone> Button<Msg>
{
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 =>
{
let text_w = canvas.measure_text( label, self.label_font_size( canvas ) );

View File

@@ -275,12 +275,10 @@ impl<Msg: Clone> Container<Msg>
/// Return the preferred `(width, height)` accounting for padding.
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
{
let vp = canvas.viewport_layout();
let em = Length::EM_BASE_DEFAULT;
let pad_l = self.pad_left.resolve( vp, em );
let pad_r = self.pad_right.resolve( vp, em );
let pad_t = self.pad_top.resolve( vp, em );
let pad_b = self.pad_bottom.resolve( vp, em );
let pad_l = canvas.resolve_geom( self.pad_left );
let pad_r = canvas.resolve_geom( self.pad_right );
let pad_t = canvas.resolve_geom( self.pad_top );
let pad_b = canvas.resolve_geom( self.pad_bottom );
let avail = self.max_width.map( |m| max_width.min( m ) ).unwrap_or( max_width );
let pad_x = pad_l + pad_r;
let pad_y = pad_t + pad_b;

View File

@@ -108,7 +108,7 @@ impl Image
if let Some( extent ) = &self.short_side
{
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 sh = self.height as f32;
if sw <= 0.0 || sh <= 0.0 { return ( s, s ); }
@@ -123,10 +123,8 @@ impl Image
}
if let Some( ( w, h ) ) = &self.display_size
{
let vp = canvas.viewport_layout();
let em = Length::EM_BASE_DEFAULT;
let rw = w.resolve( vp, em ).max( 0.0 );
let rh = h.resolve( vp, em ).max( 0.0 );
let rw = canvas.resolve_geom( *w ).max( 0.0 );
let rh = canvas.resolve_geom( *h ).max( 0.0 );
return ( rw, rh );
}
if self.cover

View File

@@ -208,7 +208,7 @@ impl<Msg: Clone> ListItem<Msg>
let label_size = canvas.font_px( theme::LABEL_SIZE );
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 ) );
let has_sub = self.subtitle.is_some();
let label_y = if has_sub

View File

@@ -97,7 +97,7 @@ impl<Msg: Clone> RichText<Msg>
#[ inline ]
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>>

View File

@@ -61,7 +61,7 @@ impl Separator
fn resolved_thickness( &self, canvas: &Canvas ) -> f32
{
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 ) )
}
@@ -70,7 +70,7 @@ impl Separator
fn resolved_pad_v( &self, canvas: &Canvas ) -> f32
{
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 ) )
}

View File

@@ -82,7 +82,7 @@ impl Text
#[ inline ]
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

View File

@@ -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
{
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 ) )
}
@@ -444,10 +444,10 @@ impl<Msg: Clone> TextEdit<Msg>
( max_width, h )
} else {
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 );
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 ) );
( w, h )
}

View File

@@ -139,10 +139,8 @@ impl<Msg: Clone> VSlider<Msg>
/// the type-level docs on intrinsic sizing.
pub fn preferred_size( &self, _max_width: f32, canvas: &Canvas ) -> (f32, f32)
{
let vp = canvas.viewport_layout();
let em = Length::EM_BASE_DEFAULT;
let w = self.width.resolve( vp, em ).max( 2.0 );
let h = self.height.resolve( vp, em ).max( 2.0 );
let w = canvas.resolve_geom( self.width ).max( 2.0 );
let h = canvas.resolve_geom( self.height ).max( 2.0 );
( w, h )
}