ltk: responsive padding/spacing and scrolling, expanded theme palette, and bundled Adwaita cursors
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

A mixed pass over the default theme and the layout/input core, plus the toolkit's own cursor set. Grouped by area below.
== Responsive sizing ==
Add `Length::dp( px )` — a "design pixel". It interprets `px` against a configurable reference vmin (default 412 px, the eydos mobile reference width) and returns `Vmin( px / reference * 100 ).clamp( px * 0.7, px * 1.5 )`, so a value authored against a mock-up scales with the surface without collapsing on tiny screens or ballooning on a 4K desktop. The reference is process-global, set via `set_design_reference()` and read via `design_reference()` (stored as f32 bits in an AtomicU32); both are re-exported from `lib.rs`.
Make container and grid insets relative. `Container`'s four padding fields become `Length` instead of `f32`; every setter (`padding`, `padding_h`, `padding_v`, `padding_top`/`right`/`bottom`/`left`) now takes `impl Into<Length>`, so existing `f32` call sites keep compiling via the `From<f32>` shim. The values are resolved against the viewport in `Container::preferred_size` and in the container draw path (`draw/layout.rs`). `WrapGrid`'s `spacing_x`, `spacing_y` and `padding` get the same treatment, with a `resolved( canvas )` helper funnelling the per-frame resolution and `grid()` seeding `Length::px` defaults. Container tests now compare against `Length::px( … )`.
== Scrolling ==
`Scroll::preferred_size` is now axis-aware. A horizontal-only scroll reports its child's natural height rather than claiming all remaining vertical space, so it no longer steals Y from its siblings when it sits inside a `Column`; vertical and both-axis scrolls keep the spacer-like `( max_width, 0.0 )`. `Column`'s space-distribution correspondingly treats a `Scroll` as a vertical space-claimer only when its axis allows Y.
Disambiguate nested scroll viewports by direction. On press the gesture state now collects every scroll viewport under the point (`scroll_candidates`, innermost first) instead of committing to one; on the first 8 px of motion it locks onto the candidate whose axis matches the dominant direction (`scroll_locked`), so a horizontal scroller nested inside a vertical list no longer grabs the wrong axis. The pointer scroll hit test is aligned to the same innermost-first ordering.
== Theme palette ==
`themes/default/theme.json` gains named colours (green / green-deep, yellow, orange / orange-deep, pink / pink-soft, sky-deep, error / error-soft, neutral-tertiary) and new semantic slots in both light and dark modes: `danger`, `text-tertiary`, `accept`, `chip` / `chip-active` / `chip-active-fg`, and `avatar-1` … `avatar-9`.
== Cursors ==
Bundle GNOME's Adwaita cursor theme — the cursors GNOME Shell uses — into `themes/default/cursors/` so a Wayland compositor can draw consistent, complete pointers for ltk applications without the toolkit rasterising cursors itself and without depending on adwaita-icon-theme being installed on the target. The cursors are copied verbatim in XCursor binary format: 35 image files, one per CSS/freedesktop cursor name (default, text, pointer, *-resize, …), plus 27 customary X11 alias symlinks (arrow → default, hand2 → pointer, …); a sibling `cursor.theme` makes the tree a valid XCursor theme. The existing `ltk-theme-default.install` copies `themes/default` recursively, so the directory ships with no packaging change. Applications keep declaring a `CursorShape` per widget over `wp_cursor_shape_v1`; the compositor resolves it against the active theme's `cursors/` directory by name, and the set covers all 34 `CursorShape` variants.
Document the set in `themes/default/cursors/README.md` (what it is, the XCursor layout, the full shape list, how the compositor consumes it, guidance for forks) and `themes/default/cursors/LICENSE.md` (attribution and licence options, modelled on the icons catalogue LICENSE). `lib.rs` lists the cursors in its third-party-assets section.
Close out licensing in `debian/copyright`: a `Files: themes/default/cursors/*` paragraph records the upstream dual offer (CC-BY-SA-3.0 or LGPL-3, and CC-BY-SA-4.0 for the newer assets) attributed to the GNOME Project, with standalone CC-BY-SA-3.0, CC-BY-SA-4.0 and LGPL-3 paragraphs (summary-plus-canonical-URL for the CC licences, matching the existing CC-BY-4.0 entry; LGPL-3 referencing /usr/share/common-licenses/LGPL-3). The files are unmodified from upstream, so there is nothing to declare under the ShareAlike "indicate if changes were made" clause.
Add `tests/cursor_assets.rs`: every `CursorShape` name resolves to a valid XCursor file (Xcur magic, following symlinks), `cursor.theme` is present, no entry is a dangling symlink, and the expected-name list stays in sync with the enum's 34 variants.
This commit is contained in:
2026-05-28 23:11:14 +02:00
parent 3d8523533c
commit 9ca3b60f3a
78 changed files with 570 additions and 102 deletions

