Files
ltk/tests/cursor_assets.rs
Pedro M. de Echanove Pasquin 9ca3b60f3a
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
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.
2026-05-28 23:11:14 +02:00

106 lines
2.8 KiB
Rust

use std::io::Read;
use std::path::{ Path, PathBuf };
/// Every shape `ltk::CursorShape` can request, by its CSS / freedesktop
/// cursor name. The default theme must ship an XCursor file (or alias
/// symlink) for each so the compositor never has to fall back. Keep in
/// sync with the `CursorShape` enum in `src/types.rs`.
const CURSOR_NAMES: &[ &str ] =
&[
"default", "context-menu", "help", "pointer", "progress", "wait",
"cell", "crosshair", "text", "vertical-text", "alias", "copy", "move",
"no-drop", "not-allowed", "grab", "grabbing", "e-resize", "n-resize",
"ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize",
"w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize",
"col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out",
];
fn theme_dir() -> PathBuf
{
PathBuf::from( env!( "CARGO_MANIFEST_DIR" ) ).join( "themes/default" )
}
fn cursors_dir() -> PathBuf
{
theme_dir().join( "cursors" )
}
/// True if `path` (following symlinks) is a binary XCursor file, i.e. it
/// opens and starts with the four magic bytes `Xcur`.
fn is_xcursor( path: &Path ) -> bool
{
let mut file = match std::fs::File::open( path )
{
Ok( f ) => f,
Err( _ ) => return false,
};
let mut magic = [ 0u8; 4 ];
file.read_exact( &mut magic ).is_ok() && &magic == b"Xcur"
}
#[ test ]
fn cursor_theme_manifest_present()
{
let manifest = theme_dir().join( "cursor.theme" );
let body = std::fs::read_to_string( &manifest )
.unwrap_or_else( |e| panic!( "reading {}: {e}", manifest.display() ) );
assert!
(
body.contains( "[Icon Theme]" ),
"cursor.theme is missing the [Icon Theme] header",
);
}
#[ test ]
fn every_cursor_shape_has_a_valid_xcursor_file()
{
let dir = cursors_dir();
let missing: Vec<&str> = CURSOR_NAMES
.iter()
.copied()
.filter( |name| !is_xcursor( &dir.join( name ) ) )
.collect();
assert!
(
missing.is_empty(),
"default theme has no valid XCursor file for: {missing:?}",
);
}
/// Packaging (`dh_install`) can in principle drop or break the alias
/// symlinks; a dangling link would make a cursor name unresolvable at
/// runtime. Every entry must resolve to an existing file.
#[ test ]
fn no_dangling_entries()
{
let dir = cursors_dir();
let entries = std::fs::read_dir( &dir )
.unwrap_or_else( |e| panic!( "reading {}: {e}", dir.display() ) );
let mut dangling = Vec::new();
for entry in entries
{
let path = entry.expect( "dir entry" ).path();
// `metadata` follows symlinks, so a broken link is an Err here.
if std::fs::metadata( &path ).is_err()
{
dangling.push( path );
}
}
assert!( dangling.is_empty(), "dangling cursor entries: {dangling:?}" );
}
/// Guard against the name list above drifting from the enum it mirrors.
#[ test ]
fn cursor_name_list_matches_shape_count()
{
assert_eq!
(
CURSOR_NAMES.len(),
34,
"CURSOR_NAMES is out of sync with ltk::CursorShape (34 variants)",
);
}