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:
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user