Commit Graph

  • 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. main Pedro M. de Echanove Pasquin 2026-07-07 17:40:33 +02:00
  • d4d7ee742e Bump to 0.2.0: SW/GLES paint parity, shared font resolution, FrameState refactor, docs and packaging fixes Rendering parity (software ↔ GLES). The software backend now rounds glyph pen positions and image destinations to the nearest integer pixel, matching what the GLES backend already did; previously it truncated, so text and 1:1 images could land up to half a pixel off between the two backends and the bilinear sample read ~1 px softer than the source. Gradients and shadows are deliberately left unimplemented on the software backend, and the GLES multi-rect glScissor clip is left coarse on purpose: making it exact would need stencil bits the EGL config does not carry, or routing the partial-redraw path through the offscreen clip layer, which would break its fill / clear_rects_transparent scissor semantics. Adds software-backend pixel tests covering the snapping. Font resolution unification. The system-font candidate chain, find_font_opt and load_default_font_bytes lived in two copies (render/helpers and gles_render/helpers) that had already diverged — one resolved through find_font_opt, the other inlined the candidate loop — and now live once in system_fonts. The two per-backend OnceLock default-font caches and primary_handle collapse into a single system_fonts::default_handle. Module docs and the font_registry caller are updated accordingly. Shared image validation and rect inflation. draw_image_data's dimension check and its one-line warning were byte-duplicated across both backends and are now render::helpers::validate_rgba_dims. The six manual symmetric Rect-inflate literals in the GLES primitives reuse the existing Rect::expand. FrameState and DrawCtx de-duplication. The eleven SurfaceState fields the draw pass owns and threads through DrawCtxwidget_rects, the cursor / selection maps, the scroll state, accessible_extras, prev_focused / prev_hovered / prev_pressed — move into a FrameState sub-struct. The per-frame build_draw_ctx / commit_draw_ctx helpers can then borrow &mut ss.frame, disjoint from ss.canvas and ss.pool, so the four frame paths (software / GLES × full / partial) replace their duplicated DrawCtx construction and write-back with a single helper call each. A whole-SurfaceState borrow could not express this (partial borrows do not cross function boundaries), which is why the helpers take the sub-struct. content_dirty stays on SurfaceState — it is an invalidation flag, not frame state — and is reset at the call site. rich_text tests. Adds the previously-missing tests.rs for the RichText widget: one hit rect per visual line a link spans (the widget's core invariant), the single-line and no-link cases, preferred-size growth with hard line breaks, and map_msg range preservation — all headless against a software Canvas, with line counts forced by \n so they do not depend on any system font's measured width. Documentation. Fills the rustdoc gaps on the embedder-facing surface: core::UiSurface accessors, the egl_context public API, the GlesCanvas methods, theme::typography and theme::error, and the RichText / Text builders; adds a crate-level "Rendering backends" overview. CHANGELOG.md is added (0.2.0 / 0.1.0), and docs/widgets.md / docs/cookbook.md gain rich_text, external, and the CPU-draw / path-clip / externally-laid-out-tree recipes. debian/changelog gets the 0.2.0-1 entry. Private intra-doc links to system_fonts are demoted to code spans so cargo doc is warning-free. Packaging. The libltk-dev registry crate shipped a Cargo.toml declaring the lookup bench while the Makefile install copied only src/, so Cargo refused to parse the manifest over a missing benches/lookup.rs; the install now ships benches/ as well (the file alone satisfies the parse — criterion is a dev-dependency and is not resolved when the crate is consumed as a library). Cargo.toml is bumped to 0.2.0 to match the package version and the ltk-0.2.0 registry directory. Pedro M. de Echanove Pasquin 2026-06-25 12:43:40 +02:00
  • 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). Pedro M. de Echanove Pasquin 2026-06-24 22:42:50 +02:00
  • 8809313be1 theme: replace Figma-exported launcher SVGs with renderer-compatible versions yamabush1 2026-06-21 11:27:03 +02:00
  • 588a3f7e36 Fix vertically-flipped content in the GLES path-clip composite The clip-layer composite shader sampled the offscreen layer at the wrong vertical position, so path-clipped content (e.g. a circular avatar) came out upside down on the GLES backend. v_uv.y runs bottom-to-top in screen space — the layer FBO has GL's lower-left origin, and ortho_rect plus the texture shader's flip establish that v_uv.y == 1 is the top edge. The composite computed the fragment's screen Y as bbox.y + v_uv.y * bbox.h, which is the inverted Y, so it read the layer mirrored about the horizontal axis. Compute it as bbox.y + (1 - v_uv.y) * bbox.h instead. The mask sampling was already correct (and a symmetric circle hid the flip in the example; a photo reveals it). The software backend is unaffected — it clips through a coverage mask with no layer round-trip. Pedro M. de Echanove Pasquin 2026-06-19 00:04:24 +02:00
  • f8c45f0e30 Add Canvas::set_clip_path — anti-aliased arbitrary-path clipping on both backends Add Canvas::set_clip_path(&[PathCmd]), clipping subsequent draws to an arbitrary vector path with an anti-aliased edge, on both the software and GLES backends. It complements the existing rect clip (set_clip_rects) and is what an embedder needs to render a shaped clip — a circular avatar, a rounded card, a VectorDrawable mask — rather than a bounding box. Kept general rather than tied to any one consumer. Software backend: rasterise the path into an anti-aliased tiny-skia coverage Mask (Winding fill) and install it as the active clip mask. Every software primitive already threads clip_mask through tiny-skia (fills, strokes, lines, paths, images, text, blit), so the path clip applies uniformly with smooth edges. clip_bounds reports the path's bounding box while it is active. GLES backend: a 1-bit stencil would clip exactly but leave a hard, aliased edge, so instead the clipped draws are captured into an offscreen layer and composited back through an anti-aliased coverage mask. set_clip_path rasterises the path coverage (tiny-skia, anti-aliased), uploads it as a mask texture, allocates a full-canvas layer FBO on first use, and redirects subsequent draws to it via activate_target. Ending the clip (clear_clip / set_clip_rects / a new set_clip_path) composites the layer back onto the canvas FBO with a new two-sampler program (CLIP_COMPOSITE_FRAG_SRC) that multiplies the layer colour by the mask coverage and blends it premultiplied-over. The layer attaches to the canvas's own shadow FBO, so it needs no stencil bits in the EGL config; it is freed and reallocated on resize and freed on drop, and shared programs/uniforms are copied to sub-canvases like the rest. Usage: a path clip is bracketed — set_clip_path then, after the clipped draws, clear_clip or set_clip_rects to flush it (on GLES this is when the layer is composited). Snapshot the prior clip with clip_bounds beforehand and restore it with set_clip_rects to compose with an outer clip without leaking state. Add an examples/clip_path.rs demo (rounded rect, circle, triangle — same smooth result on both backends) and software-backend unit tests covering the bounding box, the empty-path clear, and a pixel-level check that a triangular clip masks a fill to the path silhouette rather than its bounding box. The GLES layer-composite path needs a live GL context and is exercised by the example. Also fix three rustdoc intra-doc-link warnings surfaced along the way: a private-item link in app.rs (scroll) and the new GLES doc (SoftwareCanvas::set_clip_path) demoted to code spans, and a redundant explicit link target in chassis.rs. Pedro M. de Echanove Pasquin 2026-06-18 23:59:38 +02:00
  • 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. Pedro M. de Echanove Pasquin 2026-06-12 22:14:02 +02:00
  • df8fcbf757 app: client-side xdg-activation token requests Adds an outbound path so an app can obtain an xdg-activation-v1 token from the compositor and hand it to a child it is about to launch (the $XDG_ACTIVATION_TOKEN convention). Until now ltk only honoured inbound activation (self-activating the main surface from an inherited token); requesting a token for another app was out of scope. Two new App trait methods, both defaulting to no-op: take_activation_requests returns the tags the app wants tokens for this iteration, and on_activation_token delivers the issued token paired with its tag. The run loop drains the requests right after poll_external and calls ActivationState::request_token, carrying the tag in RequestData::app_id; ActivationHandler::new_token reads the tag back out and routes the token to the app through on_activation_token. When the compositor never advertised the activation global, each request is answered immediately with an empty token so the caller still proceeds and can fall back to its own matching instead of stalling. Pedro M. de Echanove Pasquin 2026-06-09 23:53:31 +02:00
  • 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. Pedro M. de Echanove Pasquin 2026-06-07 16:45:59 +02:00
  • 68c6a87bf6 Support viewport-relative widget sizing yamabush1 2026-06-04 12:38:59 +02:00
  • ae8380b1ac event_loop: retry focus requests that land before the view is rebuilt take_focus_request() looks up the target widget by WidgetId in widget_rects. Read-only TextEdit widgets are not tracked as interactive and therefore absent from widget_rects. A swipe-reveal flips the fields from read-only to interactive on SlideRevealed, but an intervening input event (e.g. the touch-up at the end of the swipe) can cause dispatch() to return before the vblank frame callback clears frame_pending, skipping the draw that would have rebuilt the widget tree. When the focus request fires in that same iteration it finds nothing and is consumed without effect. AppData gains a focus_retry field. When take_focus_request() (or a previous retry) returns an id but the widget is not in widget_rects, the id is stored in focus_retry, view_dirty is set and a redraw is requested. The next time around the view has been rebuilt with interactive fields and the widget is present. Pedro M. de Echanove Pasquin 2026-06-02 14:10:37 +02:00
  • e40ab637a6 bench: add missing LaidOutWidget accessibility fields to lookup bench yamabush1 2026-06-02 08:43:13 +02:00
  • d221d5a0cd app: add App::subsurface_motion_only so a slide-to-reveal panel animates without re-rastering the main surface An app whose gesture/animation only moves an input-transparent subsurface (a slide-to-reveal panel over a static main surface) still paid a full main-surface re-raster on every frame of the slide, because the runtime force-dirties the main view on two paths it cannot tell apart from a real content change: MoveOutcome::Swipe calls dirty_caches() + request_redraw() per motion sample, and the WlCallback Main handler sets view_dirty + request_redraw() on every frame callback while is_animating. The subsurface reconcile already repositions the panel with a cheap set_position + bare parent commit, so the main re-raster is wasted work — and on slow targets (Librem5) it competes with the reposition and makes the slide stutter. New opt-in App::subsurface_motion_only (default false, so existing apps are unaffected). When it returns true: - the swipe dispatch (input/dispatch/outcomes.rs) still calls the on_swipe_progress family but skips dirty_caches() / request_redraw(); incoming motion events pump the loop and the per-iteration reconcile_subsurfaces carries the move. - the frame-callback animation pump (event_loop/handlers.rs) keeps the vsync cadence without a re-raster by requesting a bare wl.frame() + commit() on the main surface (no buffer attach) instead of dirtying the view; poll_external advances the animation and the reconcile repositions each frame. The main surface still redraws for genuine content changes (messages, resize, the one-shot redraw on swipe release) — only the per-frame slide raster is dropped. Pedro M. de Echanove Pasquin 2026-06-01 21:53:40 +02:00
  • b8a32cfb96 Added pkg-config debian dependency Pedro M. de Echanove Pasquin 2026-06-01 20:51:45 +02:00
  • 34b3e76ac1 test: make button-paints-content render check theme-robust yamabush1 2026-06-01 11:33:35 +02:00
  • 48c5a89712 touch: treat a compositor re-grab down as a drag continuation When the surface that owns a touch drag is destroyed mid-gesture, the compositor re-opens the touch grab by issuing a fresh down on the surface now under the finger (smithay pins touch focus at down and won't re-target on motion). That down arrives for a slot the main surface already holds as its primary, with the drag state already migrated here. Handling it through the normal down path would either restart the gesture or, since the slot is taken, demote it to an auxiliary touch — both abandon the in-flight drag. TouchHandler::down now early-returns when the focused surface's primary_touch_id already equals the incoming slot, so the redundant down is a no-op and the following motion/up keep driving the migrated drag. Completes the cross-surface drag fix whose other half (migrating primary_touch_id on overlay teardown) already landed. Pedro M. de Echanove Pasquin 2026-06-01 00:19:49 +02:00
  • 582f9e1a37 Removed extra debug prints Pedro M. de Echanove Pasquin 2026-05-31 02:14:34 +02:00
  • 042652ec73 windows: centralise input focus on a single FocusTarget, raise+focus on every window interaction Focus was scattered across three stores — Layouts::focus, Layers::focus and Windows::activated — plus a hack that smuggled X11 surfaces through Layers::focus, with the keyboard target derived each frame from a layers.focus.or(layout) precedence. As a result "focused", "activated" and "has the keyboard" could drift apart: a window could be raised without taking the keyboard, the recurring "click a console window, then click it again before you can actually type" symptom. New windows/focus.rs with FocusTarget { Layout(Weak<Window>), Layer(WlSurface, app_id), X11(WlSurface) }. Windows now holds one focus_target: Option<FocusTarget> as the single source of truth; activated survives only as a reconciliation cache for the xdg Activated state and is never set by hand. set_focus takes one Option<FocusTarget> and is the only entry point for changing focus: it brings the target to the front (z_promote + desktop stacking), refreshes the MRU and stores the target, and keyboard focus plus activation are applied from it on the next focus(). focus() resolves a baseline (the active layout's window, which carries Activated) and an overlay (a focused layer/X11 surface that takes the keyboard over the baseline without de-activating it), mirroring the old precedence but from one field, so keyboard focus follows focus by construction. Layers::focus is removed and X11 no longer rides inside it; the exclusive-keyboard layer path and reap_layer drive focus_target directly, and every click/touch/activation site collapses to set_focus(Some(FocusTarget::…)). surface_at popup tie-break. When a layout hit is a popup or subsurface, its wl_surface is not tracked in toplevel_z_order, so the layout-vs-X11 z compare resolved to None and the X window underneath wrongly won — clicking a GTK menu (e.g. Firefox's) over an X11 window pressed the X window instead of the menu item. The tie-break now compares the owning toplevel's root z, taken from the hit's FocusTarget::Layout weak, so the popup's parent stacking decides. Raise and focus on every window interaction, matching standard desktop behaviour. Client-driven (un)maximize, the SSD maximize/minimize/close buttons and the maximize keybind now bring the window to the front in both the render z-order and the hit-test stacking through a new raise_toplevel helper; previously only the stacking was updated, so an unmaximised window could stay visually behind another. Starting a move or resize — including a press on the resize edge — now routes through set_focus, so the window comes to the front and takes the keyboard before the drag instead of being resized or moved while it stays behind. Input robustness around grabs and drags. A press under an active pointer grab (Wayland popup, drag-and-drop or explicit client grab) is routed to the grab instead of forge's own surface_at handling, so a click cannot leak to a window underneath a grabbing popup and a click outside the popup dismisses it cleanly. Only the press is blocked, never the release, so an in-flight move/resize drag or topbar swipe always reaches its end handler and can never stay glued to the cursor if a grab appears mid-gesture. on_touch_up now ends any active move/resize drag before its other early-return paths (armed SSD button, topbar swipe, app switcher) and drops a stale armed button, fixing a race where pressing a second, overlapping window left the drag running forever after release. fix(input): keep the primary touch slot when migrating a drag across surfaces A touch drag started on an overlay that hides mid-gesture (e.g. the app launcher when dropping an icon onto the dock or the homescreen) froze: it neither moved nor dropped. Destroying the origin surface migrated the drag state (long_press_fired / long_press_origin) and touch_focus to the main surface, but not its primary_touch_id; subsequent touch motion/up events then fell through the auxiliary path and never reached the gesture machine, leaving on_drag_move and on_drop uncalled. reconcile_overlays and discard_overlay now adopt the destroyed overlay's primary_touch_id when a drag is in flight, so the rest of the touch sequence keeps driving the main surface. Touch-only; the mouse already migrated correctly via pointer_focus. ltk: add a chassis module for full-screen ambient surfaces New src/chassis.rs, re-exported from the crate root, gathers the scaffolding that every full-screen ambient surface — greeter, lock screen, kiosk — otherwise repeats by hand over the existing theme and WallpaperBundle primitives. set_default_theme(mode) finds, installs and activates the default theme document, returning the failure message instead of exiting so the caller decides how to abort. theme_logo_rgba(size) decodes the active theme's horizontal logo to RGBA, and theme_icon_tinted(name, size, tint) loads a symbolic theme icon and tints it. branding_bundle_or_solid(name) resolves a theme branding image ("wallpaper", "lockscreen", …) to a WallpaperBundle, falling back to a solid fill of the palette background when the theme ships none; wallpaper_bundle_or_solid() is the "wallpaper" convenience. backdrop(content, &wallpaper, w, h) stacks content over the wallpaper resolved for the surface size. No new capability — thin convenience over what the theme module and WallpaperBundle already expose — but it removes the per-application load_theme_logo / build_wallpaper_bundle / theme-bootstrap duplication. ltk: animatable, input-transparent child surfaces via App::subsurfaces() New App::subsurfaces() -> Vec<SubsurfaceSpec<Msg>> (default empty) describes input-transparent child surfaces composited over the main surface, with SubsurfaceSpec { id, view, x, y, content_version } and the stable SubsurfaceId. The motivating use is a slide/reveal that tracks a finger without the per-frame full-screen CPU re-raster a single-surface opacity or translate would cost: the content buffer is rasterised once and the compositor moves it. SubcompositorState is bound in event_loop/run.rs from the compositor's wl_compositor (absent → App::subsurfaces silently degrades to none); delegate_subcompositor! is added on AppData, which gains a subcompositor binding and a subsurfaces map. New event_loop/subsurface.rs reconciles the live subsurfaces against the specs: each spec becomes a wl_subsurface sized to the main surface with an empty input region, so all pointer/touch falls through to the parent and the host keeps a single gesture/input model. The content — its own SHM pool and Canvas, drawn through the existing DrawCtx / layout_and_draw path — is rasterised only when the surface size or the spec's content_version changes; a position-only change emits wl_subsurface.set_position and commits the child surface, then a bare parent commit for placement. Committing the child is deliberate: desync subsurface state (the position) is applied on the child surface's own commit, not the parent's, so without it the move is queued but never lands. Positions are given in layout (physical) pixels and divided by the surface scale for the logical set_position. The reconcile pass runs on every run-loop iteration rather than being gated behind a main-surface redraw, so a finger-driven move repositions at input-event rate, decoupled from the frame-callback cadence that paces full redraws — without this the subsurface only moved when the main surface happened to redraw, so a drag over a static background froze the panel in place. Pedro M. de Echanove Pasquin 2026-05-29 23:28:48 +02:00
  • 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-1avatar-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. Pedro M. de Echanove Pasquin 2026-05-28 23:11:14 +02:00
  • 3d8523533c text-input: re-sync on enter and flag secure fields as Password LTK drives the on-screen keyboard through zwp_text_input_v3: focusing a text widget enables text-input so the compositor's input-method (squeekboard) brings the keyboard up. Two gaps are fixed here. Handle the enter event by re-emitting enable + content type + commit. The compositor sends enter when it (re)focuses the text input — notably when an input-method connects after the field has already enabled text-input (a startup race). LTK previously ignored enter, so that activation was lost and the keyboard never appeared; it now re-declares its state and the OSK comes up. Declare the content type from the field's secure flag: secure fields are flagged ContentPurpose::Password with ContentHint::SensitiveData | HiddenText (so the IME/OSK skips prediction, autocorrect and storing the value), everything else stays Normal/None. The content type is also refreshed when focus moves between two text fields (e.g. username → password) without re-creating the text-input object, and a new AppData::text_input_secure lets the enter re-emit path preserve the current field's type. Correct the docs: TextEdit::secure() and SECURITY.md claimed secure "skips text-input-v3 registration". It does not — the field still activates text-input so the OSK works on it; the protection is the Password / sensitive flagging, and the value still reaches a trusted compositor/IME. Pedro M. de Echanove Pasquin 2026-05-27 22:31:36 +02:00
  • 1e2cb836f4 ltk: convert physical sizes to logical for overlays and input regions on HiDPI Overlay set_size and input regions were handed physical (layout-space) pixels straight to layer-shell and wl_region, both of which expect logical coordinates. They only coincide at scale 1, so on a scale-2 output every overlay requested a surface twice its intended size and the input region covered the wrong area. apply_input_region now takes the surface scale and divides each Rect down to logical before adding it to the region; the four draw paths (software and gles, full and partial) forward their scale. reconcile_overlays converts OverlaySpec::size to logical for both set_size and the initial OverlayConfig (0 = fill survives the divide), and seeds the new surface's scale_factor from the parent so the first configure allocates a HiDPI buffer instead of rendering at scale 1 until scale_factor_changed lands a frame or two later. Pedro M. de Echanove Pasquin 2026-05-26 22:22:18 +02:00
  • fc045a9c22 ltk: ext-session-lock-v1 client surface mode, plus a read-only mode for text fields Add a third Wayland surface type to the runtime so an ltk App can be a screen locker, alongside the existing xdg-shell window and wlr-layer-shell surfaces. A new ShellMode::SessionLock makes run() bind ext_session_lock_manager_v1 and request the lock at startup; the lock surface itself is created in the new SessionLockHandler::locked callback (one surface on the first advertised output) and replaces the SurfaceKind::PendingLock placeholder the main surface holds until the compositor grants the lock. The configure event routes through the same on_configure path as layer and xdg surfaces, so sizing and rendering are unchanged, and finished (the compositor denied or ended the lock) tears the loop down. The whole thing is additive and opt-in: the Window and Layer paths are untouched and nothing enters lock mode unless an App returns ShellMode::SessionLock, so existing apps are unaffected — the only non-additive edits are the two exhaustive matches on SurfaceKind (wl_surface / try_wl_surface), which gain arms for the two new variants. Doing the locker as a first-class surface rather than compositing a static texture into an offscreen UiSurface is the whole point: the compositor gives the lock surface keyboard focus, so ltk's existing text-input, editing, focus and IME machinery works inside the lock exactly as on any other surface — cursor, click-to-focus, Tab, character input. A locker built on top of this is just a normal interactive ltk app that happens to be presented on the lock layer, with no special input plumbing on the compositor or the app side. App::requested_exit() is the new way an app asks the runtime to tear the surface down and leave the loop; it is polled after every batch of updates. It exists because of the one hard invariant of ext-session-lock-v1: a locker that disconnects without sending unlock leaves the compositor's outputs blanked forever — that is the protocol's deliberate anti-bypass guarantee. So when requested_exit() returns true and the surface is a session lock, the loop calls session_lock.unlock() and round-trips the connection before setting exit_requested, lifting the lock cleanly; for a Window or Layer surface there is no lock and it simply exits. The consequence for lock apps is that they must stop calling process::exit from the lock path and instead flip a flag they return from requested_exit(). text_edit gains a read_only( bool ) builder. A read-only field still renders its box and value in the normal field style but takes no keyboard focus and accepts no input: Element::is_focusable and Element::is_text_input now return false for a read-only TextEdit, which keeps it out of the Tab cycle, off the keyboard-edit path, and stops the cursor from ever being drawn on it. The flag is carried through map_msg so it survives Element::map. This is for presenting a known, non-editable value in the same visual idiom as the editable fields beside it — for example the already-known user shown on a session lock, where letting that field take focus or blink a cursor would be wrong. The shell_mode() doc comment and the README now list the SessionLock surface type and point at requested_exit() for the unlock. Two warnings are cleared along the way: the runtime no longer stores the SessionLockState after requesting the lock — it has no Drop, so the manager object outlives the dropped handle inside the connection and the lock lifecycle runs entirely off the returned SessionLock, which removes a never-read field — and a pre-existing rustdoc private_intra_doc_links warning in list_item (a public doc comment linking to the private theme::ICON_SIZE) is downgraded to plain code formatting. Pedro M. de Echanove Pasquin 2026-05-26 00:11:33 +02:00
  • cff4b12a4a event_loop: drop the duplicate on_resize that clobbered physical dimensions with logical ones Both LayerShellHandler::configure and WindowHandler::configure called self.on_configure( w, h ) — which already forwards app.on_resize( w * sf, h * sf ) in physical pixels — and then immediately called self.app.on_resize( w, h ) again with the surface-local logical size. The second call won, so on any scale > 1 surface the app saw logical dimensions while the layout pass kept working in physical pixels. On a Librem 5 at scale 2 that meant screen_width/screen_height came through as 360×720 against a 720×1440 layout: homescreen icons rendered at half their intended footprint and the mobile wallpaper, sized explicitly via .size( iw_at_h, sh ), filled only the top half of the screen (the launcher / lockscreen / greeter use .cover() so they were unaffected). Remove the redundant app.on_resize from both handlers; on_configure is the single source of truth for the physical dimensions the app and the layout both expect. Pedro M. de Echanove Pasquin 2026-05-25 11:42:45 +02:00
  • f7ef932976 list_item: optional leading icon yamabush1 2026-05-25 09:00:00 +02:00
  • 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 (H0BODY_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. Pedro M. de Echanove Pasquin 2026-05-24 00:12:50 +02:00
  • c553c4df4b themes/default: drop the inner shadows from the launcher.svg glyph In both themes/default/branding/dark/launcher.svg and themes/default/branding/light/launcher.svg the launcher glyph is built from nine cells, each with its own filterN_diiii_4479_*. Those filters layered four successive inner-shadow passes (effect2..effect5_innerShadow_*) on top of the outer drop shadow effect1_dropShadow_*: pairs of feColorMatrix in="SourceAlpha" + feOffset + feGaussianBlur + feComposite operator="arithmetic" + feBlend, with offsets (-3.6,-3.6), (1.8,1.8), (0.45,0.45) and (1.8,1.8) and blend modes plus-lighter, overlay and normal to reproduce the light-above / dark-below bevel and inner halo of the original Figma export. That is twenty-four lines per cell and nine cells per file — 216 lines per SVG, 432 in total. This change removes those four inner-shadow passes in both files; the outer drop shadow (effect1_dropShadow_*), the clipPath with bgblur, the rect elements defining each cell and the rest of the document are kept verbatim. The launcher silhouette and its placement do not change: the icon still occupies the same viewBox and produces the same clip; what disappears is the specular highlight and dark inner contour of each cell, leaving flat rectangles on the background with their projected shadow. The diff is pure deletions, with no added lines, and the existing difference between the dark and light variants is preserved (only the filter identifiers differ, _38862 versus _38700). Pedro M. de Echanove Pasquin 2026-05-23 00:53:13 +02:00
  • 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. Pedro M. de Echanove Pasquin 2026-05-22 19:38:48 +02:00
  • 0e52274053 app, event_loop: first-frame-committed hook and foreign_toplevel app_id only Two independent changes, both blockers for a working desktop session through the loginmanager-daemon handoff and the dock's running-app icons. App::on_first_frame_committed is a new trait hook fired exactly once, immediately after the very first wl_surface.commit of a rendered buffer on the main surface. AppData grows a first_frame_committed: bool, draw_frame now returns whether this call performed that first commit, and try_run invokes the hook after the borrows held during the draw are released. Used by the loginmanager-daemon-aware crustace path to signal "ready to be presented" back to the daemon as soon as the GPU has the first frame — the actual present can still be deferred under VT switching (no DRM master yet), but the client-side commit is the right edge for handoff. toplevel_display_id in the foreign-toplevel-list handler no longer falls back to info.title or info.identifier when info.app_id is empty. Smithay creates each ext-foreign-toplevel-list-v1 handle with app_id = "" and init_new_instance flushes a done immediately, so subscribers used to see the protocol-level identifier (a 32-char Alphanumeric random token) as the "app id" of every new toplevel — and remained stuck on it whenever the client's real set_app_id arrived between done events but the subscribing app's matcher couldn't resolve it to a .desktop entry. Returning the raw app_id (empty or not) makes that first transient done ignorable by the consumer's own empty-string guard; the second done, carrying the real app id, is processed normally. Pedro M. de Echanove Pasquin 2026-05-21 01:51:19 +02:00
  • 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. Pedro M. de Echanove Pasquin 2026-05-20 15:26:41 +02:00
  • 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. Pedro M. de Echanove Pasquin 2026-05-20 15:24:34 +02:00
  • 640db23de2 doc: silence rustdoc intra-doc warnings (private-item links and palette/surface ambiguities) cargo doc --no-deps was emitting eight warnings about intra-doc links resolving to private items or ambiguous fn/module names. None reflect a behaviour bug; they were just noise that cluttered the docs build output. Five of the warnings came from doc comments that cross-referenced items not in the rendered public API. widget::scroll::Scroll (struct), Scroll::horizontal, Scroll::both, event_loop::text_editing (module) and text_shaping (module) are all pub in their own modules, but the widget and event_loop parents are private to the crate root, so rustdoc treats them as private when resolving links from items that ARE on the public surface (ScrollAxis, the scroll constructor, font_bytes, the text_edit module docs). The fix is to drop the link syntax for those references: keep the identifier in backticks (so it still renders as code) but remove the surrounding [...] so rustdoc doesn't try to resolve it. Where the cross-reference had no semantic load beyond "see this module", the prose is rephrased to name the module without trying to link to it (e.g. "the event_loop::text_editing private module", "see the text_shaping private module"). The remaining three warnings were palette / surface linking ambiguously between a module of that name and a function of the same name within crate::theme. Adding () after the identifier inside the brackets (palettepalette(), surfacesurface()) disambiguates to the function, which is the intent in all three sites — the surrounding text talks about "per-slot shorthand accessors", which is what the functions are. After this cargo doc --no-deps runs clean with no warnings. Pedro M. de Echanove Pasquin 2026-05-19 22:41:42 +02:00
  • a8bbd1e35c input/keyboard: fall through to App::on_key when the Enter dispatch target widget has no submit/press message handle_key_return previously routed Return to either the focused or hovered widget's submit/press message and, only when neither index existed, fell through to app.on_key_with_modifiers. If a stale hovered_idx (left over from a prior screen) pointed at a widget that exists in the new widget_rects but exposes no submit or press handler, target.is_some() was true and the message-less dispatch silently swallowed the key — the else branch never ran and App::on_key never saw the keysym. This manifested in the eydos-loginmanager greeter as Enter on the Lock screen failing to fire Message::Unlock after a pause/resume cycle, until the user clicked something to refresh hovered_idx. Track whether the widget path actually pushed a message and, when it didn't, fall through to the app-level handler so the Return keysym still gets a chance to be interpreted by the application's on_key. No behaviour change when the focused/hovered widget does provide a submit_msg or press_msg. Pedro M. de Echanove Pasquin 2026-05-19 21:40:20 +02:00
  • 9cc65e70ea fix(event_loop): walk error source chain for BrokenPipe in OtherError yamabush1 2026-05-18 20:48:35 +02:00
  • 750eae7a93 fix(event_loop): exit cleanly on OtherError(BrokenPipe) from compositor yamabush1 2026-05-17 18:28:50 +02:00
  • 4a80165428 event_loop, a11y, text_shaping: AccessKit AT-SPI2 bridge, cross-app clipboard, xdg-activation, HarfBuzz shaping, multi-touch hooks Five orthogonal capabilities land together because they share the same try_run plumbing: an optional global is bound at startup, a piece of state is added to AppData, the run-loop iteration drains an inbox / pushes a frame snapshot, and the public surface gains a small set of opt-in App hooks. Nothing here breaks an existing app — every new path degrades to a no-op when the compositor does not advertise the relevant global or when the platform adapter cannot start. AT-SPI2 accessibility via AccessKit. A new src/a11y/ module owns the platform adapter and the inbound ActionRequest channel. A11yState::try_new constructs an accesskit_unix::Adapter; when the AT-SPI2 daemon is not on the session bus (headless CI, locked-down compositors) the constructor returns None and the rest of the pipeline runs unchanged. After every successful draw_frame, the run loop builds a fresh accesskit::TreeUpdate from widget_rects and pushes it through the adapter — main surface plus every visible overlay, each translated to global coordinates via surface_offset_for so screen readers report positions in the same frame the user sees. Buttons / toggles / checkboxes / radios / list items / sliders / text edits map to the matching Roles; Click and Focus actions are advertised on every interactive node; inbound action requests are drained at the top of each iteration and translated into a synthetic press / focus on the matching widget. The integration is documented as best-effort in docs/architecture.md under "Known gaps and non-goals": hierarchical nesting, per-widget accessible names, live regions and Action::SetValue are listed as the natural follow-ups that the foundation now supports but does not yet wire. Cross-application clipboard via wl_data_device_manager. A new src/event_loop/data_device.rs bridges the existing process-local clipboard: String to the Wayland selection. Outbound (Ctrl+C / Cut): after the local clipboard is populated, publish_clipboard_selection creates a CopyPasteSource offering text/plain;charset=utf-8 and installs it as the seat's selection; DataSourceHandler::send writes the cached string into the fd the peer hands us. Inbound (Ctrl+V from another app): DataDeviceHandler::selection asks for the offered text via WlDataOffer::receive, spawns a tiny worker thread to drain the read pipe with a 16 MiB cap to prevent paste-bomb DoS, and posts the result back through an mpsc::Sender that the run loop drains each iteration into data.clipboard. The clipboard: field's doc-comment is updated to reflect the new behaviour: process-local when the compositor does not advertise the global, synchronised with the seat selection otherwise. External drag-and-drop reception. The same data_device module handles DragOffer enter / motion / leave / drop_performed: on_drop_motion( x, y ) fires while the drag hovers over the surface, on_drop_leave() when it withdraws without dropping, and on_drop_received( x, y, mime, text ) when an external payload (text/uri-list, text/plain, …) is released on top of an ltk window. The receive path reuses the same worker-thread / channel pattern as the clipboard so the run loop never blocks on the read fd. Three new App hooks expose the events with no-op defaults; apps that ignore them get the previous behaviour. xdg-activation-v1. The global is bound optionally; when it is present, try_run reads $XDG_ACTIVATION_TOKEN from the environment, removes it immediately (single-use; preventing leaks into child processes) and stashes it on AppData::activation_token_pending. After the first successful configure of the main surface — the earliest point at which xdg_activation_v1.activate is meaningful — the token is consumed once and the surface raised to focus. Compositors without the global leave activation_state as None and the inbound path silently degrades. An App::request_activation_token outbound path is reserved on the trait but not yet exercised here. HarfBuzz shaping. A new src/text_shaping.rs::shape_line drives both renderers: the logical-order string is run through unicode-bidi, split into per-font sub-runs, and shaped through rustybuzz. Each PositionedGlyph carries the per-font glyph_id, the visual advance and the ink offsets — exactly what fontdue::Font::rasterize_indexed needs to render Arabic connected forms, Devanagari clusters and CJK shaped glyphs correctly. The GLES atlas is re-keyed on (glyph_id, size_bits, font_id) so glyphs from different fonts at the same size no longer collide, and the atlas format is selected per ES profile (GL_R8 / GL_RED on ES3, GL_LUMINANCE on ES2) — the fragment shader samples .r for both, since GL_LUMINANCE replicates the coverage byte into .r=.g=.b. Software path follows the same key. New Cargo.toml deps: unicode-bidi = "0.3", rustybuzz = "0.14". Multi-touch hooks. App::on_touch_down / on_touch_move / on_touch_up( id, x, y ) expose the raw wl_touch.id of every secondary finger. The first finger to land remains the *primary slot* and is fed through the regular gesture machine (on_pointer_*, swipe, scroll, long-press, drag-and-drop). Every additional finger fires the new callbacks instead, leaving the existing single-slot behaviour untouched for apps that do not override them. This is the substrate for app-defined pinch-zoom / two-finger pan; the toolkit itself does not yet ship a built-in pinch gesture (called out in the same "Known gaps" doc section). event_loop::frame extracted from draw/mod.rs. The draw_frame orchestrator and its per-format SHM helper (pick_shm_format) move into src/event_loop/frame.rs, leaving draw/ strictly responsible for per-surface paint primitives. The import in event_loop/run.rs is rewritten accordingly; draw/mod.rs shrinks from 192-line orchestrator to a thin module index. Overlay teardown safety. AppData::discard_overlay( id ) synchronously removes a destroyed overlay from the map and rewrites every per-device focus that pointed at it (pointer, keyboard, every touch slot), migrating an in-flight long-press drag to the main surface the same way reconcile_overlays does. Used by the compositor-driven destruction paths (PopupHandler::done, LayerShellHandler::closed) where waiting for the next reconcile would leave a window in which surface() / surface_mut() panic. The non-panicking siblings try_surface / try_surface_mut are added for callers on async dispatch paths (IME Done, tooltip arm) that may race a teardown. Miscellaneous. CI: mastermain to match the actual default branch. Makefile adds cargo run --example dialog to the examples target. src/lib.rs re-exports widget::scroll::ScrollAxis so apps can configure a scroll() axis without reaching into a pub(crate) module. Cargo.toml adds accesskit = "0.17" and accesskit_unix = "0.13". docs/architecture.md gains the "Known gaps and non-goals" section that enumerates the new capabilities, what still ships flat, and what is deferred (per-widget a11y labels, primary selection, intra-process multi-touch gestures, wp_fractional_scale_v1). Pedro M. de Echanove Pasquin 2026-05-16 22:09:59 +02:00
  • 4aa3480b64 refactor: split every monolithic module into focused submodules Each source file that had grown beyond a single concern is replaced by an identically-named directory containing focused submodules. src/event_loop/mod.rs (878 lines) becomes a directory with clipboard, context_menu, cursor_shape, drag, focus, handlers, invalidation, overlays_reconcile, repeat, run, surface, text_editing, and tooltip. Every widget, input handler, and theme component follows the same split. Public interfaces are unchanged — only the internal file layout moves. image bumped from 0.25.2 to 0.25.9. Pedro M. de Echanove Pasquin 2026-05-15 23:46:56 +02:00
  • 3d237039c6 input/keyboard: let the app intercept arrow / Tab keys before the default text-edit and focus-shift behaviours The keysym dispatcher used to give the focused widget first refusal on Left / Right / Up / Down / Tab. A text_edit swallowed the four arrows for cursor movement, and Tab walked the keyboard-focus ring through next_focusable_index. Apps only got on_key_with_modifiers for those keys when no text input was focused (Tab never), so a search field with autocomplete couldn't drive a selection cursor through arrows or Tab without putting focus on something else first — which is exactly the wrong UX for a search-as-you-type list. Three arms in AppData::handle_key_press now query self.app.on_key_with_modifiers( keysym, ctrl, shift ) *first* and only fall back to the default behaviour if the app returns None: - Left | Right: previously branched on is_text_input and routed the keypress to handle_cursor_left/right for text fields, otherwise asked the app. Now: app first; if app says None, the existing text-cursor path runs for text inputs and non-text widgets get nothing (their previous fallback already produced no useful action without an app handler). - Up | Down: the previous logic was a four-way decision that tried text-cursor movement, then move_keyboard_hover for combo / list widgets, then the app handler. Now the app gets first refusal too. When it declines, the original cascade (text-cursor → hover navigation) still runs, so multiline text_edit cursor walking and combo / scrollable-list keyboard nav are unchanged for any app that doesn't intercept these keys. - Tab | ISO_Left_Tab: app first; on None the focus-shift path (next_focusable_index + set_focus) runs. Apps that want Tab as a navigation message between custom UI states (a search field cycling through results, a wizard advancing pages) finally have a hook; apps that don't get the standard tab-through-focusable behaviour by leaving on_key_with_modifiers returning None for Tab, which is the trait's default. The interception is keysym-only — modifier state is forwarded so an app can distinguish Tab from Shift+Tab, Right from Ctrl+Right. The text-input check is preserved as a local before the dispatch so the fallback doesn't lose the "is this even a text edit?" question; it just runs *after* the app instead of before. Net effect: a list / autocomplete that needs arrow / Tab navigation can now be implemented purely by overriding App::on_key_with_modifiers, with no changes to text_edit itself and no risk of breaking apps that don't need it. The Up / Down branch's old comment about the "fall through to list hover-navigation" rationale is dropped — the cascade still exists, but the app-first ordering is the new contract and the comment was about the previous one. Pedro M. de Echanove Pasquin 2026-05-15 00:40:01 +02:00
  • 5ff4fa7e59 Fixed default dark theme Pedro M. de Echanove Pasquin 2026-05-14 23:49:08 +02:00
  • bfe27b6fef event_loop, widget, input: pointer-dwell tooltips, global drag coords, foreign-toplevel name cascade Button::tooltip( text ) registers a hint string that fires after a 600 ms pointer dwell. LaidOutWidget gains a tooltip: Option<String> field, Element::tooltip() exposes it to the input layer, and the existing pointer-hover path now calls arm_tooltip on hover-enter and cancel_tooltip on hover-leave / touch. The deadline is polled alongside next_long_press_wakeup in try_run so an idle pointer still gets a wake-up at the firing instant; on fire, tooltip_overlay() synthesises an OverlaySpec — a rounded text_primary @ 95% pill drawn with bg text — anchored above the hovered widget, flipping below or clamping inside the screen if it would clip, and pushed alongside the app's own overlays both in the redraw path and in reconcile_overlays so the layer surface is created the same frame the tooltip becomes visible. Pointer-only by design: touch events explicitly cancel because a tap-and-release should never linger into a hint. The showcase example wires .tooltip(..) on the three button variants as a smoke test. The drag pipeline now reports positions in main-surface (global) coordinates instead of per-surface. surface_offset_for( focus ) derives the top-left of an overlay surface from SurfaceState::layer_anchor — newly stored at reconcile_overlays time from OverlaySpec::anchor — combined with the main surface's dimensions; on_drag_move, on_drop, the synthetic move emitted on drag-promotion in pointer.rs, and the pending_drag_inits push site in gesture::start_drag all translate before handing coordinates to the app. The motion and release paths additionally request_redraw() every overlay so a dock-style drop target painted on an Anchor::Bottom layer surface gets repainted as the drag moves — without that, the visible drop indicator only updates when the cursor re-enters the main surface. Drops still target whichever surface fired the release; only the coordinates are unified. ForeignToplevelListHandler previously read app_id directly via ForeignToplevelList::info(). Clients that never set app_id (some winit-windowed compositors, simple test clients) were silently invisible to crustace-style docks because the empty string fell through unwrap_or_default() and the dock then keyed entries off "". toplevel_display_id() cascades: prefer app_id for desktop-entry matching, fall back to title for human-readable identification, and finally to the protocol-issued identifier which is always present and unique per handle. Applied to both new_toplevel and update_toplevel. theme::system_fontdb() lazily loads the system font database once via OnceLock and reuses the Arc for every decode_svg_bytes call. resvg's default Options::fontdb is empty, so any SVG containing <text> rendered with the built-in fallback font or no font at all; with the system DB attached, icons and decorative SVGs with embedded labels now resolve glyphs correctly. Cached because load_system_fonts() walks every font path on the system and is comfortably tens of milliseconds on a cold cache — not something to repeat per icon decode. themes/default/theme.json tweaks one variant's slot palette: surface-alt from @indigo/D9 to @white/D9 and text-primary from @white to @navy, plus a cosmetic re-alignment of the "value" columns in the same slots block. Pedro M. de Echanove Pasquin 2026-05-14 22:36:17 +02:00
  • 821037f509 container, text, date_picker: width-aware sizing pass Container::max_width(px) mirrors the same flag on Column / Row — the container reports min( offered, px ) upwards and the draw pass caps rect.width to px, so a decorated child wrapped in container().max_width(260) no longer needs a column()-of-one shell just to access the cap. Propagated through map_msg, covered by a test, and documented under the container section of docs/widgets.md. Text::no_truncate() opts out of the default ellipsis behaviour: when truncate = false the draw pass paints the full string even if measure(text) > rect.width. Useful for very short labels (calendar days "1"–"31", day-of-week stubs "Lu" / "Mi" / "Sá") inside grid slots whose width is dictated by the parent — a couple of pixels of overflow centred in the rect is invisible, while "..." in place of a single-digit number is loud. DatePicker::width(px) is a layout hint: when set, build() derives header_fs, dow_fs and day_fs from the slot width so the worst-case label in each row ("September 2026" in the header, "Mié" / "Sáb" in the DOW row, "30" in the day cell) fits at a sensible size; the design defaults stay as upper bounds. Day and DOW cells also flip on Text::no_truncate() as a safety net — heuristic font sizing can't perfectly predict glyph widths across families, and overflowing one pixel beats truncating to "...". Pedro M. de Echanove Pasquin 2026-05-13 19:38:41 +02:00
  • 96f437544a event_loop: route take_focus_request to widgets on overlay surfaces The take_focus_request block in try_run only searched data.main.widget_rects for the requested WidgetId, so an app that wanted to focus a widget living on an overlay surface (a search field on a launcher overlay, a text edit on a dialog modal, a password field on a popup) silently no-op'd: the widget existed and had been laid out, but the lookup never found it because it was scanning the wrong widget_rects. Crustace hit this trying to put cursor focus on the launcher search field when the launcher slides up — the field rendered with a visible caret but typing went to whoever the keyboard had been focused on before. The lookup now falls through to data.overlays.iter() if the main scan misses, returning the first (SurfaceFocus::Overlay(id), flat_idx) whose surface carries a widget with that id. data.set_focus already accepts the SurfaceFocus discriminant, so the call site just forwards what the lookup found and requests a redraw on the matching surface instead of unconditionally on main. No effect on apps that target main-surface widgets — the main scan still runs first and short-circuits the overlay walk. Pedro M. de Echanove Pasquin 2026-05-13 13:47:01 +02:00
  • c3839060cc theme, event_loop: window_controls.bar_bg slot + graceful exit on compositor disconnect WindowControlsSpec gains bar_bg, the background fill of the SSD title-bar strip the close / maximize / minimize controls sit on. Forge used to paint that strip with palette.surface clamped to alpha 1.0 — a hack on a translucent panel token. With a dedicated slot the theme decides directly: @off-white in light and a new @window-bar-dark shade in dark. Schema gets the matching Option<String> field (defaulting to palette.surface to keep existing themes rendering the same) and the fallback WindowControlsSpec seeds Color::WHITE so a missing theme still draws something readable. try_run's dispatch loop previously .expect("dispatch")-ed every calloop error. When the compositor closes the wayland socket — wl_display.error, forge crashing, the user logging out — the loop saw a BrokenPipe / ConnectionReset and panicked, which polluted the user's stderr with a backtrace for what is just "the session ended". The match now treats those two IoError kinds as a clean shutdown: it prints Connection::protocol_error() (the typed wl_display.error if the server sent one), sets exit_requested = true, and lets the loop drain out normally. Any other dispatch error keeps panicking — those are still genuine bugs. Pedro M. de Echanove Pasquin 2026-05-13 10:17:36 +02:00
  • dc781fb78d event_loop: client binding for ext-foreign-toplevel-list-v1 Adds an App callback that delivers the live list of open toplevels from the compositor — the data source a shell needs for dock running-app indicators, taskbar tiles, alt-tab and any other "what is currently running" UI. Hand-wiring the protocol binding from every shell that wants it is the kind of boilerplate ltk should absorb once: this is that move. Cargo.toml adds "staging" to wayland-protocols' feature list. SCTK 0.20 already pulls staging in transitively (it carries foreign_toplevel_list.rs and its own dispatch helper), so this is belt-and-braces against a future ltk tree that swaps SCTK for a different client toolkit — it keeps the protocol available crate-wide even then. src/app.rs introduces ToplevelEvent { Opened { id: u32, app_id: String }, Closed { id: u32 } } and App::on_toplevel_event( &self, ToplevelEvent ) -> Option<Self::Message> with the default returning None so apps that do not care pay nothing (no allocation, no dispatch). id is the Wayland protocol id of the handle proxy — unique per session, stable for the handle's lifetime, the same value paired across Opened and Closed. src/lib.rs re-exports ToplevelEvent from the public prelude. src/event_loop/app_data.rs grows a pub foreign_toplevel_list: ForeignToplevelList field. src/event_loop/mod.rs constructs it via ForeignToplevelList::new( &globals, &qh ) and stores it on AppData. If the compositor does not advertise the global the inner GlobalProxy just resolves to "absent" and the list yields no toplevels — no error path needed at construction. src/event_loop/handlers.rs adds the Proxy import, delegate_foreign_toplevel_list!( @<A: App> AppData<A> ) next to the rest, and implements ForeignToplevelListHandler for AppData<A>. The three SCTK callbacks (new_toplevel, update_toplevel, toplevel_closed) each pull the handle's protocol id and its currently-cached app_id from the list's info cache, call self.app.on_toplevel_event( … ), and push the returned message onto pending_msgs so it flows through the normal update cycle with the regular invalidate_after scoping path. update_toplevel re-emits Opened with the latest info — compositors fire this on title changes too, not just app_id changes, but apps whose state is keyed on (id, app_id) can absorb the repeat idempotently and apps that need title-change granularity can scope via invalidate_after. The wire-up is generic: a shell that wants finer behaviour (focus follow, per-title indicators, multi-window grouping) can layer on top by translating the event into more specific app messages. The default app pays zero, the shell that opts in gets a real event stream without touching smithay-client-toolkit directly. Pedro M. de Echanove Pasquin 2026-05-11 22:30:29 +02:00
  • 39fbafec24 container: generalise background from Color to Paint Container::background now stores Option<Paint> instead of Option<Color>, and the builder accepts impl Into<Paint> so callers can pass a plain Color (auto-wrapped in Paint::Solid via the trait impl) or an explicit LinearGradient / RadialGradient. layout_and_draw switches from canvas.fill_rect( rect, bg, corners ) to canvas.fill_paint_rect( rect, &bg, corners ) to consume the wider type. No behaviour change for existing call sites — solid-colour containers keep working unchanged thanks to the Into<Paint> for Color. Pedro M. de Echanove Pasquin 2026-05-11 12:20:28 +02:00
  • 703a1ed228 event_loop: honour App::window_size_hint() on the first configure Previously only set_min_size() was called with the requested size, on the assumption that the compositor would adopt it as the initial dimension. In practice that doesn't hold: xdg-shell has no "preferred initial size" primitive and compositors are free to pick any size within [min, max] on the first configure. The window ended up opening at the 800x600 fallback instead of the size the application had asked for. The fix pins min == max before the first commit, which forces the compositor to honour the requested size in its first configure, and then releases max_size from the configure handler via the pending_size_hint_unpin latch so the surface remains user-resizable afterwards. The 800x600 fallback now only applies when the application does not provide a hint. Pedro M. de Echanove Pasquin 2026-05-10 23:16:17 +02:00
  • f3621b72c9 Added examples in README.md Pedro M. de Echanove Pasquin 2026-05-10 15:23:22 +02:00
  • bbab5e238d First commit. Version 0.1.0 Pedro M. de Echanove Pasquin 2026-05-10 09:58:23 +02:00
  • af105b7f7d Initial commit admin 2026-04-23 11:08:43 +02:00