adbbc0248bfb0acb960f93abd29e310fe1702da4
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 1290f9400e |
row, button, widget: distribute a row's width shortfall instead of overflowing, elide button labels into the rect they are granted, icon_size takes a Length
Row deficit distribution (layout/row.rs). `Row` knew how to hand out leftover width but had no notion of a shortfall: `leftover` was floored at zero and every child was laid out at its preferred width, so a cluster wider than its rect simply overflowed — and symmetrically, because the no-spacer branch centres the block, which is why both end labels of a segmented control were clipped at once rather than only the trailing one. The shortfall now comes off the widest children first, by water filling: `width_cap` sorts the flexible widths and returns the largest per-child cap `c` for which `sum( min( w, c ) ) <= available`, so a long title absorbs the whole deficit while an icon button beside it keeps its size, and equal siblings — a segmented control — share it evenly. A uniform scale-down, which was the first attempt, was wrong precisely there: it thinned a header's back arrow along with the title it sat next to. Spacers and flex children stay out of the flexible set, so a pinned gap keeps the width it was given — an explicit spacer is a decision, not slack — and `Row::no_shrink()` opts a row out entirely, for strips deliberately wider than their viewport such as a carousel rail meant to be scrolled. `align_right` and the centring branch now measure the laid width rather than the preferred one, so a shrunken row is positioned against what it actually occupies. Button label elision (widget/button/mod.rs, widget/mod.rs, widget/list_item/mod.rs). Shrinking a row is only half the fix: a leaf handed less width than it asked for still painted its full string, so the deficit came out clipped instead of truncated. `draw_text_button` now elides the label against the rect the layout granted, less the horizontal padding, for all three variants. The truncation rule moves out of `ListItem`, where it was a private helper, into a single crate-internal `widget::elide` the two share. `Text` keeps its own inline copy for now: it measures through an optional font override that this signature does not carry, and folding the two together is a separate change. icon_size as a Length (widget/button/mod.rs). `Button::icon_size` was the one geometry setter that ignored the widget-scaling mode — it took a bare `f32`, pinned it, and used `0.0` as the "unset" sentinel. It now takes `impl Into<Length>` behind an `Option` and resolves through `Canvas::resolve_geom`, the way `height`, `width` and `font_size` already do. A bare number still means `Length::px`, so every existing call keeps its exact size; what it adds is `Length::widget( n )`, which follows the active mode the way stock icons do. Without it a back arrow pinned at 21 px sat next to a `list_item` chevron that fluid sizing had grown well past 21, and read as visibly smaller on the same row. |
|||
| 806dee5167 |
render, types, ci: resolution-time dp with per-canvas density, bounded GLES image cache, clippy gate, backend capability matrix
Length::dp no longer collapses to absolute pixels at construction: the design value travels in a new LengthBase::Dp variant and the density multiplication happens when the length is resolved. Previously dp( n ) baked in whatever density() returned at view-build time, so correctness across output changes depended on the view being rebuilt after set_density and in that order; now a density change is picked up by the very next paint with no reconstruction. Length::resolve keeps its signature (process density), and the new Length::resolve_with_density takes an explicit factor. dp becomes const in the bargain. Density also becomes overridable per canvas, the first step towards surface-local responsive state. SoftwareCanvas and GlesCanvas carry a density: Option<f32> analogous to the layout_viewport introduced for sub-canvas fluid resolution: None means "use the process global", Canvas::set_density pins a local factor, and sub-canvases inherit it. All canvas-routed resolution honours it — geom_px / font_px for stock-widget design pixels, and the new Canvas::resolve_geom / resolve_font for explicit Length values, which every widget now uses in place of the raw l.resolve( canvas.viewport_layout(), EM ) pattern (row, column, wrap_grid, spacer, container, separator, button, text, rich_text, text_edit, list_item, vslider, image, and the container draw path). Overlay sizing keeps resolving against the main surface with the global density, which is what it describes. New tests cover explicit-density resolution, resolution-time application, the local-over-global override and sub-canvas inheritance. The GLES image texture cache is now bounded. It was content-keyed but unbounded and never evicted, so a stream of distinct buffers — a photo carousel, video thumbnails — grew GPU memory for the lifetime of the canvas. The cache now tracks an estimated byte total (RGBA8, w × h × 4) against a 32 MiB budget and evicts least-recently-drawn textures on insert; the most recent entry is never evicted, so a single texture larger than the whole budget still draws and simply owns the cache until replaced. Drop-time cleanup is unchanged: drain deletes whatever the map holds. CI gains a Clippy step (workspace, all targets, test-support, -D warnings) sharing the build cache of the test job, with make clippy mirroring the invocation locally and CONTRIBUTING listing it. Run make clippy locally before pushing the first time — the gate has not seen the tree yet and pre-existing lints will fail CI until addressed. docs/backends.md formalises the software/GLES capability matrix that was previously scattered across per-method rustdoc: parity set (fills, strokes, text, images, paths, path clips), graceful degradations on software (flat-fill gradients, no shadows, no backdrop blur, hard bottom edge), GPU-only features (external textures), the shared Oklab-fallback limitation, and the cross-backend blit panic. Linked from README, onboarding and architecture's known-gaps list, which now states the parity gaps explicitly. The dp/density prose in architecture.md, lib.rs and the Length rustdoc is updated for resolution-time semantics and the per-canvas override. |
|||
| a7f953ca42 |
layout: adaptive grid and vertical flex; enforce the mechanical style rules
grid_min_cell( width ) adds the adaptive mode WrapGrid was missing: instead of a fixed column count, the count is derived at layout time from the available width so every cell is at least `width` wide (any Length, so a fluid threshold works; never fewer than one column), re-derived on every resize. Cells then share the width equally, and WrapGrid::max_columns( n ) caps the derived count so cells grow instead of multiplying on very wide surfaces — the cap only applies to the adaptive mode, grid( n ) keeps the fixed behaviour untouched. Four new layout tests cover width derivation, spacing accounting, the one-column floor and the cap; the row-wrap assertions check X positions rather than Y because zero-height spacer children produce zero-height rows. flex( child ) now distributes leftover height inside a Column, mirroring its leftover-width behaviour in a Row: flex children join the weight pool alongside weight-only spacers and y-scrolls, draw their child inside the allocated share at full inner width, and contribute zero to the column's natural height exactly like a row counts flex width. This closes the documented "vertical flex is not yet implemented" gap; the Flex rustdoc and the widgets.md entry now describe both axes. Add scripts/style-check.sh and a make stylecheck target, wired into CI: mechanical verification of the grep-checkable subset of code_style_guide.md — tab indentation and spaces inside attribute brackets — with comment lines skipped so prose may cite raw attribute syntax. To turn the check on green, the 82 unspaced attribute sites across 24 files (#[test], #[derive(...)], #[cfg(...)], #[allow(...)]) were normalized to the spaced form. CONTRIBUTING and the style guide reference the new target; brace placement and paren spacing remain review concerns. |
|||
| 16c04f4159 |
event_loop, text_edit, layout: focus-time cursor as an end-of-value sentinel, caret height tied to the text line, row align_top/fill_height modes
Focus-time cursor (event_loop/focus.rs): focusing a text input pinned the cursor to a concrete `value.len()` snapshot. When the value keeps growing after focus without the widget seeing keystrokes — crustace's launcher search field is fed over IPC while forge routes the type-to-search keys around Wayland — that snapshot goes stale, and the first key delivered normally afterwards inserts mid-string. The cursor is now seeded with the `usize::MAX` sentinel ("end of whatever value is rendered"), the same convention `select_on_focus` fields already rely on; every consumer (insert/delete, arrow keys, draw, hit-test, context-menu paste offset, a11y tree) clamps via `cursor.min( value.len() )`, so the cursor tracks external growth and collapses to a concrete position on the first real keystroke or click. Click placement is untouched — the pointer path writes its own hit-tested offset.
Caret height (widget/text_edit/draw.rs): the single-line caret spanned `rect.height - 16`, so a field taller than its text line (the launcher's borderless search pill) grew a caret about twice the glyph height. It now measures `font_size + 4`, vertically centered like the text — matching what the multiline caret already did.
Row alignment modes (layout/row.rs): `Row` gains `align_top()` (children pinned to the top edge instead of the default vertical centering) and `fill_height()` (every non-spacer child stretched to the row's inner height, the row itself still sized by its tallest child). Both exist for siblings whose natural heights differ by a few font-metric pixels — like the QS media card next to the wifi/bluetooth chip column — where equal-height layouts cannot be achieved by estimating text heights: `new_line_size` is font-dependent, so px arithmetic in the app always drifts. Containers paint their chrome over the full rect they receive and columns absorb the extra in weighted spacers, so a stretched card keeps its content anchored where its internal spacers put it.
|
|||
| ce893ac776 |
responsive fluid/physical scaling, widget-API stabilization, and perf guardrails
Responsive scaling. ltk now offers two first-class ways to size a UI so it adapts across screens, chosen per process via `WidgetScaling { Fluid, Physical }` (`set_widget_scaling` / `widget_scaling`, default `Fluid`). Fluid sizing (`Length::fluid( px )`) makes a design pixel a proportion of the surface's smaller side, calibrated against a reference width (`set_fluid_reference` / `fluid_reference`, 412 px default) and bounded by `FLUID_MIN` / `FLUID_MAX`; physical sizing (`Length::dp( px )`) is a constant-physical-size pixel scaled by display density (`set_density` / `density`). `Length` gains `orient( portrait, landscape )` — resolve one value in portrait, another in landscape — plus `widget( px )`, which picks fluid or dp per the active mode. Canvas exposes `geom_px` (geometry, resolved in physical layout space) and `font_px` (font size, bridging logical / physical per mode) so widgets and apps share one resolution path. Note the rename: `set_design_reference` / `design_reference` became `set_fluid_reference` / `fluid_reference`, and `Length::dp` changed meaning — the old surface-proportional behaviour now lives on `Length::fluid`.
Widgets. Every stock widget resolves its default geometry and font through the widget-scaling mode instead of frozen pixels, so a whole UI scales coherently without per-call units. New size builders where they were missing: `button` gains `font_size` / `height`, `text_edit` gains `height` / `font_size_fluid`, `separator` gains `pad_v`, and assorted widgets accept a `Length` where they previously took only `f32`.
Overlays. `OverlaySpec::size` is now `( Length, Length )` instead of `( u32, u32 )`, resolved against the main surface when the overlay is materialized, so overlays can scale with the display; `Length::px( … )` reproduces the old fixed sizing.
API stabilization (toward 1.0). Widget struct fields are now `pub( crate )` — they are configured through builders, not field access — except the value / state types apps genuinely read or construct (`Time`, `Date`, `ComboState`), which stay public. The internal `test_support` helpers move behind a `test-support` Cargo feature (off by default, so third-party builds never see them; ltk's own `make test` enables it). `Separator` drops its `0.0`-means-mode sentinel for `Option<Length>`, so an explicit `pad_v( 0.0 )` is a real flush divider distinct from the mode-following default.
Performance guardrails. Opt-in diagnostics via `LTK_PERF_WARN=1` warn about stuck animations, sustained software-render animation, and low `poll_interval`; software-rendered animation is capped near 30 Hz to spare CPU on machines that fall back off EGL. Apps can override the cap with `App::cap_software_animation`.
Docs and build. The two scaling modes are documented in README, onboarding and architecture, with the earlier gradient / backdrop doc drift cleaned up. The Makefile now ships the `locales/` directory into the packaged crate (fixing i18n keys rendering raw for downstreams), builds the new `responsive` example, and runs tests with `--features test-support`.
|
|||
| fb3552e9f7 |
Embedder primitives: placed-child clipping, offscreen RGBA readback, standalone text measurement
Three additions an embedder needs to drive ltk as the render backend for a retained, externally-owned widget tree, each kept general rather than tied to one consumer. Stack placed-child clipping. `Stack::push_placed_clipped(e, rect, clip)` places a child at an exact rect like `push_placed` but clips its subtree's drawing to `clip` (in the Stack's coordinate space) — Android's clipChildren: content that overflows, such as a scrolled list row reaching above the list or an inner card past a rounded bubble, is not painted. The Stack child tuple grows an 8th `Option<Rect>` for the clip, and `layout_and_draw` brackets a clipped child with `set_clip_rects`/restore around the recursion. The clip is shifted by the Stack's own origin to match the placed rect (which `Stack::layout` already offsets), so a Stack laid out at a non-zero origin clips in the right place rather than off by that origin. Offscreen RGBA readback. `Canvas::read_rgba_pixels(out)` reads any canvas into tightly packed straight-alpha RGBA8, top-left row first. Unlike `read_gles_rgba_pixels` it also serves the software backend, un-premultiplying its pixmap, so an offscreen software canvas (an embedder's scratch bitmap) can be read back into a straight-alpha buffer. `Canvas::is_software()` lets a caller branch on the backend — e.g. to honour a real path clip on software but only a bounding rect on GLES. Standalone text measurement. `measure_text(text, size)`, re-exported at the crate root, measures one line with the default UI font and the system fallback chain, returning `(width, line_height)` in pixels without a live `Canvas` — for an embedder's measure pass that must produce the same metrics the renderer will later use. It is backed by `system_fonts::primary_handle()`, a process-wide cached handle for the primary UI font (the same default a canvas loads, with the bundled-font fallback), which widens `render::helpers::load_default_font_bytes` to `pub(crate)`. |
|||
| b00cf460bb |
Mature ltk to host an externally-laid-out Android view tree
Add the primitives rustdroid needs to project an Android view hierarchy onto an ltk surface, all kept general rather than Android-specific. Canvas gains arbitrary vector path fill and stroke: `Canvas::fill_path` / `stroke_path` over a new `PathCmd` command list (MoveTo/LineTo/QuadTo/CubicTo/Close in surface coordinates). The software backend rasterises directly with tiny-skia; the GLES backend rasterises into a tiny-skia pixmap and blits it (CPU fallback, no GPU path shader). This is what renders an Android `Path`, a `VectorDrawable`, or a Lottie frame. `ExternalSource::Cpu` (and the `External::cpu` constructor) adds an immediate-mode CPU drawing closure, invoked once per frame with the canvas and the widget's laid-out rect, working on both the GLES and software backends. It hosts a custom `View.onDraw` straight onto the ltk canvas without a GL texture round-trip, unlike the existing `Texture` source which only renders on GLES. `Stack::push_placed` appends a child at an exact rect, bypassing alignment and intrinsic sizing. This lets a view tree whose geometry is computed elsewhere — Android's measure/layout pass, which yields an absolute rect per view — be projected onto a Stack in paint order. New `RichText` widget: wrapped paragraph text carrying a message per clickable link range, with the layout pass emitting one hit rect per link line so taps land on the link rather than the whole paragraph. It is the ltk side of an Android `Spanned` carrying `URLSpan` / `ClickableSpan`. gesture: only drive the horizontal pager once the axis locks horizontal `on_move` emitted horizontal swipe progress whenever `swipe_axis != Vertical`, which includes the pre-lock window where `swipe_axis` is still `None` (the first ~8 px of travel). But `horizontal_drag_started` only flips once `dx.abs() > 8`. A vertical gesture that opened with a few pixels of lateral drift — a swipe-up to the launcher, a scroll — therefore emitted a tiny horizontal progress sample, which armed the consumer's pager, then locked vertical without `dx` ever passing 8, so `horizontal_drag_started` stayed false. On release the horizontal branch was skipped, no `on_swipe_horizontal_progress(0.0)` fired, and the pager stayed stuck active — on the crustace homescreen that froze the surface (the main stays motion-only behind stale page subsurfaces) until an unrelated gesture reset it. Emit horizontal progress only once `swipe_axis == Some(Horizontal)`. Locking onto the horizontal axis already implies `dx.abs() > 8`, so `horizontal_drag_started` is set in the same step, restoring the invariant that a frame which drives the pager always has a matching release event to settle it. The cost is that the first ~8 px of a horizontal drag no longer move the page, which is the same deadband the axis lock already imposes on the vertical panels. Adds `pre_lock_lateral_drift_does_not_drive_horizontal_pager`. |
|||
| cfa0faff26 |
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.
|
|||
| 9ca3b60f3a |
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. |
|||
| 24f4d2703a |
ltk: introduce viewport-relative Length so any size, padding, spacing or font height can scale with the surface instead of being frozen at a px constant, fix text::preferred_size to honour the font-declared line gap, and add a responsive typographic scale
The motivating bug was a lockscreen in a downstream app (eydos-loginmanager) where the clock at 87 px overlapped the date at 24 px on a Pinephone but not on a winit dev screen. The root cause split in two: the layout was wired with a single `f32` spacing constant that worked at the dev resolution and broke at the smaller one, and `text::Text::preferred_size` was returning `ascent - descent` for the line height — fontdue's terminology for "the minimum bounding box of an unaccented line", which deliberately drops the `line_gap` that every typographic renderer (Pango, CoreText, DirectWrite) reserves between adjacent rows. At Sora's 200/em line gap, an 87 px row was visually 17 px taller than the rect the column allocated for it; stacked tight against the row above, the descenders bled into the row below. This commit fixes both halves at the toolkit level so every consumer benefits without bolting on a per-screen `Sizing` helper in their own view code. `types::Length` (with the `LengthBase` enum behind it) is the new currency for any "how big" or "how far apart" parameter. Six variants — `Px`, `Vw`, `Vh`, `Vmin`, `Vmax`, `Em` — cover the cases a real UI hits: absolute pixels for fixed-chrome decisions, viewport-relative percentages for sizes that have to survive a portrait/landscape rotation, and root-font-size multiples for typographic hierarchy. Optional `min_px` / `max_px` bounds attach to the same `Length` value via `.clamp( lo, hi )` (both ends), `.at_least( lo )` and `.at_most( hi )` (one-sided); the names are intentionally divergent from `f32::min`/`f32::max` to avoid being read with the opposite semantics (`x.min(24)` in std means "the smaller of x and 24", which is the inverse of what a min bound expresses). The bounds are stored as raw `f32` rather than nested `Length` values, which keeps `Length` `Copy` and avoids a `Box` allocation per widget per frame — the bounded-by-relative case (`Vmin(20).clamp(Vmin(10), Vmin(40))`) is rare enough that the trade is the right one. `From<f32>`, `From<i32>` and `From<u32>` are implemented so every legacy `.size( 24.0 )` / `.padding( 8.0 )` / `.spacing( 4.0 )` call keeps compiling unchanged; the migration is opt-in per call site. The `EM_BASE_DEFAULT = 16.0` constant matches `theme::typography::BODY` so `Length::em( 2.0 )` resolves consistently with the body-text default; a future change can thread a theme-supplied em base through without breaking the resolver shape. The resolver — `Length::resolve( viewport: ( f32, f32 ), em_base: f32 ) -> f32` — runs at layout time against a viewport supplied by the renderer. `Canvas::viewport_logical()` is the new helper that exposes that viewport: it divides the canvas's physical size by `dpi_scale` and falls back to physical size when `dpi_scale <= 0.0`, guarding the misconfigured-canvas path so a Vmin call doesn't poison every downstream measurement with `NaN` or `inf`. The viewport is in **logical** pixels — matching what every wayland `xdg_toplevel.configure` event already hands the client — so `Length::vmin( 18.0 )` on a 360×720-logical Librem 5 portrait surface resolves to 64.8 px and the same expression on a 1600×900 dev screen resolves to 162 px, automatically. Every widget setter that took an `f32` size, padding, spacing, max-width, or fixed dimension now takes `impl Into<Length>` and stores the value as `Length`: - `widget::text::Text::size( impl Into<Length> )`; the `size` field is now `Length`. `Text::resolved_size( &Canvas )` is the internal accessor that every measurement / drawing path routes through, so the field can stay `Length` without churning the call sites. `preferred_size` and `draw` now read `new_line_size = ascent - descent + line_gap` from fontdue's `LineMetrics` (the fix for the original bug) — the baseline placement is unchanged, only the row height grows by the font's declared leading, which is what every stacked layout was implicitly relying on. - `layout::Spacer::height( impl Into<Length> )` / `.width( impl Into<Length> )`; `fixed_height` / `fixed_width` are now `Option<Length>`. New `resolved_height( &Canvas )` / `resolved_width( &Canvas )` helpers replace the direct `s.fixed_height.unwrap_or( 0.0 )` reads in `layout::column`, `layout::row` and `layout::stack`. `Spacer::preferred_size` grows a `&Canvas` parameter for the same reason; `Element::preferred_size` passes the canvas through. - `layout::Column::spacing` / `.padding` / `.max_width`, `layout::Row::spacing` / `.padding` — all take `impl Into<Length>` and store `Length`. Internal `resolved_spacing( &Canvas )`, `resolved_padding( &Canvas )`, `resolved_max_width( &Canvas )` helpers funnel every read, so the layout code paths stay readable. The column's `inner_w` private helper picks up a `&Canvas` argument; the test that used it directly is updated. `theme::typography` keeps its historic `f32` constants (`H0`…`BODY_XS`, plus `LINE_HEIGHT`) so the migration is gradual, and adds a parallel responsive scale exposed as functions returning `Length`: `h0()`, `h1()`, `h2()`, `h3()`, `body()`, `body_s()`, `body_xs()`. Each is a `Length::vmin( pct ).clamp( min_px, max_px )` whose percentage is calibrated against a 1000-px smaller side reproducing the legacy px constant exactly, and whose px clamps protect both ends of the spectrum — a 360-px Pinephone hits the lower clamp on the larger headings, a 4K desktop hits the upper one. The tests in `theme::typography` exercise all three regimes (narrow phone, calibration point, large display) so future drift in the percentages or clamps is caught immediately. `Canvas::viewport_logical` is the only render-surface API touched. None of the existing per-frame paths (`draw_text`, `measure_text`, `font_line_metrics`) change shape, so backends and external embedders aren't disturbed. The `dpi_scale` accessor already existed; this commit only adds the convenience that ratios it against the surface size to return the unit layout actually wants. Test coverage rounds out the addition rather than just smoke-testing the happy path: 22 new tests, broken down as `types::length_tests` (7 — every variant, clamp with relative value, clamp with swapped bounds, `From<f32>`), `render::viewport_tests` (3 — scale 1, scale 2, scale 0 fallback), `theme::typography::tests` (3 — phone-clamped, calibrated, 4K-clamped), `layout::spacer::tests` (4 — px height, vmin height, vw width, flex spacer reports `None`), `layout::column::tests` (3 new — vmin spacing accumulates, vmin padding, vmin max-width caps inner-w), `layout::row::tests` (2 new — vmin padding, vmin spacing produces correct visible gap between non-flex children regardless of the row's centering anchor), and `widget::text::tests` (3 updated/new — defaults compare against `Length::px(16.0)`, `.size( f32 )` and `.size( Length )` both verified). The existing integration test in `tests/layout_stack_spacer.rs` is updated to call `Spacer::preferred_size( &canvas )` and compare `fixed_height` / `fixed_width` against `Some( Length::px( n ) )`. Documentation is updated end-to-end so the new API is discoverable from `cargo doc` without grepping the source: `lib.rs` gets a new entry for `Length` under the **Types** section and a new **Designing for multiple resolutions** section that lists the three patterns (relative `Length` for sizing, responsive typography for hierarchy, `view()`-level branching on surface dimensions only when the structure itself must change). `Canvas::viewport_logical` ships with a runnable `assert_eq!` example covering the scale-2 case. The module-level docstrings for `Spacer`, `Column` and `Row` now show both an `f32` example (legacy, still valid) and a `Length::vmin( ... ).clamp( ... )` example for the responsive variant — `cargo doc` renders both side by side so the upgrade path is obvious. Out of scope for this commit, deliberate: `WrapGrid::spacing_x` / `spacing_y` / `padding`, `widget::text_edit::TextEdit::font_size`, and `widget::image::Image::size` still take `f32`. None of them are on a critical responsive path right now, the `From<f32>` shim means migrating later is a one-line setter signature change per widget, and keeping this commit focused on the widgets the lockscreen actually uses keeps the diff reviewable. The line-gap fix in `text::preferred_size` already benefits `TextEdit` indirectly because its caret/row math reads from the same metrics helpers. |
|||
| 88385e14b2 |
add Carousel widget and WrapGrid::centre_last_row
Forge's app switcher needs two layouts the existing widget set didn't cover. The desktop grid wants a partial last row centred under the rows above (3 tiles → row 1: two, row 2: one centred) so a 7-of-9 leftover band reads balanced rather than left-aligned. The mobile variant wants a horizontal carousel where the focused tile sits centred in the viewport at a configurable fraction of its width and its neighbours peek out on the sides at a fixed gap.
Extend `WrapGrid` with `centre_last_row( bool )`. When set, layout offsets a row that has fewer than `columns` children by `(missing * (cell_w + spacing)) / 2` so it stays centred inside the content rect. Defaults to false; every existing call site continues to land tiles flush-left. Covered by three layout tests (centred partial row, full row no-op, off-by-default).
Add the `Carousel` widget at `src/widget/carousel/`. It is a pure layout primitive: `focused_width_frac` (0.05–1.0, clamped), `gap` and `offset` are owned by the caller, leaving drag / inertia / snap policy to the host so the compositor can plug in its existing touch pipeline. Each child gets a rect at `base_x + idx * (child_w + gap)` and the full viewport height; `snap_offset( viewport_w, idx )` translates index to centring offset and `focused_index( viewport_w )` rounds the current offset back to the nearest tile. Plumbed into `Element::Carousel` with the matching arms in `widget/element.rs` and walker in `draw/layout.rs`; re-exported as `ltk::{ Carousel, carousel }`. Covered by nine unit tests (layout, offset shift, snap / focus round-trip, frac clamp, child height) plus a `cargo run --example carousel` demo with Prev / Next / arrow-key navigation against an external offset state. The example is wired into the `examples` Makefile target.
Updates the widget catalogue and the `widget/mod.rs` landing comment to list the carousel under "Clipping wrappers" and to mention `centre_last_row` in the grid section.
|
|||
| 78a7ae151c |
layout/stack: opt-in Stack::fit_content() so a wrapping container can adopt the stack's intrinsic size
`Stack::preferred_size` unconditionally reported `(max_width, max_h)` — every stack claimed the full width its parent offered, regardless of what its children actually needed. That is the right default for a FrameLayout-style overlay (the existing callers all rely on it) but it makes the natural "pin a stack to a fixed-size child (e.g. a spacer of `card_w × card_h`) and centre other children on top of it" pattern impossible: a container wrapping such a stack always inherited the full parent width and ignored the spacer's footprint. The new `Stack::fit_content()` builder mirrors the `Column::fit_content` flag — when set, `preferred_size` returns the max of children's intrinsic widths and heights instead of claiming the parent's `max_width`, with the same "skip filler widgets that themselves claim `max_width`" exclusion list (`Spacer` with no `fixed_width`, `Separator`, `Scroll`, `ProgressBar`, `Slider`, `TextEdit` without `fixed_width`) so the flag is not defeated by a flexible child slipping into the stack. Default behaviour is unchanged. |
|||
| 757063694e |
layout/stack: opt-in Stack::fit_content() so a wrapping container can adopt the stack's intrinsic size
`Stack::preferred_size` unconditionally reported `(max_width, max_h)` — every stack claimed the full width its parent offered, regardless of what its children actually needed. That is the right default for a FrameLayout-style overlay (the existing callers all rely on it) but it makes the natural "pin a stack to a fixed-size child (e.g. a spacer of `card_w × card_h`) and centre other children on top of it" pattern impossible: a container wrapping such a stack always inherited the full parent width and ignored the spacer's footprint. The new `Stack::fit_content()` builder mirrors the `Column::fit_content` flag — when set, `preferred_size` returns the max of children's intrinsic widths and heights instead of claiming the parent's `max_width`, with the same "skip filler widgets that themselves claim `max_width`" exclusion list (`Spacer` with no `fixed_width`, `Separator`, `Scroll`, `ProgressBar`, `Slider`, `TextEdit` without `fixed_width`) so the flag is not defeated by a flexible child slipping into the stack. Default behaviour is unchanged. |
|||
| bbab5e238d | First commit. Version 0.1.0 |