ltk: subsurface slides over overlays, axis-locked swipes, physical-space layout, touch reset on resume
Subsurfaces can now be parented to an overlay surface, not just the main surface. `SubsurfaceSpec` gains `parent: SubsurfaceParent { Main, Overlay(id) }` so a sliding panel can ride above app windows the way an overlay panel does, and an optional `gpu: bool` so content that uses the `surface-panel` backdrop-filter glass (a GLES-only pass) keeps it while sliding instead of dropping to the software rasteriser. `reconcile_subsurfaces` resolves each spec's parent independently — skipping an `Overlay` parent that is absent, unconfigured, or zero-sized — tracks the rastered size on the slot, and commits each touched parent once per frame. The ~14 GLES shader programs are compiled once into a shared `AppData::subsurface_gles_canvas` reused across every subsurface, so a lazily re-created sliding panel never recompiles them (hundreds of ms on a mobile GPU).
Vertical and horizontal swipes are now mutually exclusive: a gesture locks onto its dominant axis within the first 8 px of travel (new `SwipeAxis`) and ignores the perpendicular axis for the rest of the gesture, so a vertical swipe that drifts sideways no longer also drives the pager (and vice-versa). The upward swipe progress is no longer clamped at 1.0 — follow-the-finger panels can keep tracking the finger past the commit threshold — and a release below threshold delivers a final `progress = 0.0` cancellation pulse. A vertical swipe also no longer re-rasters the full-screen main surface on every motion event: only the overlays it drives are refreshed, while the horizontal pager (which does move the main surface) still redraws it. Re-rastering the main on every frame of a vertical drag was wasted work that stalled the loop and made the gesture feel laggy to start on a slow GPU.
Layout-affecting `Length` values (widths, paddings, gaps, widget sizes, including `Vw` / `Vh`) now resolve against the physical viewport via the new `Canvas::viewport_layout()` — the space the layout tree is actually computed in — so a `Vw(100)` fills the surface on a HiDPI (scale 2) output instead of covering half of it. Font sizes are unchanged: they still resolve against the logical viewport and are scaled at raster time. Switched column, row, spacer, wrap_grid, container, image, and vslider over to it; `img_widget` and `vslider` now document `Length::vw` / `vh` sizing and the showcase example demonstrates a viewport-relative image.
Touch gesture state is reset when the touch capability is added or removed — suspend / resume on devices that power the touchscreen down. A yanked capability never delivers the pending `up` / `cancel`, so the shared `reset_touch_state()` (also used by the `wl_touch.cancel` handler) drops the stranded `primary_touch_id` / slot state across the main surface and every overlay, keeping the first post-resume gesture clean. Also drops an accidental duplicate `on_scale_changed` call from the scale-change handler.
This commit is contained in:
@@ -275,7 +275,7 @@ impl<Msg: Clone> Container<Msg>
|
||||
/// Return the preferred `(width, height)` accounting for padding.
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
|
||||
{
|
||||
let vp = canvas.viewport_logical();
|
||||
let vp = canvas.viewport_layout();
|
||||
let em = Length::EM_BASE_DEFAULT;
|
||||
let pad_l = self.pad_left.resolve( vp, em );
|
||||
let pad_r = self.pad_right.resolve( vp, em );
|
||||
|
||||
@@ -16,10 +16,13 @@ use crate::render::Canvas;
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use ltk::{ img_widget, Element };
|
||||
/// # use ltk::{ img_widget, Element, Length };
|
||||
/// # #[ derive( Clone ) ] enum Msg {}
|
||||
/// # fn _ex( rgba_bytes: Arc<Vec<u8>>, width: u32, height: u32 ) -> Element<Msg> {
|
||||
/// // Display at 40 % of the viewport width by 20 % of its height instead of
|
||||
/// // the source's intrinsic pixel size — scales across screens with no tweaks.
|
||||
/// img_widget( rgba_bytes, width, height )
|
||||
/// .size( Length::vw( 40.0 ), Length::vh( 20.0 ) )
|
||||
/// .opacity( 0.8 )
|
||||
/// .into()
|
||||
/// # }
|
||||
@@ -34,7 +37,7 @@ 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 units.
|
||||
/// Optional explicit display size (Length values, resolved at layout time).
|
||||
pub display_size: Option<( Length, Length )>,
|
||||
/// Opacity multiplier in `[0.0, 1.0]`. Default: `1.0`.
|
||||
pub opacity: f32,
|
||||
@@ -66,7 +69,8 @@ impl Image
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an explicit display size in logical units.
|
||||
/// Set an explicit display size. Accepts logical `f32` pixels or any
|
||||
/// [`Length`] variant (e.g. `Length::vw(13.0)` for 13 % of viewport width).
|
||||
pub fn size( mut self, width: impl Into<Length>, height: impl Into<Length> ) -> Self
|
||||
{
|
||||
self.display_size = Some( ( width.into(), height.into() ) );
|
||||
@@ -81,15 +85,16 @@ impl Image
|
||||
}
|
||||
|
||||
/// Return the preferred `(width, height)` given available `max_width`.
|
||||
/// `canvas` is used to resolve viewport-relative [`Length`] values.
|
||||
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
if let Some( ( width, height ) ) = self.display_size
|
||||
if let Some( ( w, h ) ) = &self.display_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 ),
|
||||
);
|
||||
let vp = canvas.viewport_layout();
|
||||
let em = Length::EM_BASE_DEFAULT;
|
||||
let rw = w.resolve( vp, em ).max( 0.0 );
|
||||
let rh = h.resolve( vp, em ).max( 0.0 );
|
||||
return ( rw, rh );
|
||||
}
|
||||
if self.cover
|
||||
{
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use super::*;
|
||||
use crate::render::Canvas;
|
||||
|
||||
fn solid_rgba( w: u32, h: u32 ) -> Arc<Vec<u8>>
|
||||
{
|
||||
Arc::new( vec![ 255u8; ( w * h * 4 ) as usize ] )
|
||||
}
|
||||
|
||||
fn dummy_canvas() -> Canvas
|
||||
{
|
||||
Canvas::new( 800, 600 )
|
||||
}
|
||||
|
||||
// ── builders / defaults ───────────────────────────────────────────────────
|
||||
|
||||
#[ test ]
|
||||
@@ -57,8 +63,7 @@ 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 canvas = Canvas::new( 480, 900 );
|
||||
let ( w, h ) = img.preferred_size( 400.0, &canvas );
|
||||
let ( w, h ) = img.preferred_size( 400.0, &dummy_canvas() );
|
||||
assert_eq!( w, 400.0 );
|
||||
assert_eq!( h, 200.0 );
|
||||
}
|
||||
@@ -67,8 +72,7 @@ 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 );
|
||||
let canvas = Canvas::new( 480, 900 );
|
||||
assert_eq!( img.preferred_size( 1000.0, &canvas ), ( 50.0, 25.0 ) );
|
||||
assert_eq!( img.preferred_size( 1000.0, &dummy_canvas() ), ( 50.0, 25.0 ) );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
@@ -84,8 +88,7 @@ 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 canvas = Canvas::new( 480, 900 );
|
||||
let ( w, h ) = img.preferred_size( 300.0, &canvas );
|
||||
let ( w, h ) = img.preferred_size( 300.0, &dummy_canvas() );
|
||||
assert_eq!( w, 300.0 );
|
||||
assert_eq!( h, 150.0 );
|
||||
}
|
||||
@@ -98,8 +101,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();
|
||||
let canvas = Canvas::new( 480, 900 );
|
||||
assert_eq!( normal.preferred_size( 200.0, &canvas ), covered.preferred_size( 200.0, &canvas ) );
|
||||
let c = dummy_canvas();
|
||||
assert_eq!( normal.preferred_size( 200.0, &c ), covered.preferred_size( 200.0, &c ) );
|
||||
}
|
||||
|
||||
// ── Arc sharing ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -65,9 +65,9 @@ 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 logical units. Defaults to 56 px.
|
||||
/// Width of the pill. Defaults to 56 px; accepts any [`Length`].
|
||||
pub width: Length,
|
||||
/// Fixed height of the pill in logical units. Defaults to 160 px.
|
||||
/// Height of the pill. Defaults to 160 px; accepts any [`Length`].
|
||||
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
|
||||
@@ -117,9 +117,10 @@ impl<Msg: Clone> VSlider<Msg>
|
||||
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.
|
||||
/// Override the pill size. Accepts logical `f32` pixels or any [`Length`]
|
||||
/// variant (e.g. `Length::vw(16.0)` for 16 % of the viewport width). 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.into();
|
||||
@@ -138,11 +139,11 @@ impl<Msg: Clone> VSlider<Msg>
|
||||
/// the type-level docs on intrinsic sizing.
|
||||
pub fn preferred_size( &self, _max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
{
|
||||
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 ),
|
||||
)
|
||||
let vp = canvas.viewport_layout();
|
||||
let em = Length::EM_BASE_DEFAULT;
|
||||
let w = self.width.resolve( vp, em ).max( 2.0 );
|
||||
let h = self.height.resolve( vp, em ).max( 2.0 );
|
||||
( w, h )
|
||||
}
|
||||
|
||||
/// Compute the value `[0.0, 1.0]` from a tap/drag y position within `rect`.
|
||||
|
||||
Reference in New Issue
Block a user