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

@@ -0,0 +1,3 @@
[Icon Theme]
Name=Default
Comment=Liberux default cursor theme

View File

@@ -0,0 +1,57 @@
# Cursor theme licence
The pointer cursors in this directory are GNOME's **Adwaita** cursor
theme — the cursors GNOME Shell ships — bundled verbatim into the ltk
default theme. They are distributed by upstream under a choice of
licences:
- **Author:** the GNOME Project (Adwaita icon theme authors, 20022014)
- **Source:** <https://gitlab.gnome.org/GNOME/adwaita-icon-theme>
- **Licence:** **CC BY-SA 3.0 _or_ LGPL 3**, and CC BY-SA 4.0 for the
newer assets — see below.
The licence is a disjunction: redistributing the cursors under **any
one** of the offered options satisfies it. ltk passes the choice
through unchanged.
- CC BY-SA 3.0 — <https://creativecommons.org/licenses/by-sa/3.0/>
- CC BY-SA 4.0 — <https://creativecommons.org/licenses/by-sa/4.0/>
- LGPL 3 — <https://www.gnu.org/licenses/lgpl-3.0.html>
## Required attribution
When redistributing this directory (or a binary that embeds these
cursor files), credit must be given to the GNOME Project and a link to
the chosen licence must be reachable. The wording is flexible — any of
the following satisfies the obligation:
> Cursors by the [GNOME Project](https://www.gnome.org), Adwaita cursor
> theme, CC BY-SA 3.0 / LGPL 3.
> Pointer cursors: © the GNOME Project, Adwaita, distributed under
> CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/).
Upstream notes that, for attribution, "using 'GNOME Project' is enough".
The full Debian-style attribution lives in the top-level
[`debian/copyright`](../../../debian/copyright) of the `ltk` source
tree under the `Files: themes/default/cursors/*` paragraph.
## Modifications
The cursor files in this directory are **unmodified** copies of the
upstream Adwaita XCursor files (the alias symlinks are part of the
upstream theme). No content changes have been made, so there is nothing
to declare under the ShareAlike "indicate if changes were made" clause.
Anyone needing a pristine copy can also fetch it from the upstream
`adwaita-icon-theme` release.
## How licensing applies to the rest of the theme
The rest of the default theme is licensed separately: branding artwork,
app icons and the unknown-app fallback are original Liberux Labs work
under `LGPL-2.1-only`, and the catalogue glyphs under
`themes/default/icons/catalogue/` are Streamline's under CC BY 4.0 (see
that directory's `LICENSE.md`). Forks should preserve the split:
re-license your own artwork freely, but the contents of this `cursors/`
directory remain the GNOME Project's under the terms above.

View File

@@ -0,0 +1,73 @@
# Default theme cursors
This directory holds the pointer cursors for the ltk **default** theme.
They are GNOME's **Adwaita** cursors (what GNOME Shell uses), bundled
verbatim so the theme is self-contained and does not depend on
`adwaita-icon-theme` being installed on the target system.
Licence and attribution are documented separately in
[`LICENSE.md`](./LICENSE.md).
## Layout
Standard XCursor theme layout:
```
themes/default/
├── cursor.theme # XCursor theme manifest (Name=Default)
└── cursors/
├── default # one binary XCursor file per CSS cursor name
├── text
├── pointer
├── ew-resize
├── …
├── arrow -> default # customary X11 alias symlinks
└── hand2 -> pointer
```
- Each regular file is a binary XCursor image set (magic `Xcur`),
carrying one or more nominal sizes and, for the busy cursors, several
animation frames.
- The file **name is the CSS / freedesktop cursor name** (`default`,
`text`, `pointer`, `not-allowed`, `ns-resize`, …), which is exactly
the name a `wp_cursor_shape_v1` shape resolves to. The alias symlinks
(`arrow`, `hand2`, `bottom_left_corner`, …) are the legacy X11 names;
they are not required by the cursor-shape protocol but are kept so the
tree also works as a plain XCursor theme (e.g. for XWayland).
## Cursor set
The theme provides a file for every shape ltk can request — the 34
variants of [`ltk::CursorShape`](../../../src/types.rs), which mirror
`cursor_icon::CursorIcon` 1:1:
`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`.
`wait` and `progress` are animated (multiple frames with per-frame
delays). The `tests/cursor_assets.rs` integration test enforces that
every name above resolves to a valid XCursor file.
## How they are used
ltk does **not** rasterise cursors itself; as a Wayland client it
declares a shape per widget through `wp_cursor_shape_v1` and the
compositor draws it. The Liberux compositor (forge) resolves the
requested shape against the active theme's `cursors/` directory by CSS
name, picks the image whose nominal size is closest to `24px × output
scale`, and uploads it at the cursor's hotspot — so these files are what
the user actually sees under forge. A compositor that does not advertise
`wp_cursor_shape_v1`, or that uses a different cursor theme, will ignore
this directory.
## Forking / overriding
To ship different cursors in a derived theme, drop replacement XCursor
files (same CSS names) into the fork's `cursors/` directory. Keep
`cursor.theme` so the tree stays a valid XCursor theme, and update the
`Files: themes/default/cursors/*` paragraph in `debian/copyright` plus
this directory's `LICENSE.md` to match the new source and licence.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
default

View File

@@ -0,0 +1 @@
nwse-resize

View File

@@ -0,0 +1 @@
sw-resize

View File

@@ -0,0 +1 @@
se-resize

View File

@@ -0,0 +1 @@
s-resize

BIN
themes/default/cursors/cell Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
themes/default/cursors/copy Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
crosshair

View File

@@ -0,0 +1 @@
crosshair

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
crosshair

