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

@@ -212,12 +212,10 @@ pub( crate ) fn layout_and_draw<Msg: Clone>(
{
canvas.stroke_rect( rect, color, width, c.corners );
}
let vp = canvas.viewport_layout();
let em = crate::types::Length::EM_BASE_DEFAULT;
let pad_l = c.pad_left.resolve( vp, em );
let pad_r = c.pad_right.resolve( vp, em );
let pad_t = c.pad_top.resolve( vp, em );
let pad_b = c.pad_bottom.resolve( vp, em );
let pad_l = canvas.resolve_geom( c.pad_left );
let pad_r = canvas.resolve_geom( c.pad_right );
let pad_t = canvas.resolve_geom( c.pad_top );
let pad_b = canvas.resolve_geom( c.pad_bottom );
let inner = crate::types::Rect
{
x: rect.x + pad_l,

View File

@@ -15,6 +15,14 @@
//! serve the stale texture for the new content. Content-keying makes
//! that impossible: identical bytes → identical key, regardless of
//! 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::hash::{ Hash, Hasher };
@@ -26,6 +34,12 @@ use crate::types::Rect;
use super::helpers::{ ortho_rect, upload_rgba_texture };
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
/// cache. Hashes the full byte slice for small buffers (icons,
/// 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 );
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
@@ -107,9 +128,9 @@ impl GlesCanvas
let mvp = ortho_rect( self.width, self.height, dest );
let alpha = opacity * self.global_alpha;
// SAFETY: see `primitives.rs` module doc. `*tex` is owned by
// `self.image_cache` so it outlives the call. The image cache
// stays valid as long as `&mut self` is held — no eviction
// path runs concurrently with the draw.
// `self.image_cache` so it outlives the call. Eviction ran
// before this borrow and never removes the most-recent key,
// which is `cache_key`.
unsafe
{
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`.
///
/// The caller owns the texture and is responsible for keeping it valid

View File

@@ -45,7 +45,7 @@
//! method — see the module's own doc for when to use the guards
//! and when not to.
use std::collections::HashMap;
use std::collections::{ HashMap, VecDeque };
use std::sync::Arc;
use fontdue::Font;
@@ -157,6 +157,11 @@ pub struct GlesCanvas
/// the same inside offscreen content (scroll viewports, clip layers)
/// as outside it.
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 width: u32,
pub height: u32,
@@ -349,8 +354,13 @@ pub struct GlesCanvas
// frame (the cache would happily serve the stale texture).
// Content-keying tolerates that case at the cost of one
// `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)>,
// 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.
// Gradients are theme-derived and constant across frames; caching avoids

View File

@@ -337,6 +337,7 @@ impl GlesCanvas
font_registry: None,
dpi_scale: 1.0,
layout_viewport: None,
density: None,
global_alpha: 1.0,
width,
height,
@@ -459,6 +460,8 @@ impl GlesCanvas
atlas_row_height: 0,
glyph_cache: HashMap::new(),
image_cache: HashMap::new(),
image_cache_lru: std::collections::VecDeque::new(),
image_cache_bytes: 0,
gradient_lut_cache: HashMap::new(),
clip_scissor: None,
clip_layer: None,
@@ -550,6 +553,7 @@ impl GlesCanvas
dpi_scale: self.dpi_scale,
layout_viewport: Some( self.layout_viewport.unwrap_or(
( self.width as f32, self.height as f32 ) ) ),
density: self.density,
global_alpha: self.global_alpha,
width,
height,
@@ -672,6 +676,8 @@ impl GlesCanvas
atlas_row_height: 0,
glyph_cache: HashMap::new(),
image_cache: HashMap::new(),
image_cache_lru: std::collections::VecDeque::new(),
image_cache_bytes: 0,
gradient_lut_cache: HashMap::new(),
clip_scissor: None,
clip_layer: None,

View File

@@ -122,19 +122,19 @@ impl<Msg: Clone> Column<Msg>
#[ inline ]
fn resolved_spacing( &self, canvas: &Canvas ) -> f32
{
self.spacing.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT )
canvas.resolve_geom( self.spacing )
}
#[ inline ]
fn resolved_padding( &self, canvas: &Canvas ) -> f32
{
self.padding.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT )
canvas.resolve_geom( self.padding )
}
#[ inline ]
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