View File

@@ -195,12 +195,18 @@ pub( crate ) fn layout_and_draw<Msg: Clone>(
{
canvas.stroke_rect( rect, color, width, c.corners );
}
let vp = canvas.viewport_logical();
let em = crate::types::Length::EM_BASE_DEFAULT;
let pad_l = c.pad_left.resolve( vp, em );
let pad_r = c.pad_right.resolve( vp, em );
let pad_t = c.pad_top.resolve( vp, em );
let pad_b = c.pad_bottom.resolve( vp, em );
let inner = crate::types::Rect
{
x: rect.x + c.pad_left,
y: rect.y + c.pad_top,
width: ( rect.width - c.pad_left - c.pad_right ).max( 0.0 ),
height: ( rect.height - c.pad_top - c.pad_bottom ).max( 0.0 ),
x: rect.x + pad_l,
y: rect.y + pad_t,
width: ( rect.width - pad_l - pad_r ).max( 0.0 ),
height: ( rect.height - pad_t - pad_b ).max( 0.0 ),
};
let result = layout_and_draw::<Msg>( c.child.as_ref(), canvas, inner, ctx, flat_idx );

View File

@@ -68,6 +68,10 @@ pub struct GestureState<Msg: Clone>
/// Index of the Scroll viewport that owns the current gesture, if
/// the press landed inside one.
pub scrolling_widget: Option<( usize, crate::widget::scroll::ScrollAxis )>,
/// Scroll viewports containing the press, innermost first.
pub scroll_candidates: Vec<( usize, crate::widget::scroll::ScrollAxis )>,
/// `true` once the first 8 px of motion has pinned `scrolling_widget` to a definite axis.
pub scroll_locked: bool,
/// Scroll-viewport drag exceeded the 8 px start tolerance — the
/// release will be consumed as a scroll instead of a tap.
pub scroll_drag_started: bool,
@@ -136,6 +140,8 @@ impl<Msg: Clone> GestureState<Msg>
start: None,
pressed_idx: None,
scrolling_widget: None,
scroll_candidates: Vec::new(),
scroll_locked: false,
scroll_drag_started: false,
horizontal_drag_started: false,
vertical_drag_started: false,
@@ -175,9 +181,12 @@ impl<Msg: Clone> GestureState<Msg>
}).unwrap_or( ( None, None ) );
self.start = Some( pos );
self.scrolling_widget = scroll_rects.iter().rev()
.find( |( r, _, _ )| r.contains( pos ) )
.map( |( _, idx, ax )| ( *idx, *ax ) );
self.scroll_candidates = scroll_rects.iter()
.filter( |( r, _, _ )| r.contains( pos ) )
.map( |( _, idx, ax )| ( *idx, *ax ) )
.collect();
self.scrolling_widget = self.scroll_candidates.first().copied();
self.scroll_locked = false;
self.scroll_drag_started = false;
self.horizontal_drag_started = false;
self.vertical_drag_started = false;
@@ -289,19 +298,30 @@ impl<Msg: Clone> GestureState<Msg>
return MoveOutcome::Idle;
}
// Scroll viewport drag: mutate offset in place and advance the
// gesture origin so the next delta is frame-to-frame, not
// press-to-now (otherwise the first 8 px trip the threshold
// and the entire scroll gets absorbed into one delta). Both
// axes are routed independently; an axis the viewport does not
// allow is just ignored (delta still consumed by the gesture,
// so the swipe handler does not fight the scroll for it).
if let Some( ( scroll_idx, axis ) ) = self.scrolling_widget
if self.scrolling_widget.is_some()
{
if let Some( start ) = self.start
{
let dx = pos.x - start.x;
let dy = pos.y - start.y;
let dx = pos.x - start.x;
let dy = pos.y - start.y;
if !self.scroll_locked
{
if dx.abs() <= 8.0 && dy.abs() <= 8.0
{
return MoveOutcome::Idle;
}
let prefer_x = dx.abs() > dy.abs();
if let Some( c ) = self.scroll_candidates.iter()
.find( |( _, ax )| if prefer_x { ax.allows_x() } else { ax.allows_y() } )
.copied()
{
self.scrolling_widget = Some( c );
}
self.scroll_locked = true;
}
let ( scroll_idx, axis ) = self.scrolling_widget.unwrap();
let entry = scroll_offsets.entry( scroll_idx ).or_insert( ( 0.0, 0.0 ) );
if axis.allows_x() { entry.0 = ( entry.0 - dx ).max( 0.0 ); }
if axis.allows_y() { entry.1 = ( entry.1 - dy ).max( 0.0 ); }

View File

@@ -33,7 +33,7 @@ impl<A: App> AppData<A>
let scroll_hit =
{
let ss = self.surface( focus );
ss.scroll_rects.iter().rev()
ss.scroll_rects.iter()
.find( |( r, _, _ )| r.contains( pos ) )
.map( |( _, idx, ax )| ( *idx, *ax ) )
};

View File

@@ -224,21 +224,19 @@ impl<Msg: Clone> Column<Msg>
let pad = self.resolved_padding( canvas );
let spacing = self.resolved_spacing( canvas );
// Flexible spacers and Scroll widgets claim remaining vertical space.
// Fixed-height spacers behave like normal fixed-size children.
let total_weight: u32 = self.children.iter()
.map( |c| match c
{
Element::Spacer( s ) if s.resolved_height( canvas ).is_none() => s.weight,
Element::Scroll( _ ) => 1,
_ => 0,
Element::Scroll( s ) if s.axis.allows_y() => 1,
_ => 0,
} )
.sum();
let fixed_h: f32 = self.children.iter()
.map( |c|
{
if matches!( c, Element::Scroll( _ ) )
if matches!( c, Element::Scroll( s ) if s.axis.allows_y() )
{
0.0
} else if let Element::Spacer( s ) = c {
@@ -282,7 +280,7 @@ impl<Msg: Clone> Column<Msg>
};
( inner_w, h )
},
Element::Scroll( _ ) =>
Element::Scroll( s ) if s.axis.allows_y() =>
{
let h = if total_weight > 0
{

View File

@@ -2,7 +2,7 @@
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::render::Canvas;
use crate::types::Rect;
use crate::types::{ Length, Rect };
use crate::widget::Element;
/// A grid layout that wraps children into rows of a fixed column count.
@@ -35,12 +35,12 @@ pub struct WrapGrid<Msg: Clone>
pub children: Vec<Element<Msg>>,
/// Number of columns per row.
pub columns: usize,
/// Horizontal gap between cells (pixels).
pub spacing_x: f32,
/// Vertical gap between rows (pixels).
pub spacing_y: f32,
/// Padding on all sides (pixels).
pub padding: f32,
/// Horizontal gap between cells.
pub spacing_x: Length,
/// Vertical gap between rows.
pub spacing_y: Length,
/// Padding on all sides.
pub padding: Length,
/// When `true`, a partial last row is centred horizontally within
/// the grid's content rect instead of being left-aligned.
pub centre_last_row: bool,
@@ -56,31 +56,32 @@ impl<Msg: Clone> WrapGrid<Msg>
}
/// Set both horizontal and vertical gap between cells (default 8.0).
pub fn spacing( mut self, s: f32 ) -> Self
pub fn spacing( mut self, s: impl Into<Length> ) -> Self
{
let s = s.into();
self.spacing_x = s;
self.spacing_y = s;
self
}
/// Set only the horizontal gap between cells; leaves vertical spacing untouched.
pub fn spacing_x( mut self, s: f32 ) -> Self
pub fn spacing_x( mut self, s: impl Into<Length> ) -> Self
{
self.spacing_x = s;
self.spacing_x = s.into();
self
}
/// Set only the vertical gap between rows; leaves horizontal spacing untouched.
pub fn spacing_y( mut self, s: f32 ) -> Self
pub fn spacing_y( mut self, s: impl Into<Length> ) -> Self
{
self.spacing_y = s;
self.spacing_y = s.into();
self
}
/// Set the padding on all sides (default 0.0).
pub fn padding( mut self, p: f32 ) -> Self
pub fn padding( mut self, p: impl Into<Length> ) -> Self
{
self.padding = p;
self.padding = p.into();
self
}
@@ -92,6 +93,17 @@ impl<Msg: Clone> WrapGrid<Msg>
self
}
fn resolved( &self, canvas: &Canvas ) -> ( f32, f32, f32 )
{
let vp = canvas.viewport_logical();
let em = Length::EM_BASE_DEFAULT;
(
self.spacing_x.resolve( vp, em ),
self.spacing_y.resolve( vp, em ),
self.padding.resolve( vp, em ),
)
}
/// Compute the preferred size given an available width.
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
{
@@ -99,12 +111,13 @@ impl<Msg: Clone> WrapGrid<Msg>
{
return ( max_width, 0.0 );
}
let ( sx, sy, pad ) = self.resolved( canvas );
let cols = self.columns;
let inner_w = (max_width - self.padding * 2.0).max( 0.0 );
let cell_w = (inner_w - self.spacing_x * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
let inner_w = (max_width - pad * 2.0).max( 0.0 );
let cell_w = (inner_w - sx * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
let row_count = (self.children.len() + cols - 1) / cols;
let mut total_h = self.padding * 2.0;
let mut total_h = pad * 2.0;
for row in 0..row_count
{
let start = row * cols;
@@ -114,7 +127,7 @@ impl<Msg: Clone> WrapGrid<Msg>
.map( |c| c.preferred_size( cell_w, canvas ).1 )
.fold( 0.0_f32, f32::max );
total_h += row_h;
if row + 1 < row_count { total_h += self.spacing_y; }
if row + 1 < row_count { total_h += sy; }
}
( max_width, total_h )
}
@@ -126,11 +139,12 @@ impl<Msg: Clone> WrapGrid<Msg>
{
return Vec::new();
}
let ( sx, sy, pad ) = self.resolved( canvas );
let cols = self.columns;
let inner_w = (rect.width - self.padding * 2.0).max( 0.0 );
let cell_w = (inner_w - self.spacing_x * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
let x0 = rect.x + self.padding;
let mut y = rect.y + self.padding;
let inner_w = (rect.width - pad * 2.0).max( 0.0 );
let cell_w = (inner_w - sx * (cols as f32 - 1.0)).max( 0.0 ) / cols as f32;
let x0 = rect.x + pad;
let mut y = rect.y + pad;
let row_count = (self.children.len() + cols - 1) / cols;
let mut out = Vec::with_capacity( self.children.len() );
@@ -148,16 +162,16 @@ impl<Msg: Clone> WrapGrid<Msg>
let row_offset = if self.centre_last_row && items_in_row < cols
{
let missing = (cols - items_in_row) as f32;
missing * (cell_w + self.spacing_x) / 2.0
missing * (cell_w + sx) / 2.0
} else { 0.0 };
for col in 0..items_in_row
{
let x = x0 + row_offset + col as f32 * (cell_w + self.spacing_x);
let x = x0 + row_offset + col as f32 * (cell_w + sx);
let crect = Rect { x, y, width: cell_w, height: row_h };
out.push( ( crect, start + col ) );
}
y += row_h + self.spacing_y;
y += row_h + sy;
}
out
}
@@ -396,9 +410,9 @@ pub fn grid<Msg: Clone>( columns: usize ) -> WrapGrid<Msg>
{
children: Vec::new(),
columns,
spacing_x: 8.0,
spacing_y: 8.0,
padding: 0.0,
spacing_x: Length::px( 8.0 ),
spacing_y: Length::px( 8.0 ),
padding: Length::px( 0.0 ),
centre_last_row: false,
}
}

View File

@@ -159,6 +159,16 @@
//! - **Sora Regular** (`src/theme/fallback/Sora-Regular.otf`) — the
//! embedded font fallback, [SIL OFL 1.1](https://scripts.sil.org/OFL),
//! © The Sora Project Authors, Jonathan Barnbrook, Julián Moncada.
//! - **Pointer cursors** under `themes/default/cursors/` — GNOME's
//! *Adwaita* cursor theme (the cursors GNOME Shell ships), bundled
//! verbatim, © the GNOME Project. Offered upstream under
//! [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) *or*
//! [LGPL 3](https://www.gnu.org/licenses/lgpl-3.0.html), and
//! [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) for
//! the newer assets; any one option satisfies the licence. See
//! `themes/default/cursors/README.md` (what the set is and how it is
//! used) and `themes/default/cursors/LICENSE.md` (attribution).
//! Upstream: <https://gitlab.gnome.org/GNOME/adwaita-icon-theme>.
//!
//! The remaining artwork in the default theme (wallpapers, lockscreens,
//! launcher logo, brand-mark variants, per-application icons) is
@@ -230,6 +240,7 @@ pub use gles_render::{ BorrowedGlesTexture, GlesVersion };
pub use render::is_software_render;
pub use wallpaper::{ WallpaperBundle, ImageData };
pub use types::{ Color, Corners, CursorShape, Length, LengthBase, Point, Rect, Size, WidgetId };
pub use types::{ design_reference, set_design_reference };
pub use widget::{ Element, button, icon_button, text_edit, image as img_widget, text, container };
pub use widget::button::ButtonVariant;
pub use widget::slider::{ Slider, slider, SliderAxis };

View File

@@ -24,6 +24,8 @@
//! `ltk::Rect`, …) so application code rarely needs the `ltk::types::`
//! prefix.
use std::sync::atomic::{ AtomicU32, Ordering };
/// An RGBA color with floating-point channels in the range `[0.0, 1.0]`.
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub struct Color
@@ -524,6 +526,17 @@ impl Length
pub const fn vmax( v: f32 ) -> Self { Self::from_base( LengthBase::Vmax( v ) ) }
pub const fn em( v: f32 ) -> Self { Self::from_base( LengthBase::Em( v ) ) }
/// "Design pixel": `px` interpreted at the reference vmin set via
/// [`set_design_reference`] (defaults to 412 px — the eydos mobile
/// reference width). The result is a `Vmin` value clamped to
/// `[px * 0.7, px * 1.5]`, so the layout scales with the screen
/// without collapsing on tiny surfaces or ballooning on 4K.
pub fn dp( px: f32 ) -> Self
{
let r = design_reference();
Length::vmin( px / r * 100.0 ).clamp( px * 0.7, px * 1.5 )
}
/// Resolve to a concrete logical-pixel value given a viewport and an
/// `em_base` (the root font size that `Em` is a fraction of).
pub fn resolve( &self, viewport: ( f32, f32 ), em_base: f32 ) -> f32
@@ -573,6 +586,23 @@ impl Length
}
}
static DESIGN_REFERENCE_BITS: AtomicU32 = AtomicU32::new( 412.0_f32.to_bits() );
/// Set the reference vmin width that [`Length::dp`] interprets `px` against.
/// Call once at startup (e.g. before [`crate::run`]) to align the design
/// scale to the surface mock-up the app was designed for.
pub fn set_design_reference( reference_vmin: f32 )
{
DESIGN_REFERENCE_BITS.store( reference_vmin.to_bits(), Ordering::Relaxed );
}
/// Current value used by [`Length::dp`] — the px width at which `dp(n)`
/// resolves to `n` logical pixels.
pub fn design_reference() -> f32
{
f32::from_bits( DESIGN_REFERENCE_BITS.load( Ordering::Relaxed ) )
}
impl From<f32> for Length
{
fn from( v: f32 ) -> Self { Length::px( v ) }

View File

@@ -2,7 +2,7 @@
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::theme::Paint;
use crate::types::{ Color, Corners };
use crate::types::{ Color, Corners, Length };
use crate::render::Canvas;
use super::Element;
@@ -72,15 +72,16 @@ pub struct Container<Msg: Clone>
/// left edge, …) without hitting the renderer with an offset
/// trick.
pub corners: Corners,
/// Padding on the top edge in logical px — gap between the
/// container's top boundary and its child.
pub pad_top: f32,
/// Padding on the right edge in logical px.
pub pad_right: f32,
/// Padding on the bottom edge in logical px.
pub pad_bottom: f32,
/// Padding on the left edge in logical px.
pub pad_left: f32,
/// Padding on the top edge — gap between the container's top boundary
/// and its child. Stored as a [`Length`] so it can scale with the
/// viewport via [`Length::dp`] / [`Length::vmin`].
pub pad_top: Length,
/// Padding on the right edge.
pub pad_right: Length,
/// Padding on the bottom edge.
pub pad_bottom: Length,
/// Padding on the left edge.
pub pad_left: Length,
pub opacity: f32,
/// Optional `( color, width_px )` border stroke painted around the
/// container's rounded rectangle, after the fill / surface and
@@ -109,10 +110,10 @@ impl<Msg: Clone> Container<Msg>
background: None,
surface: None,
corners: Corners::ZERO,
pad_top: 0.0,
pad_right: 0.0,
pad_bottom: 0.0,
pad_left: 0.0,
pad_top: Length::px( 0.0 ),
pad_right: Length::px( 0.0 ),
pad_bottom: Length::px( 0.0 ),
pad_left: Length::px( 0.0 ),
opacity: 1.0,
border: None,
max_width: None,
@@ -197,8 +198,9 @@ impl<Msg: Clone> Container<Msg>
/// edges, so calling this first and then a per-edge setter is the
/// idiomatic way to express "uniform padding except for one
/// edge".
pub fn padding( mut self, p: f32 ) -> Self
pub fn padding( mut self, p: impl Into<Length> ) -> Self
{
let p = p.into();
self.pad_top = p;
self.pad_right = p;
self.pad_bottom = p;
@@ -207,16 +209,18 @@ impl<Msg: Clone> Container<Msg>
}
/// Set horizontal padding (left + right each).
pub fn padding_h( mut self, p: f32 ) -> Self
pub fn padding_h( mut self, p: impl Into<Length> ) -> Self
{
let p = p.into();
self.pad_left = p;
self.pad_right = p;
self
}
/// Set vertical padding (top + bottom each).
pub fn padding_v( mut self, p: f32 ) -> Self
pub fn padding_v( mut self, p: impl Into<Length> ) -> Self
{
let p = p.into();
self.pad_top = p;
self.pad_bottom = p;
self
@@ -225,30 +229,30 @@ impl<Msg: Clone> Container<Msg>
/// Set the top edge padding only. Pairs with
/// [`padding_bottom`](Self::padding_bottom) for asymmetric
/// vertical insets.
pub fn padding_top( mut self, p: f32 ) -> Self
pub fn padding_top( mut self, p: impl Into<Length> ) -> Self
{
self.pad_top = p;
self.pad_top = p.into();
self
}
/// Set the right edge padding only.
pub fn padding_right( mut self, p: f32 ) -> Self
pub fn padding_right( mut self, p: impl Into<Length> ) -> Self
{
self.pad_right = p;
self.pad_right = p.into();
self
}
/// Set the bottom edge padding only.
pub fn padding_bottom( mut self, p: f32 ) -> Self
pub fn padding_bottom( mut self, p: impl Into<Length> ) -> Self
{
self.pad_bottom = p;
self.pad_bottom = p.into();
self
}
/// Set the left edge padding only.
pub fn padding_left( mut self, p: f32 ) -> Self
pub fn padding_left( mut self, p: impl Into<Length> ) -> Self
{
self.pad_left = p;
self.pad_left = p.into();
self
}
@@ -271,9 +275,15 @@ 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 em = Length::EM_BASE_DEFAULT;
let pad_l = self.pad_left.resolve( vp, em );
let pad_r = self.pad_right.resolve( vp, em );
let pad_t = self.pad_top.resolve( vp, em );
let pad_b = self.pad_bottom.resolve( vp, em );
let avail = self.max_width.map( |m| max_width.min( m ) ).unwrap_or( max_width );
let pad_x = self.pad_left + self.pad_right;
let pad_y = self.pad_top + self.pad_bottom;
let pad_x = pad_l + pad_r;
let pad_y = pad_t + pad_b;
let inner_w = ( avail - pad_x ).max( 0.0 );
let ( cw, ch ) = self.child.preferred_size( inner_w, canvas );
( cw + pad_x, ch + pad_y )

View File

@@ -3,6 +3,7 @@
use super::*;
use crate::layout::spacer::spacer;
use crate::types::Length;
#[ test ]
fn default_no_background()
@@ -15,30 +16,30 @@ fn default_no_background()
fn padding_sets_all_four_sides()
{
let c = container::<()>( spacer() ).padding( 10.0 );
assert_eq!( c.pad_top, 10.0 );
assert_eq!( c.pad_right, 10.0 );
assert_eq!( c.pad_bottom, 10.0 );
assert_eq!( c.pad_left, 10.0 );
assert_eq!( c.pad_top, Length::px( 10.0 ) );
assert_eq!( c.pad_right, Length::px( 10.0 ) );
assert_eq!( c.pad_bottom, Length::px( 10.0 ) );
assert_eq!( c.pad_left, Length::px( 10.0 ) );
}
#[ test ]
fn padding_h_only_touches_left_and_right()
{
let c = container::<()>( spacer() ).padding_h( 8.0 );
assert_eq!( c.pad_left, 8.0 );
assert_eq!( c.pad_right, 8.0 );
assert_eq!( c.pad_top, 0.0 );
assert_eq!( c.pad_bottom, 0.0 );
assert_eq!( c.pad_left, Length::px( 8.0 ) );
assert_eq!( c.pad_right, Length::px( 8.0 ) );
assert_eq!( c.pad_top, Length::px( 0.0 ) );
assert_eq!( c.pad_bottom, Length::px( 0.0 ) );
}
#[ test ]
fn padding_v_only_touches_top_and_bottom()
{
let c = container::<()>( spacer() ).padding_v( 6.0 );
assert_eq!( c.pad_top, 6.0 );
assert_eq!( c.pad_bottom, 6.0 );
assert_eq!( c.pad_left, 0.0 );
assert_eq!( c.pad_right, 0.0 );
assert_eq!( c.pad_top, Length::px( 6.0 ) );
assert_eq!( c.pad_bottom, Length::px( 6.0 ) );
assert_eq!( c.pad_left, Length::px( 0.0 ) );
assert_eq!( c.pad_right, Length::px( 0.0 ) );
}
#[ test ]
@@ -48,10 +49,10 @@ fn per_edge_overrides_uniform_padding()
let c = container::<()>( spacer() )
.padding( 12.0 )
.padding_bottom( 22.0 );
assert_eq!( c.pad_top, 12.0 );
assert_eq!( c.pad_right, 12.0 );
assert_eq!( c.pad_bottom, 22.0 );
assert_eq!( c.pad_left, 12.0 );
assert_eq!( c.pad_top, Length::px( 12.0 ) );
assert_eq!( c.pad_right, Length::px( 12.0 ) );
assert_eq!( c.pad_bottom, Length::px( 22.0 ) );
assert_eq!( c.pad_left, Length::px( 12.0 ) );
}
#[ test ]

View File

@@ -93,13 +93,27 @@ impl<Msg: Clone> Scroll<Msg>
self
}
/// Returns `(max_width, 0.0)` — the Scroll node claims all remaining space in
/// the parent layout, exactly like a [`Spacer`](crate::layout::spacer::Spacer).
/// The actual viewport size is determined at render time from the rect the
/// parent assigns.
pub fn preferred_size( &self, max_width: f32, _canvas: &Canvas ) -> (f32, f32)
/// Preferred size — axis-aware.
///
/// - **Vertical or both**: returns `(max_width, 0.0)`. The Scroll node
/// claims all remaining space in the parent layout, exactly like a
/// [`Spacer`](crate::layout::spacer::Spacer). The actual viewport size
/// is determined at render time from the rect the parent assigns.
/// - **Horizontal-only**: claims `max_width` but reports the child's
/// natural height. A horizontal scroll has an intrinsic height (the
/// row of items it clips) and must not steal Y space from its siblings
/// when it sits inside a [`Column`](crate::layout::column::Column).
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
{
( max_width, 0.0 )
match self.axis
{
ScrollAxis::Horizontal =>
{
let ( _, h ) = self.child.preferred_size( max_width, canvas );
( max_width, h )
}
ScrollAxis::Vertical | ScrollAxis::Both => ( max_width, 0.0 ),
}
}
/// No-op — rendering is handled entirely by `layout_and_draw` in `draw.rs`.