View File

@@ -0,0 +1 @@
default

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
nesw-resize

View File

@@ -0,0 +1 @@
all-resize

BIN
themes/default/cursors/grab Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
grab

View File

@@ -0,0 +1 @@
pointer

BIN
themes/default/cursors/help Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
default

View File

@@ -0,0 +1 @@
w-resize

1
themes/default/cursors/move Symbolic link
View File

@@ -0,0 +1 @@
default

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
help

View File

@@ -0,0 +1 @@
e-resize

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
ew-resize

View File

@@ -0,0 +1 @@
ns-resize

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
crosshair

BIN
themes/default/cursors/text Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
default

View File

@@ -0,0 +1 @@
nw-resize

View File

@@ -0,0 +1 @@
ne-resize

View File

@@ -0,0 +1 @@
n-resize

Binary file not shown.

Binary file not shown.

BIN
themes/default/cursors/wait Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
wait

View File

@@ -0,0 +1 @@
text

Binary file not shown.

Binary file not shown.

View File

@@ -37,7 +37,18 @@
"glass-elev": "#212121",
"ink": "#000000",
"cyan-deep": "#0091AD",
"teal-fill-end": "#00687C"
"teal-fill-end": "#00687C",
"green": "#00D344",
"green-deep": "#00B41A",
"yellow": "#FFD400",
"orange": "#FF8156",
"orange-deep": "#BD6403",
"pink": "#EF15FA",
"pink-soft": "#FFD7FF",
"sky-deep": "#1E8FD9",
"error": "#F00013",
"error-soft": "#FF6B6B",
"neutral-tertiary": "#807E88"
},
"gradients": {
@@ -111,6 +122,21 @@
"accent": { "type": "color", "value": "@cyan", "meta": { "semantic": "palette/accent" } },
"divider": { "type": "color", "value": "@navy/14", "meta": { "semantic": "palette/divider" } },
"icon": { "type": "color", "value": "@navy", "meta": { "semantic": "palette/icon" } },
"danger": { "type": "color", "value": "@error", "meta": { "semantic": "palette/danger" } },
"text-tertiary": { "type": "color", "value": "@neutral-tertiary", "meta": { "semantic": "palette/text_tertiary" } },
"accept": { "type": "color", "value": "@green-deep", "meta": { "semantic": "status/accept" } },
"chip": { "type": "color", "value": "@off-white", "meta": { "semantic": "palette/chip" } },
"chip-active": { "type": "color", "value": "@navy", "meta": { "semantic": "palette/chip_active" } },
"chip-active-fg": { "type": "color", "value": "@white", "meta": { "semantic": "palette/chip_active_fg" } },
"avatar-1": { "type": "color", "value": "@sky", "meta": { "semantic": "avatar/1" } },
"avatar-2": { "type": "color", "value": "@green", "meta": { "semantic": "avatar/2" } },
"avatar-3": { "type": "color", "value": "@orange", "meta": { "semantic": "avatar/3" } },
"avatar-4": { "type": "color", "value": "@pink", "meta": { "semantic": "avatar/4" } },
"avatar-5": { "type": "color", "value": "@yellow", "meta": { "semantic": "avatar/5" } },
"avatar-6": { "type": "color", "value": "@sky-deep", "meta": { "semantic": "avatar/6" } },
"avatar-7": { "type": "color", "value": "@cyan", "meta": { "semantic": "avatar/7" } },
"avatar-8": { "type": "color", "value": "@green-deep", "meta": { "semantic": "avatar/8" } },
"avatar-9": { "type": "color", "value": "@orange-deep", "meta": { "semantic": "avatar/9" } },
"shadows-glass": {
"type": "shadows",
@@ -225,6 +251,21 @@
"accent": { "type": "color", "value": "@cyan", "meta": { "semantic": "palette/accent" } },
"divider": { "type": "color", "value": "@white/14", "meta": { "semantic": "palette/divider" } },
"icon": { "type": "color", "value": "@white", "meta": { "semantic": "palette/icon" } },
"danger": { "type": "color", "value": "@error-soft", "meta": { "semantic": "palette/danger" } },
"text-tertiary": { "type": "color", "value": "@white/80", "meta": { "semantic": "palette/text_tertiary" } },
"accept": { "type": "color", "value": "@green", "meta": { "semantic": "status/accept" } },
"chip": { "type": "color", "value": "@white/14", "meta": { "semantic": "palette/chip" } },
"chip-active": { "type": "color", "value": "@cyan", "meta": { "semantic": "palette/chip_active" } },
"chip-active-fg": { "type": "color", "value": "@navy", "meta": { "semantic": "palette/chip_active_fg" } },
"avatar-1": { "type": "color", "value": "@sky", "meta": { "semantic": "avatar/1" } },
"avatar-2": { "type": "color", "value": "@green", "meta": { "semantic": "avatar/2" } },
"avatar-3": { "type": "color", "value": "@orange", "meta": { "semantic": "avatar/3" } },
"avatar-4": { "type": "color", "value": "@pink", "meta": { "semantic": "avatar/4" } },
"avatar-5": { "type": "color", "value": "@yellow", "meta": { "semantic": "avatar/5" } },
"avatar-6": { "type": "color", "value": "@sky-deep", "meta": { "semantic": "avatar/6" } },
"avatar-7": { "type": "color", "value": "@cyan", "meta": { "semantic": "avatar/7" } },
"avatar-8": { "type": "color", "value": "@green-deep", "meta": { "semantic": "avatar/8" } },
"avatar-9": { "type": "color", "value": "@orange-deep", "meta": { "semantic": "avatar/9" } },
"shadows-glass": {
"type": "shadows",