Resolve fluid geometry bounds in logical space and let the runtime set the density from the buffer scale
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

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:
2026-09-10 21:18:24 +02:00
parent 2cee9a1c57
commit 7c1ef86afe
5 changed files with 56 additions and 9 deletions
+1
View File
@@ -447,6 +447,7 @@ impl<A: App> AppData<A>
// current buffer scale before handing the dimensions to the app.
let sf = self.main.scale_factor.max( 1 ) as u32;
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_resize( w * sf, h * sf );
// `on_resize` may flip app-state that the view depends on (apps that
+1
View File
@@ -130,6 +130,7 @@ impl<A: App> CompositorHandler for AppData<A>
if matches!( focus, super::SurfaceFocus::Main )
{
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_resize( pw, ph );
self.dirty_caches();
+3 -2
View File
@@ -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 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.1.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_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