Commit Graph
Select branches
Hide Pull Requests
main
Select branches
Hide Pull Requests
main
-
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, defaultFluid). 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 byFLUID_MIN/FLUID_MAX; physical sizing (Length::dp( px )) is a constant-physical-size pixel scaled by display density (set_density/density).Lengthgainsorient( portrait, landscape )— resolve one value in portrait, another in landscape — pluswidget( px ), which picks fluid or dp per the active mode. Canvas exposesgeom_px(geometry, resolved in physical layout space) andfont_px(font size, bridging logical / physical per mode) so widgets and apps share one resolution path. Note the rename:set_design_reference/design_referencebecameset_fluid_reference/fluid_reference, andLength::dpchanged meaning — the old surface-proportional behaviour now lives onLength::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:buttongainsfont_size/height,text_editgainsheight/font_size_fluid,separatorgainspad_v, and assorted widgets accept aLengthwhere they previously took onlyf32. Overlays.OverlaySpec::sizeis 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 nowpub( 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 internaltest_supporthelpers move behind atest-supportCargo feature (off by default, so third-party builds never see them; ltk's ownmake testenables it).Separatordrops its0.0-means-mode sentinel forOption<Length>, so an explicitpad_v( 0.0 )is a real flush divider distinct from the mode-following default. Performance guardrails. Opt-in diagnostics viaLTK_PERF_WARN=1warn about stuck animations, sustained software-render animation, and lowpoll_interval; software-rendered animation is capped near 30 Hz to spare CPU on machines that fall back off EGL. Apps can override the cap withApp::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 thelocales/directory into the packaged crate (fixing i18n keys rendering raw for downstreams), builds the newresponsiveexample, and runs tests with--features test-support. mainPedro 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
glScissorclip 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 itsfill/clear_rects_transparentscissor semantics. Adds software-backend pixel tests covering the snapping. Font resolution unification. The system-font candidate chain,find_font_optandload_default_font_byteslived in two copies (render/helpersandgles_render/helpers) that had already diverged — one resolved throughfind_font_opt, the other inlined the candidate loop — and now live once insystem_fonts. The two per-backendOnceLockdefault-font caches andprimary_handlecollapse into a singlesystem_fonts::default_handle. Module docs and thefont_registrycaller 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 nowrender::helpers::validate_rgba_dims. The six manual symmetricRect-inflate literals in the GLES primitives reuse the existingRect::expand. FrameState and DrawCtx de-duplication. The elevenSurfaceStatefields the draw pass owns and threads throughDrawCtx—widget_rects, the cursor / selection maps, the scroll state,accessible_extras,prev_focused/prev_hovered/prev_pressed— move into aFrameStatesub-struct. The per-framebuild_draw_ctx/commit_draw_ctxhelpers can then borrow&mut ss.frame, disjoint fromss.canvasandss.pool, so the four frame paths (software / GLES × full / partial) replace their duplicatedDrawCtxconstruction and write-back with a single helper call each. A whole-SurfaceStateborrow could not express this (partial borrows do not cross function boundaries), which is why the helpers take the sub-struct.content_dirtystays onSurfaceState— it is an invalidation flag, not frame state — and is reset at the call site. rich_text tests. Adds the previously-missingtests.rsfor theRichTextwidget: 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, andmap_msgrange preservation — all headless against a softwareCanvas, with line counts forced by\nso they do not depend on any system font's measured width. Documentation. Fills the rustdoc gaps on the embedder-facing surface:core::UiSurfaceaccessors, theegl_contextpublic API, theGlesCanvasmethods,theme::typographyandtheme::error, and theRichText/Textbuilders; adds a crate-level "Rendering backends" overview.CHANGELOG.mdis added (0.2.0 / 0.1.0), anddocs/widgets.md/docs/cookbook.mdgainrich_text,external, and the CPU-draw / path-clip / externally-laid-out-tree recipes.debian/changeloggets the 0.2.0-1 entry. Private intra-doc links tosystem_fontsare demoted to code spans socargo docis warning-free. Packaging. Thelibltk-devregistry crate shipped aCargo.tomldeclaring thelookupbench while theMakefileinstall copied onlysrc/, so Cargo refused to parse the manifest over a missingbenches/lookup.rs; the install now shipsbenches/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.tomlis bumped to 0.2.0 to match the package version and theltk-0.2.0registry 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 likepush_placedbut clips its subtree's drawing toclip(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 8thOption<Rect>for the clip, andlayout_and_drawbrackets a clipped child withset_clip_rects/restore around the recursion. The clip is shifted by the Stack's own origin to match the placed rect (whichStack::layoutalready 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. Unlikeread_gles_rgba_pixelsit 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 liveCanvas— for an embedder's measure pass that must produce the same metrics the renderer will later use. It is backed bysystem_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 widensrender::helpers::load_default_font_bytestopub(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.yruns bottom-to-top in screen space — the layer FBO has GL's lower-left origin, andortho_rectplus the texture shader's flip establish thatv_uv.y == 1is the top edge. The composite computed the fragment's screen Y asbbox.y + v_uv.y * bbox.h, which is the inverted Y, so it read the layer mirrored about the horizontal axis. Compute it asbbox.y + (1 - v_uv.y) * bbox.hinstead. 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, aVectorDrawablemask — 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 coverageMask(Winding fill) and install it as the active clip mask. Every software primitive already threadsclip_maskthrough tiny-skia (fills, strokes, lines, paths, images, text, blit), so the path clip applies uniformly with smooth edges.clip_boundsreports 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_pathrasterises 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 viaactivate_target. Ending the clip (clear_clip/set_clip_rects/ a newset_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_paththen, after the clipped draws,clear_cliporset_clip_rectsto flush it (on GLES this is when the layer is composited). Snapshot the prior clip withclip_boundsbeforehand and restore it withset_clip_rectsto compose with an outer clip without leaking state. Add anexamples/clip_path.rsdemo (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 inapp.rs(scroll) and the new GLES doc (SoftwareCanvas::set_clip_path) demoted to code spans, and a redundant explicit link target inchassis.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_pathover a newPathCmdcommand 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 AndroidPath, aVectorDrawable, or a Lottie frame.ExternalSource::Cpu(and theExternal::cpuconstructor) 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 customView.onDrawstraight onto the ltk canvas without a GL texture round-trip, unlike the existingTexturesource which only renders on GLES.Stack::push_placedappends 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. NewRichTextwidget: 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 AndroidSpannedcarryingURLSpan/ClickableSpan. gesture: only drive the horizontal pager once the axis locks horizontalon_moveemitted horizontal swipe progress wheneverswipe_axis != Vertical, which includes the pre-lock window whereswipe_axisis stillNone(the first ~8 px of travel). Buthorizontal_drag_startedonly flips oncedx.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 withoutdxever passing 8, sohorizontal_drag_startedstayed false. On release the horizontal branch was skipped, noon_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 onceswipe_axis == Some(Horizontal). Locking onto the horizontal axis already impliesdx.abs() > 8, sohorizontal_drag_startedis 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. Addspre_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-v1token from the compositor and hand it to a child it is about to launch (the$XDG_ACTIVATION_TOKENconvention). 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 newApptrait methods, both defaulting to no-op:take_activation_requestsreturns the tags the app wants tokens for this iteration, andon_activation_tokendelivers the issued token paired with its tag. The run loop drains the requests right afterpoll_externaland callsActivationState::request_token, carrying the tag inRequestData::app_id;ActivationHandler::new_tokenreads the tag back out and routes the token to the app throughon_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.
SubsurfaceSpecgainsparent: SubsurfaceParent { Main, Overlay(id) }so a sliding panel can ride above app windows the way an overlay panel does, and an optionalgpu: boolso content that uses thesurface-panelbackdrop-filter glass (a GLES-only pass) keeps it while sliding instead of dropping to the software rasteriser.reconcile_subsurfacesresolves each spec's parent independently — skipping anOverlayparent 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 sharedAppData::subsurface_gles_canvasreused 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 (newSwipeAxis) 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 finalprogress = 0.0cancellation 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-affectingLengthvalues (widths, paddings, gaps, widget sizes, includingVw/Vh) now resolve against the physical viewport via the newCanvas::viewport_layout()— the space the layout tree is actually computed in — so aVw(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_widgetandvslidernow documentLength::vw/vhsizing 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 pendingup/cancel, so the sharedreset_touch_state()(also used by thewl_touch.cancelhandler) drops the strandedprimary_touch_id/ slot state across the main surface and every overlay, keeping the first post-resume gesture clean. Also drops an accidental duplicateon_scale_changedcall 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 byWidgetIdinwidget_rects. Read-onlyTextEditwidgets are not tracked as interactive and therefore absent fromwidget_rects. A swipe-reveal flips the fields from read-only to interactive onSlideRevealed, but an intervening input event (e.g. the touch-up at the end of the swipe) can causedispatch()to return before the vblank frame callback clearsframe_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.AppDatagains afocus_retryfield. Whentake_focus_request()(or a previous retry) returns an id but the widget is not inwidget_rects, the id is stored infocus_retry,view_dirtyis 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_onlyso 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::Swipecallsdirty_caches()+request_redraw()per motion sample, and theWlCallbackMain handler setsview_dirty+request_redraw()on every frame callback whileis_animating. The subsurface reconcile already repositions the panel with a cheapset_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-inApp::subsurface_motion_only(defaultfalse, so existing apps are unaffected). When it returnstrue: - the swipe dispatch (input/dispatch/outcomes.rs) still calls theon_swipe_progressfamily but skipsdirty_caches()/request_redraw(); incoming motion events pump the loop and the per-iterationreconcile_subsurfacescarries the move. - the frame-callback animation pump (event_loop/handlers.rs) keeps the vsync cadence without a re-raster by requesting a barewl.frame()+commit()on the main surface (no buffer attach) instead of dirtying the view;poll_externaladvances 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
downas 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 freshdownon the surface now under the finger (smithay pins touch focus atdownand won't re-target on motion). Thatdownarrives for a slot the main surface already holds as its primary, with the drag state already migrated here. Handling it through the normaldownpath would either restart the gesture or, since the slot is taken, demote it to an auxiliary touch — both abandon the in-flight drag.TouchHandler::downnow early-returns when the focused surface'sprimary_touch_idalready equals the incoming slot, so the redundantdownis a no-op and the following motion/up keep driving the migrated drag. Completes the cross-surface drag fix whose other half (migratingprimary_touch_idon 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::focusandWindows::activated— plus a hack that smuggled X11 surfaces throughLayers::focus, with the keyboard target derived each frame from alayers.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. Newwindows/focus.rswithFocusTarget { Layout(Weak<Window>), Layer(WlSurface, app_id), X11(WlSurface) }.Windowsnow holds onefocus_target: Option<FocusTarget>as the single source of truth;activatedsurvives only as a reconciliation cache for the xdgActivatedstate and is never set by hand.set_focustakes oneOption<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 nextfocus().focus()resolves a baseline (the active layout's window, which carriesActivated) 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::focusis removed and X11 no longer rides inside it; the exclusive-keyboard layer path andreap_layerdrivefocus_targetdirectly, and every click/touch/activation site collapses toset_focus(Some(FocusTarget::…)). surface_at popup tie-break. When a layout hit is a popup or subsurface, itswl_surfaceis not tracked intoplevel_z_order, so the layout-vs-X11 z compare resolved toNoneand 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'sFocusTarget::Layoutweak, 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 newraise_toplevelhelper; 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 throughset_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_upnow 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 Newsrc/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 andWallpaperBundleprimitives.set_default_theme(mode)finds, installs and activates thedefaulttheme 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, andtheme_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 aWallpaperBundle, 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 andWallpaperBundlealready expose — but it removes the per-applicationload_theme_logo/build_wallpaper_bundle/ theme-bootstrap duplication. ltk: animatable, input-transparent child surfaces via App::subsurfaces() NewApp::subsurfaces() -> Vec<SubsurfaceSpec<Msg>>(default empty) describes input-transparent child surfaces composited over the main surface, withSubsurfaceSpec { id, view, x, y, content_version }and the stableSubsurfaceId. 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.SubcompositorStateis bound inevent_loop/run.rsfrom the compositor'swl_compositor(absent →App::subsurfacessilently degrades to none);delegate_subcompositor!is added onAppData, which gains asubcompositorbinding and asubsurfacesmap. Newevent_loop/subsurface.rsreconciles the live subsurfaces against the specs: each spec becomes awl_subsurfacesized 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 andCanvas, drawn through the existingDrawCtx/layout_and_drawpath — is rasterised only when the surface size or the spec'scontent_versionchanges; a position-only change emitswl_subsurface.set_positionand 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 logicalset_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 interpretspxagainst a configurable reference vmin (default 412 px, the eydos mobile reference width) and returnsVmin( 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 viaset_design_reference()and read viadesign_reference()(stored as f32 bits in an AtomicU32); both are re-exported fromlib.rs. Make container and grid insets relative.Container's four padding fields becomeLengthinstead off32; every setter (padding,padding_h,padding_v,padding_top/right/bottom/left) now takesimpl Into<Length>, so existingf32call sites keep compiling via theFrom<f32>shim. The values are resolved against the viewport inContainer::preferred_sizeand in the container draw path (draw/layout.rs).WrapGrid'sspacing_x,spacing_yandpaddingget the same treatment, with aresolved( canvas )helper funnelling the per-frame resolution andgrid()seedingLength::pxdefaults. Container tests now compare againstLength::px( … ). == Scrolling ==Scroll::preferred_sizeis 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 aColumn; vertical and both-axis scrolls keep the spacer-like( max_width, 0.0 ).Column's space-distribution correspondingly treats aScrollas 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.jsongains 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, andavatar-1…avatar-9. == Cursors == Bundle GNOME's Adwaita cursor theme — the cursors GNOME Shell uses — intothemes/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 siblingcursor.thememakes the tree a valid XCursor theme. The existingltk-theme-default.installcopiesthemes/defaultrecursively, so the directory ships with no packaging change. Applications keep declaring aCursorShapeper widget overwp_cursor_shape_v1; the compositor resolves it against the active theme'scursors/directory by name, and the set covers all 34CursorShapevariants. Document the set inthemes/default/cursors/README.md(what it is, the XCursor layout, the full shape list, how the compositor consumes it, guidance for forks) andthemes/default/cursors/LICENSE.md(attribution and licence options, modelled on the icons catalogue LICENSE).lib.rslists the cursors in its third-party-assets section. Close out licensing indebian/copyright: aFiles: 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. Addtests/cursor_assets.rs: everyCursorShapename resolves to a valid XCursor file (Xcur magic, following symlinks),cursor.themeis 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
enterand 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 theenterevent by re-emitting enable + content type + commit. The compositor sendsenterwhen 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 ignoredenter, 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'ssecureflag: secure fields are flaggedContentPurpose::PasswordwithContentHint::SensitiveData | HiddenText(so the IME/OSK skips prediction, autocorrect and storing the value), everything else staysNormal/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 newAppData::text_input_securelets theenterre-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_sizeand input regions were handed physical (layout-space) pixels straight to layer-shell andwl_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_regionnow takes the surface scale and divides eachRectdown to logical before adding it to the region; the four draw paths (software and gles, full and partial) forward their scale.reconcile_overlaysconvertsOverlaySpec::sizeto logical for bothset_sizeand the initialOverlayConfig(0 = fill survives the divide), and seeds the new surface'sscale_factorfrom the parent so the first configure allocates a HiDPI buffer instead of rendering at scale 1 untilscale_factor_changedlands 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
Appcan be a screen locker, alongside the existing xdg-shell window and wlr-layer-shell surfaces. A newShellMode::SessionLockmakesrun()bindext_session_lock_manager_v1and request the lock at startup; the lock surface itself is created in the newSessionLockHandler::lockedcallback (one surface on the first advertised output) and replaces theSurfaceKind::PendingLockplaceholder the main surface holds until the compositor grants the lock. Theconfigureevent routes through the sameon_configurepath as layer and xdg surfaces, so sizing and rendering are unchanged, andfinished(the compositor denied or ended the lock) tears the loop down. The whole thing is additive and opt-in: theWindowandLayerpaths are untouched and nothing enters lock mode unless anAppreturnsShellMode::SessionLock, so existing apps are unaffected — the only non-additive edits are the two exhaustivematches onSurfaceKind(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 offscreenUiSurfaceis 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 ofupdates. It exists because of the one hard invariant ofext-session-lock-v1: a locker that disconnects without sendingunlockleaves the compositor's outputs blanked forever — that is the protocol's deliberate anti-bypass guarantee. So whenrequested_exit()returns true and the surface is a session lock, the loop callssession_lock.unlock()and round-trips the connection before settingexit_requested, lifting the lock cleanly; for aWindoworLayersurface there is no lock and it simply exits. The consequence for lock apps is that they must stop callingprocess::exitfrom the lock path and instead flip a flag they return fromrequested_exit().text_editgains aread_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_focusableandElement::is_text_inputnow return false for a read-onlyTextEdit, 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 throughmap_msgso it survivesElement::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. Theshell_mode()doc comment and the README now list theSessionLocksurface type and point atrequested_exit()for the unlock. Two warnings are cleared along the way: the runtime no longer stores theSessionLockStateafter requesting the lock — it has noDrop, so the manager object outlives the dropped handle inside the connection and the lock lifecycle runs entirely off the returnedSessionLock, which removes a never-read field — and a pre-existing rustdocprivate_intra_doc_linkswarning inlist_item(a public doc comment linking to the privatetheme::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::configureandWindowHandler::configurecalledself.on_configure( w, h )— which already forwardsapp.on_resize( w * sf, h * sf )in physical pixels — and then immediately calledself.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 meantscreen_width/screen_heightcame 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 redundantapp.on_resizefrom both handlers;on_configureis 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
Lengthso any size, padding, spacing or font height can scale with the surface instead of being frozen at a px constant, fixtext::preferred_sizeto 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 singlef32spacing constant that worked at the dev resolution and broke at the smaller one, andtext::Text::preferred_sizewas returningascent - descentfor the line height — fontdue's terminology for "the minimum bounding box of an unaccented line", which deliberately drops theline_gapthat 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-screenSizinghelper in their own view code.types::Length(with theLengthBaseenum 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. Optionalmin_px/max_pxbounds attach to the sameLengthvalue via.clamp( lo, hi )(both ends),.at_least( lo )and.at_most( hi )(one-sided); the names are intentionally divergent fromf32::min/f32::maxto 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 rawf32rather than nestedLengthvalues, which keepsLengthCopyand avoids aBoxallocation 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>andFrom<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. TheEM_BASE_DEFAULT = 16.0constant matchestheme::typography::BODYsoLength::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 bydpi_scaleand falls back to physical size whendpi_scale <= 0.0, guarding the misconfigured-canvas path so a Vmin call doesn't poison every downstream measurement withNaNorinf. The viewport is in **logical** pixels — matching what every waylandxdg_toplevel.configureevent already hands the client — soLength::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 anf32size, padding, spacing, max-width, or fixed dimension now takesimpl Into<Length>and stores the value asLength: -widget::text::Text::size( impl Into<Length> ); thesizefield is nowLength.Text::resolved_size( &Canvas )is the internal accessor that every measurement / drawing path routes through, so the field can stayLengthwithout churning the call sites.preferred_sizeanddrawnow readnew_line_size = ascent - descent + line_gapfrom fontdue'sLineMetrics(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_widthare nowOption<Length>. Newresolved_height( &Canvas )/resolved_width( &Canvas )helpers replace the directs.fixed_height.unwrap_or( 0.0 )reads inlayout::column,layout::rowandlayout::stack.Spacer::preferred_sizegrows a&Canvasparameter for the same reason;Element::preferred_sizepasses the canvas through. -layout::Column::spacing/.padding/.max_width,layout::Row::spacing/.padding— all takeimpl Into<Length>and storeLength. Internalresolved_spacing( &Canvas ),resolved_padding( &Canvas ),resolved_max_width( &Canvas )helpers funnel every read, so the layout code paths stay readable. The column'sinner_wprivate helper picks up a&Canvasargument; the test that used it directly is updated.theme::typographykeeps its historicf32constants (H0…BODY_XS, plusLINE_HEIGHT) so the migration is gradual, and adds a parallel responsive scale exposed as functions returningLength:h0(),h1(),h2(),h3(),body(),body_s(),body_xs(). Each is aLength::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 intheme::typographyexercise all three regimes (narrow phone, calibration point, large display) so future drift in the percentages or clamps is caught immediately.Canvas::viewport_logicalis 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. Thedpi_scaleaccessor 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 astypes::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 reportsNone),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), andwidget::text::tests(3 updated/new — defaults compare againstLength::px(16.0),.size( f32 )and.size( Length )both verified). The existing integration test intests/layout_stack_spacer.rsis updated to callSpacer::preferred_size( &canvas )and comparefixed_height/fixed_widthagainstSome( Length::px( n ) ). Documentation is updated end-to-end so the new API is discoverable fromcargo docwithout grepping the source:lib.rsgets a new entry forLengthunder the **Types** section and a new **Designing for multiple resolutions** section that lists the three patterns (relativeLengthfor sizing, responsive typography for hierarchy,view()-level branching on surface dimensions only when the structure itself must change).Canvas::viewport_logicalships with a runnableassert_eq!example covering the scale-2 case. The module-level docstrings forSpacer,ColumnandRownow show both anf32example (legacy, still valid) and aLength::vmin( ... ).clamp( ... )example for the responsive variant —cargo docrenders 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, andwidget::image::Image::sizestill takef32. None of them are on a critical responsive path right now, theFrom<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 intext::preferred_sizealready benefitsTextEditindirectly 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.svgglyph In boththemes/default/branding/dark/launcher.svgandthemes/default/branding/light/launcher.svgthe launcher glyph is built from nine cells, each with its ownfilterN_diiii_4479_*. Those filters layered four successive inner-shadow passes (effect2..effect5_innerShadow_*) on top of the outer drop shadoweffect1_dropShadow_*: pairs offeColorMatrix 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 modesplus-lighter,overlayandnormalto 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_*), theclipPathwithbgblur, therectelements 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 sameviewBoxand 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 thedarkandlightvariants is preserved (only the filter identifiers differ,_38862versus_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
WrapGridwithcentre_last_row( bool ). When set, layout offsets a row that has fewer thancolumnschildren by(missing * (cell_w + spacing)) / 2so 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 theCarouselwidget atsrc/widget/carousel/. It is a pure layout primitive:focused_width_frac(0.05–1.0, clamped),gapandoffsetare 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 atbase_x + idx * (child_w + gap)and the full viewport height;snap_offset( viewport_w, idx )translates index to centring offset andfocused_index( viewport_w )rounds the current offset back to the nearest tile. Plumbed intoElement::Carouselwith the matching arms inwidget/element.rsand walker indraw/layout.rs; re-exported asltk::{ Carousel, carousel }. Covered by nine unit tests (layout, offset shift, snap / focus round-trip, frac clamp, child height) plus acargo run --example carouseldemo with Prev / Next / arrow-key navigation against an external offset state. The example is wired into theexamplesMakefile target. Updates the widget catalogue and thewidget/mod.rslanding comment to list the carousel under "Clipping wrappers" and to mentioncentre_last_rowin 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_committedis a new trait hook fired exactly once, immediately after the very firstwl_surface.commitof a rendered buffer on the main surface.AppDatagrows afirst_frame_committed: bool,draw_framenow returns whether this call performed that first commit, andtry_runinvokes 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_idin the foreign-toplevel-list handler no longer falls back toinfo.titleorinfo.identifierwheninfo.app_idis empty. Smithay creates eachext-foreign-toplevel-list-v1handle withapp_id = ""andinit_new_instanceflushes adoneimmediately, so subscribers used to see the protocol-level identifier (a 32-charAlphanumericrandom token) as the "app id" of every new toplevel — and remained stuck on it whenever the client's realset_app_idarrived between done events but the subscribing app's matcher couldn't resolve it to a.desktopentry. Returning the rawapp_id(empty or not) makes that first transientdoneignorable by the consumer's own empty-string guard; the seconddone, 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 sizeStack::preferred_sizeunconditionally 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 ofcard_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 newStack::fit_content()builder mirrors theColumn::fit_contentflag — when set,preferred_sizereturns the max of children's intrinsic widths and heights instead of claiming the parent'smax_width, with the same "skip filler widgets that themselves claimmax_width" exclusion list (Spacerwith nofixed_width,Separator,Scroll,ProgressBar,Slider,TextEditwithoutfixed_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 sizeStack::preferred_sizeunconditionally 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 ofcard_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 newStack::fit_content()builder mirrors theColumn::fit_contentflag — when set,preferred_sizereturns the max of children's intrinsic widths and heights instead of claiming the parent'smax_width, with the same "skip filler widgets that themselves claimmax_width" exclusion list (Spacerwith nofixed_width,Separator,Scroll,ProgressBar,Slider,TextEditwithoutfixed_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-depswas 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) andtext_shaping(module) are allpubin their own modules, but thewidgetandevent_loopparents are private to the crate root, so rustdoc treats them as private when resolving links from items that ARE on the public surface (ScrollAxis, thescrollconstructor,font_bytes, thetext_editmodule 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. "theevent_loop::text_editingprivate module", "see thetext_shapingprivate module"). The remaining three warnings werepalette/surfacelinking ambiguously between a module of that name and a function of the same name withincrate::theme. Adding()after the identifier inside the brackets (palette→palette(),surface→surface()) 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 thiscargo doc --no-depsruns 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_returnpreviously routedReturnto either the focused or hovered widget's submit/press message and, only when neither index existed, fell through toapp.on_key_with_modifiers. If a stalehovered_idx(left over from a prior screen) pointed at a widget that exists in the newwidget_rectsbut exposes no submit or press handler,target.is_some()was true and the message-less dispatch silently swallowed the key — theelsebranch never ran andApp::on_keynever saw the keysym. This manifested in the eydos-loginmanager greeter asEnteron the Lock screen failing to fireMessage::Unlockafter a pause/resume cycle, until the user clicked something to refreshhovered_idx. Track whether the widget path actually pushed a message and, when it didn't, fall through to the app-level handler so theReturnkeysym still gets a chance to be interpreted by the application'son_key. No behaviour change when the focused/hovered widget does provide asubmit_msgorpress_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_runplumbing: an optional global is bound at startup, a piece of state is added toAppData, the run-loop iteration drains an inbox / pushes a frame snapshot, and the public surface gains a small set of opt-inApphooks. 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 newsrc/a11y/module owns the platform adapter and the inboundActionRequestchannel.A11yState::try_newconstructs anaccesskit_unix::Adapter; when the AT-SPI2 daemon is not on the session bus (headless CI, locked-down compositors) the constructor returnsNoneand the rest of the pipeline runs unchanged. After every successfuldraw_frame, the run loop builds a freshaccesskit::TreeUpdatefromwidget_rectsand pushes it through the adapter — main surface plus every visible overlay, each translated to global coordinates viasurface_offset_forso screen readers report positions in the same frame the user sees. Buttons / toggles / checkboxes / radios / list items / sliders / text edits map to the matchingRoles;ClickandFocusactions 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 indocs/architecture.mdunder "Known gaps and non-goals": hierarchical nesting, per-widget accessible names, live regions andAction::SetValueare listed as the natural follow-ups that the foundation now supports but does not yet wire. Cross-application clipboard viawl_data_device_manager. A newsrc/event_loop/data_device.rsbridges the existing process-localclipboard: Stringto the Wayland selection. Outbound (Ctrl+C / Cut): after the local clipboard is populated,publish_clipboard_selectioncreates aCopyPasteSourceofferingtext/plain;charset=utf-8and installs it as the seat's selection;DataSourceHandler::sendwrites the cached string into the fd the peer hands us. Inbound (Ctrl+V from another app):DataDeviceHandler::selectionasks for the offered text viaWlDataOffer::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 anmpsc::Senderthat the run loop drains each iteration intodata.clipboard. Theclipboard: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 samedata_devicemodule handlesDragOfferenter / 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, andon_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 newApphooks 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_runreads$XDG_ACTIVATION_TOKENfrom the environment, removes it immediately (single-use; preventing leaks into child processes) and stashes it onAppData::activation_token_pending. After the first successful configure of the main surface — the earliest point at whichxdg_activation_v1.activateis meaningful — the token is consumed once and the surface raised to focus. Compositors without the global leaveactivation_stateasNoneand the inbound path silently degrades. AnApp::request_activation_tokenoutbound path is reserved on the trait but not yet exercised here. HarfBuzz shaping. A newsrc/text_shaping.rs::shape_linedrives both renderers: the logical-order string is run throughunicode-bidi, split into per-font sub-runs, and shaped throughrustybuzz. EachPositionedGlyphcarries the per-fontglyph_id, the visual advance and the ink offsets — exactly whatfontdue::Font::rasterize_indexedneeds 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_REDon ES3,GL_LUMINANCEon ES2) — the fragment shader samples.rfor both, sinceGL_LUMINANCEreplicates the coverage byte into.r=.g=.b. Software path follows the same key. NewCargo.tomldeps: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 rawwl_touch.idof 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::frameextracted fromdraw/mod.rs. Thedraw_frameorchestrator and its per-format SHM helper (pick_shm_format) move intosrc/event_loop/frame.rs, leavingdraw/strictly responsible for per-surface paint primitives. The import inevent_loop/run.rsis rewritten accordingly;draw/mod.rsshrinks 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 wayreconcile_overlaysdoes. Used by the compositor-driven destruction paths (PopupHandler::done,LayerShellHandler::closed) where waiting for the next reconcile would leave a window in whichsurface()/surface_mut()panic. The non-panicking siblingstry_surface/try_surface_mutare added for callers on async dispatch paths (IMEDone, tooltip arm) that may race a teardown. Miscellaneous. CI:master→mainto match the actual default branch.Makefileaddscargo run --example dialogto the examples target.src/lib.rsre-exportswidget::scroll::ScrollAxisso apps can configure ascroll()axis without reaching into apub(crate)module.Cargo.tomladdsaccesskit = "0.17"andaccesskit_unix = "0.13".docs/architecture.mdgains 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_editswallowed the four arrows for cursor movement, and Tab walked the keyboard-focus ring throughnext_focusable_index. Apps only goton_key_with_modifiersfor 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 inAppData::handle_key_pressnow queryself.app.on_key_with_modifiers( keysym, ctrl, shift )*first* and only fall back to the default behaviour if the app returnsNone: -Left | Right: previously branched onis_text_inputand routed the keypress tohandle_cursor_left/rightfor text fields, otherwise asked the app. Now: app first; if app saysNone, 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, thenmove_keyboard_hoverfor 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 multilinetext_editcursor walking and combo / scrollable-list keyboard nav are unchanged for any app that doesn't intercept these keys. -Tab | ISO_Left_Tab: app first; onNonethe 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 leavingon_key_with_modifiersreturningNonefor Tab, which is the trait's default. The interception is keysym-only — modifier state is forwarded so an app can distinguishTabfromShift+Tab,RightfromCtrl+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 overridingApp::on_key_with_modifiers, with no changes totext_edititself 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.LaidOutWidgetgains atooltip: Option<String>field,Element::tooltip()exposes it to the input layer, and the existing pointer-hover path now callsarm_tooltipon hover-enter andcancel_tooltipon hover-leave / touch. The deadline is polled alongsidenext_long_press_wakeupintry_runso an idle pointer still gets a wake-up at the firing instant; on fire,tooltip_overlay()synthesises anOverlaySpec— a roundedtext_primary @ 95%pill drawn withbgtext — 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 inreconcile_overlaysso 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. Theshowcaseexample 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 fromSurfaceState::layer_anchor— newly stored atreconcile_overlaystime fromOverlaySpec::anchor— combined with the main surface's dimensions;on_drag_move,on_drop, the synthetic move emitted on drag-promotion inpointer.rs, and thepending_drag_initspush site ingesture::start_dragall translate before handing coordinates to the app. The motion and release paths additionallyrequest_redraw()every overlay so a dock-style drop target painted on anAnchor::Bottomlayer 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.ForeignToplevelListHandlerpreviously readapp_iddirectly viaForeignToplevelList::info(). Clients that never setapp_id(some winit-windowed compositors, simple test clients) were silently invisible to crustace-style docks because the empty string fell throughunwrap_or_default()and the dock then keyed entries off"".toplevel_display_id()cascades: preferapp_idfor desktop-entry matching, fall back totitlefor human-readable identification, and finally to the protocol-issuedidentifierwhich is always present and unique per handle. Applied to bothnew_toplevelandupdate_toplevel.theme::system_fontdb()lazily loads the system font database once viaOnceLockand reuses theArcfor everydecode_svg_bytescall. resvg's defaultOptions::fontdbis 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 becauseload_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.jsontweaks one variant's slot palette:surface-altfrom@indigo/D9to@white/D9andtext-primaryfrom@whiteto@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 onColumn/Row— the container reportsmin( offered, px )upwards and the draw pass capsrect.widthtopx, so a decorated child wrapped incontainer().max_width(260)no longer needs acolumn()-of-one shell just to access the cap. Propagated throughmap_msg, covered by a test, and documented under thecontainersection ofdocs/widgets.md.Text::no_truncate()opts out of the default ellipsis behaviour: whentruncate = falsethe draw pass paints the full string even ifmeasure(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()derivesheader_fs,dow_fsandday_fsfrom 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 onText::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_requestto widgets on overlay surfaces Thetake_focus_requestblock intry_runonly searcheddata.main.widget_rectsfor the requestedWidgetId, 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 wrongwidget_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 todata.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_focusalready accepts theSurfaceFocusdiscriminant, 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
WindowControlsSpecgainsbar_bg, the background fill of the SSD title-bar strip the close / maximize / minimize controls sit on. Forge used to paint that strip withpalette.surfaceclamped to alpha 1.0 — a hack on a translucent panel token. With a dedicated slot the theme decides directly:@off-whitein light and a new@window-bar-darkshade in dark. Schema gets the matchingOption<String>field (defaulting topalette.surfaceto keep existing themes rendering the same) and the fallbackWindowControlsSpecseedsColor::WHITEso 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 aBrokenPipe/ConnectionResetand panicked, which polluted the user's stderr with a backtrace for what is just "the session ended". The match now treats those twoIoErrorkinds as a clean shutdown: it printsConnection::protocol_error()(the typedwl_display.errorif the server sent one), setsexit_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
Appcallback 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.tomladds"staging"towayland-protocols' feature list. SCTK 0.20 already pulls staging in transitively (it carriesforeign_toplevel_list.rsand 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.rsintroducesToplevelEvent { Opened { id: u32, app_id: String }, Closed { id: u32 } }andApp::on_toplevel_event( &self, ToplevelEvent ) -> Option<Self::Message>with the default returningNoneso apps that do not care pay nothing (no allocation, no dispatch).idis the Wayland protocol id of the handle proxy — unique per session, stable for the handle's lifetime, the same value paired acrossOpenedandClosed.src/lib.rsre-exportsToplevelEventfrom the public prelude.src/event_loop/app_data.rsgrows apub foreign_toplevel_list: ForeignToplevelListfield.src/event_loop/mod.rsconstructs it viaForeignToplevelList::new( &globals, &qh )and stores it onAppData. If the compositor does not advertise the global the innerGlobalProxyjust resolves to "absent" and the list yields no toplevels — no error path needed at construction.src/event_loop/handlers.rsadds theProxyimport,delegate_foreign_toplevel_list!( @<A: App> AppData<A> )next to the rest, and implementsForeignToplevelListHandlerforAppData<A>. The three SCTK callbacks (new_toplevel,update_toplevel,toplevel_closed) each pull the handle's protocol id and its currently-cachedapp_idfrom the list's info cache, callself.app.on_toplevel_event( … ), and push the returned message ontopending_msgsso it flows through the normalupdatecycle with the regularinvalidate_afterscoping path.update_toplevelre-emitsOpenedwith the latest info — compositors fire this on title changes too, not justapp_idchanges, but apps whose state is keyed on(id, app_id)can absorb the repeat idempotently and apps that need title-change granularity can scope viainvalidate_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 touchingsmithay-client-toolkitdirectly.Pedro M. de Echanove Pasquin
2026-05-11 22:30:29 +02:00 -
39fbafec24
container: generalise
backgroundfromColortoPaintContainer::backgroundnow storesOption<Paint>instead ofOption<Color>, and the builder acceptsimpl Into<Paint>so callers can pass a plainColor(auto-wrapped inPaint::Solidvia the trait impl) or an explicitLinearGradient/RadialGradient.layout_and_drawswitches fromcanvas.fill_rect( rect, bg, corners )tocanvas.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 theInto<Paint>forColor.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 onlyset_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 pinsmin == maxbefore the first commit, which forces the compositor to honour the requested size in its first configure, and then releasesmax_sizefrom the configure handler via thepending_size_hint_unpinlatch 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