Support viewport-relative widget sizing
This commit is contained in:
@@ -594,6 +594,11 @@ pub trait App: 'static
|
|||||||
/// physical dimensions.
|
/// physical dimensions.
|
||||||
fn on_resize( &mut self, _width: u32, _height: u32 ) {}
|
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
|
/// 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
|
/// thread to deliver messages into the event loop. Sending a message
|
||||||
/// immediately wakes the event loop — no polling delay.
|
/// 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
|
// configure.new_size is surface-local (logical), so multiply by the
|
||||||
// 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;
|
||||||
|
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
|
||||||
// branch on the new dimensions, layout caches keyed by size, …), so
|
// 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`.
|
// `App::on_resize`.
|
||||||
if matches!( focus, super::SurfaceFocus::Main )
|
if matches!( focus, super::SurfaceFocus::Main )
|
||||||
{
|
{
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ impl<Msg: Clone> Element<Msg>
|
|||||||
{
|
{
|
||||||
Element::Button( b ) => b.preferred_size( max_width, canvas ),
|
Element::Button( b ) => b.preferred_size( max_width, canvas ),
|
||||||
Element::TextEdit( t ) => t.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::Column( c ) => c.preferred_size( max_width, canvas ),
|
||||||
Element::Row( r ) => r.preferred_size( max_width, canvas ),
|
Element::Row( r ) => r.preferred_size( max_width, canvas ),
|
||||||
Element::Stack( s ) => s.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>
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::types::Rect;
|
use crate::types::{ Length, Rect };
|
||||||
use crate::render::Canvas;
|
use crate::render::Canvas;
|
||||||
|
|
||||||
/// A static image widget that renders RGBA pixel data.
|
/// A static image widget that renders RGBA pixel data.
|
||||||
@@ -34,8 +34,8 @@ pub struct Image
|
|||||||
pub height: u32,
|
pub height: u32,
|
||||||
/// When `true` the image scales to fill the available width (cover mode).
|
/// When `true` the image scales to fill the available width (cover mode).
|
||||||
pub cover: bool,
|
pub cover: bool,
|
||||||
/// Optional explicit display size in logical pixels.
|
/// Optional explicit display size in logical units.
|
||||||
pub display_size: Option<( f32, f32 )>,
|
pub display_size: Option<( Length, Length )>,
|
||||||
/// Opacity multiplier in `[0.0, 1.0]`. Default: `1.0`.
|
/// Opacity multiplier in `[0.0, 1.0]`. Default: `1.0`.
|
||||||
pub opacity: f32,
|
pub opacity: f32,
|
||||||
}
|
}
|
||||||
@@ -66,10 +66,10 @@ impl Image
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set an explicit display size in logical pixels.
|
/// Set an explicit display size in logical units.
|
||||||
pub fn size( mut self, width: f32, height: f32 ) -> Self
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,11 +81,15 @@ impl Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return the preferred `(width, height)` given available `max_width`.
|
/// 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
|
if self.cover
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ fn cover_builder_sets_cover_flag()
|
|||||||
fn size_builder_sets_explicit_display_size()
|
fn size_builder_sets_explicit_display_size()
|
||||||
{
|
{
|
||||||
let img = Image::new( solid_rgba( 4, 4 ), 4, 4 ).size( 120.0, 80.0 );
|
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 ]
|
#[ 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 );
|
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 ]
|
#[ 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.
|
// 200×100 source image at max_width = 400 → scale = 2 → height 200.
|
||||||
let img = Image::new( solid_rgba( 200, 100 ), 200, 100 );
|
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!( w, 400.0 );
|
||||||
assert_eq!( h, 200.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()
|
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 );
|
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 ]
|
#[ 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.
|
// 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 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!( w, 300.0 );
|
||||||
assert_eq!( h, 150.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.
|
// when the source is taller than wide.
|
||||||
let normal = Image::new( solid_rgba( 100, 50 ), 100, 50 );
|
let normal = Image::new( solid_rgba( 100, 50 ), 100, 50 );
|
||||||
let covered = Image::new( solid_rgba( 100, 50 ), 100, 50 ).cover();
|
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 ───────────────────────────────────────────────────────────
|
// ── Arc sharing ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::types::Rect;
|
use crate::types::{ Length, Rect };
|
||||||
use crate::render::Canvas;
|
use crate::render::Canvas;
|
||||||
use super::Element;
|
use super::Element;
|
||||||
use super::slider::intersect_clip;
|
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
|
/// Current value in `[0.0, 1.0]`. `0.0` paints no fill; `1.0` fills the
|
||||||
/// whole pill.
|
/// whole pill.
|
||||||
pub value: f32,
|
pub value: f32,
|
||||||
/// Fixed width of the pill in pixels. Defaults to 56.
|
/// Fixed width of the pill in logical units. Defaults to 56 px.
|
||||||
pub width: f32,
|
pub width: Length,
|
||||||
/// Fixed height of the pill in pixels. Defaults to 160.
|
/// Fixed height of the pill in logical units. Defaults to 160 px.
|
||||||
pub height: f32,
|
pub height: Length,
|
||||||
/// Callback invoked with the new value when the slider is tapped or
|
/// 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
|
/// dragged. `Arc` (not `Box`) so the layout pass can clone it into the
|
||||||
/// per-leaf handler snapshot for O(1) dispatch on input events.
|
/// per-leaf handler snapshot for O(1) dispatch on input events.
|
||||||
@@ -93,8 +93,8 @@ impl<Msg: Clone> VSlider<Msg>
|
|||||||
Self
|
Self
|
||||||
{
|
{
|
||||||
value: value.clamp( 0.0, 1.0 ),
|
value: value.clamp( 0.0, 1.0 ),
|
||||||
width: theme::WIDTH,
|
width: Length::px( theme::WIDTH ),
|
||||||
height: theme::HEIGHT,
|
height: Length::px( theme::HEIGHT ),
|
||||||
on_change: None,
|
on_change: None,
|
||||||
track_surface: theme::SURFACE_TRACK,
|
track_surface: theme::SURFACE_TRACK,
|
||||||
fill_surface: theme::SURFACE_FILL,
|
fill_surface: theme::SURFACE_FILL,
|
||||||
@@ -117,12 +117,13 @@ impl<Msg: Clone> VSlider<Msg>
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Override the fixed pill `(width, height)` in pixels. Both are clamped
|
/// Override the fixed pill `(width, height)` in logical units. Both are
|
||||||
/// to a minimum of `2.0` so a rounded pill can always be drawn.
|
/// clamped to a minimum of `2.0` after viewport-relative values resolve,
|
||||||
pub fn size( mut self, width: f32, height: f32 ) -> Self
|
/// 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.width = width.into();
|
||||||
self.height = height.max( 2.0 );
|
self.height = height.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,9 +136,13 @@ impl<Msg: Clone> VSlider<Msg>
|
|||||||
|
|
||||||
/// Return the preferred `(width, height)`. `max_width` is ignored — see
|
/// Return the preferred `(width, height)`. `max_width` is ignored — see
|
||||||
/// the type-level docs on intrinsic sizing.
|
/// 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`.
|
/// 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()
|
fn default_dimensions_are_the_theme_constants()
|
||||||
{
|
{
|
||||||
let s: VSlider<()> = vslider( 0.0 );
|
let s: VSlider<()> = vslider( 0.0 );
|
||||||
assert_eq!( s.width, theme::WIDTH );
|
assert_eq!( s.width, Length::px( theme::WIDTH ) );
|
||||||
assert_eq!( s.height, theme::HEIGHT );
|
assert_eq!( s.height, Length::px( theme::HEIGHT ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ test ]
|
#[ test ]
|
||||||
|
|||||||
Reference in New Issue
Block a user