Support viewport-relative widget sizing
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -411,6 +411,7 @@ impl<A: App> AppData<A>
|
||||
// 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
|
||||
|
||||
@@ -99,6 +99,7 @@ impl<A: App> CompositorHandler for AppData<A>
|
||||
// `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();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ impl<Msg: Clone> Element<Msg>
|
||||
{
|
||||
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 ),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
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<Length>, height: impl Into<Length> ) -> 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
|
||||
{
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
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<Msg: Clone>
|
||||
/// 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<Msg: Clone> VSlider<Msg>
|
||||
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<Msg: Clone> VSlider<Msg>
|
||||
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<Length>, height: impl Into<Length> ) -> 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<Msg: Clone> VSlider<Msg>
|
||||
|
||||
/// 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`.
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
Reference in New Issue
Block a user