Repository navigation
Scroll panes: move surfaces instead of repainting windows - #192
Merged
Merged
Conversation
A registered frame callback stopped draw from requesting frames, so a window with one was never paced. Keep one outstanding request per surface instead, and rebuild a surface's accessible tree only when something has painted since the last one. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
A window update no longer re-appends the root surface layer, which repainted the whole window on every subsurface style request. A surface that only moved is repositioned without re-installing its draw content. A client's opaque region is carried onto its layer so the blurred backdrop leaves it out. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
Window::request_frame_damaged reports what a frame changed; Window::set_opaque_region declares opaque pixels. ScrollSurfaces gains input passthrough, hide, a state-driven sync that paces band moves to presented frames, and a thumb stretched rather than repainted while it squashes. Adds the damage_probe and virtual_fling examples used to measure it. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
Each Miller column is a scroll pane of its own, moved by the compositor instead of repainted; the window keeps the column ground, tint and status line. List and grid report only the file area on a scroll, a landed thumbnail repaints only its column, and the file area is declared opaque so the frost skips it. The pane-subsurface flag and the in-window column rows are gone. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
A surface whose position its client sets through the style protocol was never on the pixel grid and so always bicubic, sixteen texels a pixel on every step of a scrolled band. Drawn 1:1 it is now bilinear, and a one-pixel buffer nearest. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
ScrollSurfaces bands along either axis, holds panes as a container, snaps to whole pixels and gives a pass-through clip a one-pixel buffer. Adds RowLayout and GridLayout in content coordinates, ScrollPane and ScrollContent over a view and its surfaces, ScrollGroup for gesture routing, and the scroll_nested_probe example. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
The pane's surfaces are transparent over the ground the window paints and pass the pointer through, so every event is hit-tested in window coordinates and the per-surface translation goes. The window marks the pane's ground opaque, so the frost is not computed under it. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
The column stack is a horizontal scroll container clipped to the file area, and each column a vertical pane placed once inside it, so a pan moves one surface instead of re-cropping every column and repainting the one under the sidebar. The stack's bar is the container's own, and the preview's player rides in the stack. virtual_fling can drive the horizontal axis. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
Everything that moves with the stack now lives in it: a column's tint is the colour of its clip, the hairline down its edge is a stretched pixel, and its status line and the docked preview are surfaces beside the columns. The window draws nothing that moves with a pan, so a pan no longer repaints it, and the scene loses its per-column layers. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
The grid's cells, headings, hit test, visible range and marquee, and the row strips of the list and the columns, are the kit's RowLayout and GridLayout mapped into the file area, in place of their own section walk and closed forms. The kit's grid reserves its padding on both sides and its marquee catches what a flat sweep crosses. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
ScrollSurfaces::set_highlight and ScrollPane::set_highlight mark an item with a rounded wash on a surface of its own between the pane's ground and its band. Moving the selection slides it there and repaints nothing; a scroll moves it with the content. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
On its surface the palette's rows are a scroll pane inside the card: scrolling the list moves the pane's band, and the card is painted again only when its field, prompt, message or size changes. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
The result rows are a scroll pane inside the card and the selection is the pane's sliding highlight, so moving the selection or scrolling repaints neither the card nor the rows. A wheel or touchpad over the card now scrolls the list. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
The category panes are vertical scroll panes placed side by side in a horizontal container that follows the picker's own paging, with the selected cell marked by a pane highlight. A swipe, a scroll or a new selection moves surfaces; the card is repainted only for its field, tab marker and footer. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
ScrollPane remembers the pointer from its pointer and wheel calls and answers hovered(): the content point under a still pointer through the scroll as it is now, so a selection can follow a fling after the fingers lift. update_container_at moves a container to an offset decided by other physics. Fill is a public one-pixel colour surface, stretched and rounded by the compositor, and the highlight is built on it; the highlight no longer slides while the content scrolls, where it trailed out of the pane. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
The launcher and emoji follow the pointer through ScrollPane::hovered instead of tracking it themselves, emoji's stack is a kit container moved with update_container_at, and otto-files' dividers are kit fills. A palette scroll on its own surface no longer repaints the Files window, and the launcher and emoji cards keep painting only while a transition runs rather than after every caret blink. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
ScrollSurfaces destroys its surfaces when dropped, and a destroyed subsurface releases its EGL surface first and forgets its frame bookkeeping. A frame unanswered for 500 ms no longer holds the next one back. A band claims its size after the buffer that fits it, and the thumb is claimed again when the scale lands. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
PlacedSurface is the painted child surface otto-files kept for itself: placed and sized through the style protocol at the fractional scale, claiming a size behind the buffer that fits it, painting only for a new key and never ahead of the last frame, and withdrawing its input while hidden. otto-files' status lines, preview, video, palette card, catcher and Quick View use it, and so follow the output's real scale. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
A column scroll never repaints the window, even on a pass where its band was held for the last frame: the compositor's answer wakes the loop. A held surface paint keeps the idle tick running instead of forcing a window repaint, and a thumbnail landing in the list or grid repaints the file area alone. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
The palette is always on its own surface, so the window no longer paints a fallback card or gates its pointer, scroll and sync on a setting. The Quick View and palette trace output and OTTO_FILES_NO_BLUR are gone, and stale comments follow the code. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
Key and pointer handlers only change the browser; the update loop that runs after every batch decides whether that repaints the window, the file area or only surfaces. Changes that repainted only through a handler's explicit render now mark the browser dirty: an emptied search field, the rubber band's edge, the pane a context menu moves the keyboard to, a place picked again, a palette note cleared. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
app.rs becomes app/, one file per concern: the browser's methods by what they do (navigation, selection, the palette session, file operations, Quick View and so on), the app's lifecycle, keys, pointer and presentation, and each test module in a file of its own. The code moves unchanged; methods are visible across the app module. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
The thousand-line pointer closure becomes Browser::on_pointer: whatever is up over the listing (a rename, Quick View, the sheet, the fields, the footer, the path bar) answers first, each a method of its own, then the listing by kind of event. A drag start and the context menu come back as work to do once the browser is let go of. Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Scrolling in Otto's own apps now moves surfaces instead of repainting windows. A scroll or pan in Files, Settings, the launcher or the emoji picker is presented by the compositor moving a subsurface. The frosted window behind it is left alone.
This PR has four parts. Each builds on the one before it.
Depends on nongio/layers#28.
Cargo.tomlpointslaye-rsat that branch. Re-pin it to the release tag before merging.1. Compositor: damage only what changed
src/state/mod.rs).update_window_view_for_commitused to re-append the root surface layer on every update. Appending detaches and re-attaches the node.src/surface_config_cache.rs,configure_surface_layer).Reconfigure::Placement) repositions the layer and does not reinstall its draw content.sync_surface_opaque_region).wl_surface.set_opaque_region, as smithay tracks it, is scaled to physical pixels and passed toLayer::set_opaque_region.surface_filter).fix(udev): builds with--features metricsagain.2. otto-kit: scroll components
All of this lives in
components/otto-kit/src/components/scroll/. The design is indocs/developer/scroll-pane.md(new).request_frame_damaged(&[Rect])sendseglSwapBuffersWithDamagefor a partial repaint.set_opaque_regiondeclares opaque pixels.ScrollSurfaces: a clip subsurface plus a band subsurface holding the content around the viewport, plus a thumb.ScrollPane/ScrollContent: physics (ScrollView) plus surfaces plus a content revision.set_highlightis a selection marker that slides between rows on its own surface.hovered()/wheel_attell you what is under a still pointer while content flings past it.update_container_atmoves a container that is driven by physics outside the pane.Fill: a public one-pixel colour surface, stretched and rounded by the compositor. Used for dividers and highlights.RowLayout/GridLayout: row and cell geometry in content coordinates, with hit testing, visible range and marquee.ScrollGroup: gesture axis routing between nested panes.damage_probe: damage on the wire.virtual_fling: touchpad flings throughzwlr_virtual_pointer_v1.scroll_nested_probe: a nested stack.3. Apps on the components
Fill.ScrollPanewith pass-through input, and its ground is declared opaque.Specs updated:
file-browser.md,settings-app.md,launcher.md,emoji-picker.md.file-previews.mdwas touched too.4. otto-files and kit, restructured
After an architecture review of the result:
ScrollSurfacesorScrollPanedestroys its surfaces (EGL first, frame bookkeeping forgotten); a frame the compositor never answers stops holding the next step after 500 ms; a band claims its size after the buffer that fits it; the thumb is re-claimed when the scale lands.PlacedSurface(kit). The painted child surface otto-files kept for itself, now shared: placed at the fractional scale, a size claimed only behind a matching buffer, paints keyed and paced, input withdrawn while hidden. Status lines, the docked preview and its video, the palette card and Quick View use it, which fixes their sizing on fractional scales.on_update, which runs after every batch, decides between a window repaint, a file-area repaint, or surfaces only. A column scroll never repaints the window; a thumbnail landing in list or grid repaints the file area.app.rsisapp/. One module per concern (navigation, selection, the palette session, file operations, Quick View, keys, lifecycle, presentation), each test module in its own file. The thousand-line pointer closure isBrowser::on_pointer: overlays (rename, Quick View, the sheet, the fields, the footer, the path bar) answer first as methods of their own, then the listing by event kind.OTTO_FILES_QV_TRACE,OTTO_FILES_PALETTE_TRACE,OTTO_FILES_NO_BLUR, and dead code.Reading order
The commits are meant to be read one at a time, oldest first: the kit pacing and a11y commits, the compositor commit (
perf: damage and blur only what changed), the kit scroll components, the apps moving onto them, and last the restructuring commits (fix(kit): tear down panes…onwards).Known follow-ups (not in this PR)
ScrollSurfaces::sync_staterather thanScrollPane, because their scroll views live in the browser state its headless tests drive. A pane that borrows itsScrollViewwould remove that bookkeeping.ScrollGrouphas no consumer yet; pinned grid headers as their own surface are not built.Test plan
cargo test --release --lib: otto-kit (351), otto-files (384), clippy clean for both, after the restructuring; launcher (24) and emoji (42) before itWAYLAND_DEBUG, and screenshotshttps://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54