View File

@@ -92,13 +92,13 @@ impl<Msg: Clone> Row<Msg>
#[ inline ]
fn resolved_spacing( &self, canvas: &Canvas ) -> f32
{
self.spacing.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT )
canvas.resolve_geom( self.spacing )
}
#[ inline ]
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.

View File

@@ -115,11 +115,9 @@ impl Spacer
/// weighted by `weight`.
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_height.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| canvas.resolve_geom( l ) ).unwrap_or( 0.0 ),
)
}
@@ -128,12 +126,12 @@ impl Spacer
/// layout only needs the main-axis size for one orientation.
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>
{
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.

View File

@@ -116,7 +116,7 @@ impl<Msg: Clone> WrapGrid<Msg>
{
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 );
match self.max_columns
{
@@ -130,12 +130,10 @@ impl<Msg: Clone> WrapGrid<Msg>
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 ),
self.spacing_y.resolve( vp, em ),
self.padding.resolve( vp, em ),
canvas.resolve_geom( self.spacing_x ),
canvas.resolve_geom( self.spacing_y ),
canvas.resolve_geom( self.padding ),
)
}

View File

@@ -115,8 +115,9 @@
//! - [`Length`] — a size/distance that may be absolute pixels
//! ([`LengthBase::Px`]), relative to the surface viewport
//! ([`LengthBase::Vw`] / [`LengthBase::Vh`] / [`LengthBase::Vmin`] /
//! [`LengthBase::Vmax`] / [`LengthBase::Orient`]) or to the root font
//! size ([`LengthBase::Em`]). Every setter that takes a size, padding,
//! [`LengthBase::Vmax`] / [`LengthBase::Orient`]), to the root font
//! 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
//! `.size( 24.0 )` keeps working while new code can write
//! `.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,
//! 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
//! other mode: [`Length::dp`] (a density-independent pixel — `n ×`
//! [`density`], the mainstream HiDPI unit), or [`LengthBase::Em`] for text
//! other mode: [`Length::dp`] (a density-independent pixel — `n ×` the
//! 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
//! [`theme::typography`] scale
//! ([`theme::typography::h0`]…[`theme::typography::body_xs`]) is built on

View File

@@ -143,6 +143,11 @@ pub struct SoftwareCanvas
/// the same inside offscreen content (scroll viewports, clip layers)
/// as outside it.
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 =
/// transparent, 1.0 = opaque).
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,
/// box size, gap…) through the process-wide [`crate::WidgetScaling`]
/// mode, into a concrete physical-pixel value for the layout tree.
@@ -275,7 +325,7 @@ impl Canvas
/// [`Self::viewport_layout`].
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
@@ -292,13 +342,13 @@ impl Canvas
{
WidgetScaling::Fluid =>
{
Length::fluid( design_px ).resolve( self.viewport_logical(), Length::EM_BASE_DEFAULT )
self.resolve_font( Length::fluid( design_px ) )
}
WidgetScaling::Physical =>
{
let scale = self.dpi_scale();
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 );
}
#[ 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 ]
fn font_px_is_constant_physical_in_physical_mode()
{

View File

@@ -37,6 +37,7 @@ impl SoftwareCanvas
font_registry: None,
dpi_scale: 1.0,
layout_viewport: None,
density: None,
global_alpha: 1.0,
glyph_cache: HashMap::new(),
clip_mask: None,
@@ -57,6 +58,7 @@ impl SoftwareCanvas
dpi_scale: self.dpi_scale,
layout_viewport: Some( self.layout_viewport.unwrap_or(
( self.pixmap.width() as f32, self.pixmap.height() as f32 ) ) ),
density: self.density,
global_alpha: self.global_alpha,
glyph_cache: HashMap::new(),
clip_mask: None,

View File

@@ -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()
{

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 )
}