Resolve fluid geometry bounds in logical space and let the runtime set the density from the buffer scale
Stock widgets size themselves with `Length::fluid`, whose auto-clamp `[0.7·px, 1.5·px]` is documented as logical px, yet `Canvas::resolve_geom` resolved it against the physical viewport and applied the bounds to the physical value. On any HiDPI surface whose physical short side exceeds 618 px (412 × 1.5) the cap engaged: a 1080 × 1920 phone at scale 2 got its geometry at 1.5 physical px per design px, that is 0.75 logical, while fonts, resolved by `font_px` against the logical viewport and scaled at raster time, kept growing to 2.62. Rows, paddings and icons came out at 60 % of their intended size with the text overflowing them; at scale 1 physical and logical coincide, so a winit session never showed it. `Length::resolve_physical` takes the buffer scale and multiplies `min_px` / `max_px` by it before clamping; `resolve_with_density` delegates to it with scale 1. `Canvas::resolve_geom` passes its `dpi_scale`, so every stock-widget geometry shares the surface factor of the fonts, and `overlays_reconcile` does the same for `OverlaySpec::size`, which had the same mix. A zero scale falls back to unscaled bounds. The event loop now calls `set_density( sf )` right before `App::on_scale_changed`, both on `configure` and on `scale_factor_changed` of the main surface, so `Length::dp( n )` means n logical px in every app without plumbing; an app wanting a DPI-derived density still overrides it from that callback, and crustace already sets the same value there. Two tests cover the clamp at scale 2 and the geometry/font parity on a 1080 × 2460 canvas.
This commit is contained in:
@@ -447,6 +447,7 @@ impl<A: App> AppData<A>
|
|||||||
// current buffer scale before handing the dimensions to the app.
|
// current buffer scale before handing the dimensions to the app.
|
||||||
let sf = self.main.scale_factor.max( 1 ) as u32;
|
let sf = self.main.scale_factor.max( 1 ) as u32;
|
||||||
crate::types::set_viewport_size( w * sf, h * sf );
|
crate::types::set_viewport_size( w * sf, h * sf );
|
||||||
|
crate::types::set_density( sf as f32 );
|
||||||
self.app.on_scale_changed( sf );
|
self.app.on_scale_changed( sf );
|
||||||
self.app.on_resize( w * sf, h * sf );
|
self.app.on_resize( w * sf, h * sf );
|
||||||
// `on_resize` may flip app-state that the view depends on (apps that
|
// `on_resize` may flip app-state that the view depends on (apps that
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ impl<A: App> CompositorHandler for AppData<A>
|
|||||||
if matches!( focus, super::SurfaceFocus::Main )
|
if matches!( focus, super::SurfaceFocus::Main )
|
||||||
{
|
{
|
||||||
crate::types::set_viewport_size( pw, ph );
|
crate::types::set_viewport_size( pw, ph );
|
||||||
|
crate::types::set_density( new_factor as f32 );
|
||||||
self.app.on_scale_changed( new_factor as u32 );
|
self.app.on_scale_changed( new_factor as u32 );
|
||||||
self.app.on_resize( pw, ph );
|
self.app.on_resize( pw, ph );
|
||||||
self.dirty_caches();
|
self.dirty_caches();
|
||||||
|
|||||||
@@ -114,9 +114,10 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
|||||||
let main_vp = ( data.main.physical_width() as f32, data.main.physical_height() as f32 );
|
let main_vp = ( data.main.physical_width() as f32, data.main.physical_height() as f32 );
|
||||||
let resolve_size = move | s: &( Length, Length ) | -> ( u32, u32 )
|
let resolve_size = move | s: &( Length, Length ) | -> ( u32, u32 )
|
||||||
{
|
{
|
||||||
|
let density = crate::types::density();
|
||||||
(
|
(
|
||||||
s.0.resolve( main_vp, Length::EM_BASE_DEFAULT ).max( 0.0 ).round() as u32,
|
s.0.resolve_physical( main_vp, Length::EM_BASE_DEFAULT, density, parent_scale ).max( 0.0 ).round() as u32,
|
||||||
s.1.resolve( main_vp, Length::EM_BASE_DEFAULT ).max( 0.0 ).round() as u32,
|
s.1.resolve_physical( main_vp, Length::EM_BASE_DEFAULT, density, parent_scale ).max( 0.0 ).round() as u32,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
// `OverlaySpec::size` is physical pixels (the app's layout space); the
|
// `OverlaySpec::size` is physical pixels (the app's layout space); the
|
||||||
|
|||||||
+18
-1
@@ -318,7 +318,7 @@ impl Canvas
|
|||||||
/// a `Length::dp` override follows the canvas the widget draws on.
|
/// a `Length::dp` override follows the canvas the widget draws on.
|
||||||
pub fn resolve_geom( &self, l: Length ) -> f32
|
pub fn resolve_geom( &self, l: Length ) -> f32
|
||||||
{
|
{
|
||||||
l.resolve_with_density( self.viewport_layout(), Length::EM_BASE_DEFAULT, self.density() )
|
l.resolve_physical( self.viewport_layout(), Length::EM_BASE_DEFAULT, self.density(), self.dpi_scale() )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve an explicit [`Length`] in **font** space: against
|
/// Resolve an explicit [`Length`] in **font** space: against
|
||||||
@@ -960,6 +960,23 @@ mod viewport_tests
|
|||||||
set_widget_scaling( WidgetScaling::Fluid );
|
set_widget_scaling( WidgetScaling::Fluid );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn geom_px_fluid_cap_is_logical_on_hidpi()
|
||||||
|
{
|
||||||
|
use crate::types::{ set_widget_scaling, WidgetScaling };
|
||||||
|
let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );
|
||||||
|
set_widget_scaling( WidgetScaling::Fluid );
|
||||||
|
|
||||||
|
// 1080 × 2460 phone at buffer scale 2: geometry and fonts must share
|
||||||
|
// the same surface factor (2.62 physical px per design px).
|
||||||
|
let mut c = Canvas::new( 1080, 2460 );
|
||||||
|
c.set_dpi_scale( 2.0 );
|
||||||
|
let geom = c.geom_px( 48.0 );
|
||||||
|
let font = c.font_px( 48.0 ) * 2.0;
|
||||||
|
assert!( ( geom - 48.0 / 412.0 * 1080.0 ).abs() < 1e-2 );
|
||||||
|
assert!( ( geom - font ).abs() < 1e-2 );
|
||||||
|
}
|
||||||
|
|
||||||
#[ test ]
|
#[ test ]
|
||||||
fn canvas_density_overrides_process_density()
|
fn canvas_density_overrides_process_density()
|
||||||
{
|
{
|
||||||
|
|||||||
+33
-6
@@ -639,10 +639,22 @@ impl Length
|
|||||||
/// what [`crate::Canvas`]-routed resolution calls with the canvas'
|
/// what [`crate::Canvas`]-routed resolution calls with the canvas'
|
||||||
/// own density.
|
/// own density.
|
||||||
pub fn resolve_with_density( &self, viewport: ( f32, f32 ), em_base: f32, density: f32 ) -> f32
|
pub fn resolve_with_density( &self, viewport: ( f32, f32 ), em_base: f32, density: f32 ) -> f32
|
||||||
|
{
|
||||||
|
self.resolve_physical( viewport, em_base, density, 1.0 )
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Self::resolve_with_density`] against a **physical** viewport
|
||||||
|
/// (`dpi_scale` physical px per logical px). The bounds are logical
|
||||||
|
/// px, so they are scaled to physical before they apply; otherwise a
|
||||||
|
/// `fluid` value hits its cap on any HiDPI surface whose physical
|
||||||
|
/// short side exceeds the cap, while fonts (resolved in logical space)
|
||||||
|
/// keep growing.
|
||||||
|
pub fn resolve_physical( &self, viewport: ( f32, f32 ), em_base: f32, density: f32, dpi_scale: f32 ) -> f32
|
||||||
{
|
{
|
||||||
let raw = self.base.resolve( viewport, em_base, density );
|
let raw = self.base.resolve( viewport, em_base, density );
|
||||||
let lo = self.min_px;
|
let k = if dpi_scale > 0.0 { dpi_scale } else { 1.0 };
|
||||||
let hi = self.max_px;
|
let lo = self.min_px.map( |v| v * k );
|
||||||
|
let hi = self.max_px.map( |v| v * k );
|
||||||
// If both bounds present, normalise their order so swapped args
|
// If both bounds present, normalise their order so swapped args
|
||||||
// don't produce NaN out of f32::clamp.
|
// don't produce NaN out of f32::clamp.
|
||||||
let ( lo, hi ) = match ( lo, hi )
|
let ( lo, hi ) = match ( lo, hi )
|
||||||
@@ -732,16 +744,17 @@ pub fn text_scale() -> f32
|
|||||||
static DENSITY_BITS: AtomicU32 = AtomicU32::new( 1.0_f32.to_bits() );
|
static DENSITY_BITS: AtomicU32 = AtomicU32::new( 1.0_f32.to_bits() );
|
||||||
|
|
||||||
/// Set the process-wide pixel density used by [`Length::dp`] (the
|
/// Set the process-wide pixel density used by [`Length::dp`] (the
|
||||||
/// [`WidgetScaling::Physical`] mode). Typically derived from the output's
|
/// [`WidgetScaling::Physical`] mode). The runtime sets it to the main
|
||||||
/// physical DPI so `dp` sizes stay physically constant across displays.
|
/// surface's buffer scale right before [`crate::App::on_scale_changed`],
|
||||||
/// Default: `1.0`.
|
/// so `dp( n )` is `n` logical px by default; an app that wants a
|
||||||
|
/// DPI-derived density overrides it from that callback. Default: `1.0`.
|
||||||
pub fn set_density( d: f32 )
|
pub fn set_density( d: f32 )
|
||||||
{
|
{
|
||||||
DENSITY_BITS.store( d.max( 0.0 ).to_bits(), Ordering::Relaxed );
|
DENSITY_BITS.store( d.max( 0.0 ).to_bits(), Ordering::Relaxed );
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Current pixel density — the factor [`Length::dp`] multiplies its design
|
/// Current pixel density — the factor [`Length::dp`] multiplies its design
|
||||||
/// pixels by. `1.0` until [`set_density`] is called.
|
/// pixels by. `1.0` until the runtime or [`set_density`] sets it.
|
||||||
pub fn density() -> f32
|
pub fn density() -> f32
|
||||||
{
|
{
|
||||||
f32::from_bits( DENSITY_BITS.load( Ordering::Relaxed ) )
|
f32::from_bits( DENSITY_BITS.load( Ordering::Relaxed ) )
|
||||||
@@ -946,6 +959,20 @@ mod length_tests
|
|||||||
assert_eq!( p, l );
|
assert_eq!( p, l );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn fluid_bounds_scale_with_dpi_on_a_physical_viewport()
|
||||||
|
{
|
||||||
|
// 1080 × 2460 at scale 2: the physical short side is 2.62 × the
|
||||||
|
// reference, the cap is 1.5 × 48 logical = 144 physical, so the
|
||||||
|
// unclamped 125.8 stands.
|
||||||
|
let v = Length::fluid( 48.0 ).resolve_physical( ( 1080.0, 2460.0 ), 16.0, 1.0, 2.0 );
|
||||||
|
assert!( ( v - 48.0 / 412.0 * 1080.0 ).abs() < 1e-3 );
|
||||||
|
// Same surface treated as scale 1 hits the cap in physical px.
|
||||||
|
assert_eq!( Length::fluid( 48.0 ).resolve_physical( ( 1080.0, 2460.0 ), 16.0, 1.0, 1.0 ), 72.0 );
|
||||||
|
// A zero scale falls back to unscaled bounds instead of collapsing to 0.
|
||||||
|
assert_eq!( Length::fluid( 48.0 ).resolve_physical( ( 1080.0, 2460.0 ), 16.0, 1.0, 0.0 ), 72.0 );
|
||||||
|
}
|
||||||
|
|
||||||
#[ test ]
|
#[ test ]
|
||||||
fn dp_is_identity_at_default_density()
|
fn dp_is_identity_at_default_density()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user