ltk: responsive padding/spacing and scrolling, expanded theme palette, and bundled Adwaita cursors
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:
@@ -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 )
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user