From 68c6a87bf6c09da80414dd452ed2f3944d77b6c1 Mon Sep 17 00:00:00 2001 From: yamabush1 Date: Thu, 4 Jun 2026 12:38:59 +0200 Subject: [PATCH] Support viewport-relative widget sizing --- src/app.rs | 5 +++++ src/event_loop/app_data.rs | 1 + src/event_loop/handlers.rs | 1 + src/widget/element.rs | 2 +- src/widget/image/mod.rs | 22 +++++++++++++--------- src/widget/image/tests.rs | 26 +++++++++++++++++++------- src/widget/vslider/mod.rs | 33 +++++++++++++++++++-------------- src/widget/vslider/tests.rs | 4 ++-- 8 files changed, 61 insertions(+), 33 deletions(-) diff --git a/src/app.rs b/src/app.rs index 69cf858..9c28e3e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -594,6 +594,11 @@ pub trait App: 'static /// physical dimensions. fn on_resize( &mut self, _width: u32, _height: u32 ) {} + /// Called when the compositor reports a new integer buffer scale for the + /// main surface. The default is inert so apps that only care about physical + /// pixels can keep using [`on_resize`](Self::on_resize). + fn on_scale_changed( &mut self, _scale: u32 ) {} + /// Called once at startup with a channel sender that can be used from any /// thread to deliver messages into the event loop. Sending a message /// immediately wakes the event loop — no polling delay. diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index 68d8296..b59e643 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -411,6 +411,7 @@ impl AppData // configure.new_size is surface-local (logical), so multiply by the // current buffer scale before handing the dimensions to the app. let sf = self.main.scale_factor.max( 1 ) as u32; + 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 // branch on the new dimensions, layout caches keyed by size, …), so diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index 8b3a01b..6a998e7 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -99,6 +99,7 @@ impl CompositorHandler for AppData // `App::on_resize`. if matches!( focus, super::SurfaceFocus::Main ) { + self.app.on_scale_changed( new_factor as u32 ); self.app.on_resize( pw, ph ); self.dirty_caches(); } diff --git a/src/widget/element.rs b/src/widget/element.rs index 74f8f70..d55a20f 100644 --- a/src/widget/element.rs +++ b/src/widget/element.rs @@ -51,7 +51,7 @@ impl Element { Element::Button( b ) => b.preferred_size( max_width, canvas ), Element::TextEdit( t ) => t.preferred_size( max_width, canvas ), - Element::Image( i ) => i.preferred_size( max_width ), + Element::Image( i ) => i.preferred_size( max_width, canvas ), Element::Column( c ) => c.preferred_size( max_width, canvas ), Element::Row( r ) => r.preferred_size( max_width, canvas ), Element::Stack( s ) => s.preferred_size( max_width, canvas ), diff --git a/src/widget/image/mod.rs b/src/widget/image/mod.rs index e36a0fc..1f1042b 100644 --- a/src/widget/image/mod.rs +++ b/src/widget/image/mod.rs @@ -2,7 +2,7 @@ // Copyright (C) 2026 Liberux Labs, S. L. use std::sync::Arc; -use crate::types::Rect; +use crate::types::{ Length, Rect }; use crate::render::Canvas; /// A static image widget that renders RGBA pixel data. @@ -34,8 +34,8 @@ pub struct Image pub height: u32, /// When `true` the image scales to fill the available width (cover mode). pub cover: bool, - /// Optional explicit display size in logical pixels. - pub display_size: Option<( f32, f32 )>, + /// Optional explicit display size in logical units. + pub display_size: Option<( Length, Length )>, /// Opacity multiplier in `[0.0, 1.0]`. Default: `1.0`. pub opacity: f32, } @@ -66,10 +66,10 @@ impl Image self } - /// Set an explicit display size in logical pixels. - pub fn size( mut self, width: f32, height: f32 ) -> Self + /// Set an explicit display size in logical units. + pub fn size( mut self, width: impl Into, height: impl Into ) -> Self { - self.display_size = Some( ( width.max( 0.0 ), height.max( 0.0 ) ) ); + self.display_size = Some( ( width.into(), height.into() ) ); self } @@ -81,11 +81,15 @@ impl Image } /// Return the preferred `(width, height)` given available `max_width`. - pub fn preferred_size( &self, max_width: f32 ) -> (f32, f32) + pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32) { - if let Some( size ) = self.display_size + if let Some( ( width, height ) ) = self.display_size { - return size; + let viewport = canvas.viewport_logical(); + return ( + width.resolve( viewport, Length::EM_BASE_DEFAULT ).max( 0.0 ), + height.resolve( viewport, Length::EM_BASE_DEFAULT ).max( 0.0 ), + ); } if self.cover { diff --git a/src/widget/image/tests.rs b/src/widget/image/tests.rs index 6dbaf0d..066f1d6 100644 --- a/src/widget/image/tests.rs +++ b/src/widget/image/tests.rs @@ -32,14 +32,14 @@ fn cover_builder_sets_cover_flag() fn size_builder_sets_explicit_display_size() { let img = Image::new( solid_rgba( 4, 4 ), 4, 4 ).size( 120.0, 80.0 ); - assert_eq!( img.display_size, Some( ( 120.0, 80.0 ) ) ); + assert_eq!( img.display_size, Some( ( Length::px( 120.0 ), Length::px( 80.0 ) ) ) ); } #[ test ] -fn size_builder_clamps_negative_dimensions_to_zero() +fn size_builder_keeps_length_values_for_later_viewport_resolution() { let img = Image::new( solid_rgba( 4, 4 ), 4, 4 ).size( -10.0, -20.0 ); - assert_eq!( img.display_size, Some( ( 0.0, 0.0 ) ) ); + assert_eq!( img.display_size, Some( ( Length::px( -10.0 ), Length::px( -20.0 ) ) ) ); } #[ test ] @@ -57,7 +57,8 @@ fn preferred_size_default_scales_height_to_match_width_aspect() { // 200×100 source image at max_width = 400 → scale = 2 → height 200. let img = Image::new( solid_rgba( 200, 100 ), 200, 100 ); - let ( w, h ) = img.preferred_size( 400.0 ); + let canvas = Canvas::new( 480, 900 ); + let ( w, h ) = img.preferred_size( 400.0, &canvas ); assert_eq!( w, 400.0 ); assert_eq!( h, 200.0 ); } @@ -66,7 +67,16 @@ fn preferred_size_default_scales_height_to_match_width_aspect() fn preferred_size_with_explicit_size_wins_over_aspect_logic() { let img = Image::new( solid_rgba( 200, 100 ), 200, 100 ).size( 50.0, 25.0 ); - assert_eq!( img.preferred_size( 1000.0 ), ( 50.0, 25.0 ) ); + let canvas = Canvas::new( 480, 900 ); + assert_eq!( img.preferred_size( 1000.0, &canvas ), ( 50.0, 25.0 ) ); +} + +#[ test ] +fn preferred_size_resolves_explicit_viewport_lengths() +{ + let img = Image::new( solid_rgba( 200, 100 ), 200, 100 ).size( Length::vw( 10.0 ), Length::vh( 10.0 ) ); + let canvas = Canvas::new( 480, 900 ); + assert_eq!( img.preferred_size( 1000.0, &canvas ), ( 48.0, 90.0 ) ); } #[ test ] @@ -74,7 +84,8 @@ fn preferred_size_cover_mode_keeps_aspect_ratio() { // 100×50 source in cover mode at max_width = 300 → height = 300 * (50/100) = 150. let img = Image::new( solid_rgba( 100, 50 ), 100, 50 ).cover(); - let ( w, h ) = img.preferred_size( 300.0 ); + let canvas = Canvas::new( 480, 900 ); + let ( w, h ) = img.preferred_size( 300.0, &canvas ); assert_eq!( w, 300.0 ); assert_eq!( h, 150.0 ); } @@ -87,7 +98,8 @@ fn preferred_size_cover_and_default_match_for_uniform_scaling() // when the source is taller than wide. let normal = Image::new( solid_rgba( 100, 50 ), 100, 50 ); let covered = Image::new( solid_rgba( 100, 50 ), 100, 50 ).cover(); - assert_eq!( normal.preferred_size( 200.0 ), covered.preferred_size( 200.0 ) ); + let canvas = Canvas::new( 480, 900 ); + assert_eq!( normal.preferred_size( 200.0, &canvas ), covered.preferred_size( 200.0, &canvas ) ); } // ── Arc sharing ─────────────────────────────────────────────────────────── diff --git a/src/widget/vslider/mod.rs b/src/widget/vslider/mod.rs index 6fb1fb0..274506f 100644 --- a/src/widget/vslider/mod.rs +++ b/src/widget/vslider/mod.rs @@ -2,7 +2,7 @@ // Copyright (C) 2026 Liberux Labs, S. L. use std::sync::Arc; -use crate::types::Rect; +use crate::types::{ Length, Rect }; use crate::render::Canvas; use super::Element; use super::slider::intersect_clip; @@ -65,10 +65,10 @@ pub struct VSlider /// Current value in `[0.0, 1.0]`. `0.0` paints no fill; `1.0` fills the /// whole pill. pub value: f32, - /// Fixed width of the pill in pixels. Defaults to 56. - pub width: f32, - /// Fixed height of the pill in pixels. Defaults to 160. - pub height: f32, + /// Fixed width of the pill in logical units. Defaults to 56 px. + pub width: Length, + /// Fixed height of the pill in logical units. Defaults to 160 px. + pub height: Length, /// Callback invoked with the new value when the slider is tapped or /// dragged. `Arc` (not `Box`) so the layout pass can clone it into the /// per-leaf handler snapshot for O(1) dispatch on input events. @@ -93,8 +93,8 @@ impl VSlider Self { value: value.clamp( 0.0, 1.0 ), - width: theme::WIDTH, - height: theme::HEIGHT, + width: Length::px( theme::WIDTH ), + height: Length::px( theme::HEIGHT ), on_change: None, track_surface: theme::SURFACE_TRACK, fill_surface: theme::SURFACE_FILL, @@ -117,12 +117,13 @@ impl VSlider self } - /// Override the fixed pill `(width, height)` in pixels. Both are clamped - /// to a minimum of `2.0` so a rounded pill can always be drawn. - pub fn size( mut self, width: f32, height: f32 ) -> Self + /// Override the fixed pill `(width, height)` in logical units. Both are + /// clamped to a minimum of `2.0` after viewport-relative values resolve, + /// so a rounded pill can always be drawn. + pub fn size( mut self, width: impl Into, height: impl Into ) -> Self { - self.width = width.max( 2.0 ); - self.height = height.max( 2.0 ); + self.width = width.into(); + self.height = height.into(); self } @@ -135,9 +136,13 @@ impl VSlider /// Return the preferred `(width, height)`. `max_width` is ignored — see /// the type-level docs on intrinsic sizing. - pub fn preferred_size( &self, _max_width: f32, _canvas: &Canvas ) -> (f32, f32) + pub fn preferred_size( &self, _max_width: f32, canvas: &Canvas ) -> (f32, f32) { - ( self.width, self.height ) + let viewport = canvas.viewport_logical(); + ( + self.width.resolve( viewport, Length::EM_BASE_DEFAULT ).max( 2.0 ), + self.height.resolve( viewport, Length::EM_BASE_DEFAULT ).max( 2.0 ), + ) } /// Compute the value `[0.0, 1.0]` from a tap/drag y position within `rect`. diff --git a/src/widget/vslider/tests.rs b/src/widget/vslider/tests.rs index 66aa328..70f8c99 100644 --- a/src/widget/vslider/tests.rs +++ b/src/widget/vslider/tests.rs @@ -100,8 +100,8 @@ fn preferred_size_ignores_max_width() fn default_dimensions_are_the_theme_constants() { let s: VSlider<()> = vslider( 0.0 ); - assert_eq!( s.width, theme::WIDTH ); - assert_eq!( s.height, theme::HEIGHT ); + assert_eq!( s.width, Length::px( theme::WIDTH ) ); + assert_eq!( s.height, Length::px( theme::HEIGHT ) ); } #[ test ]