feat: span fullscreen="all" across all monitors on Wayland (layer-shell)#546
feat: span fullscreen="all" across all monitors on Wayland (layer-shell)#546FedotCompot wants to merge 9 commits into
Conversation
GDK's all-monitor fullscreen (FullscreenMode::AllMonitors) is a no-op on Wayland, so fullscreen="all" only ever covered a single output. A single Wayland surface also cannot span multiple outputs. Realize it instead with one wlr-layer-shell surface per monitor: the App root window becomes the primary surface and SketchBoard creates an extra layer-shell surface for every other output. Each surface renders its own native-scale slice of the screenshot via a fixed "layout view" transform. Drawables are broadcast (cloned) to every area so each renders the shared scene on its own GL context; secondary areas forward pointer input in image coordinates. Save/copy keeps rendering the full image on the primary, so the exported result contains annotations from every monitor. The blur tool screenshots the canvas, so it now clamps the sampled region to the framebuffer to avoid out-of-bounds sampling on surfaces that only show a slice. Falls back to the previous behaviour on X11, on compositors without layer-shell, and for single-monitor setups.
The crop shadow was sized to the canvas and anchored at image origin (0,0), so it only lined up on the monitor whose image slice starts at (0,0). On the other layer-shell surfaces the layout-view offset shifted the overlay (and its even-odd hole), producing inverted/misplaced dimming. Dim the shared image bounds instead, so the overlay and the crop cut-out are positioned in image coordinates and render correctly on every monitor.
The blur tool screenshots the canvas and samples the rect under it. With fullscreen="all" each monitor's canvas only renders its own slice of the image, so a blur belonging to another monitor maps entirely off this target. The previous clamp forced the sampled width/height to a minimum of 1, so when the rect sat past the right/bottom edge `left` was already at the framebuffer width and `left + 1` ran one pixel out of bounds — imgref's sub_image asserts `left + width <= width()` and aborted the whole process. Clamp each edge independently (no forced minimum) so the bound always holds, and when the rect doesn't overlap the target, return None without caching. Leaving the cache empty lets the full-image render used for saving/copying recompute the blur from real pixels, so a blur drawn on a non-primary monitor still exports correctly instead of as an empty box. The overlap check now runs before the screenshot read-back, skipping it entirely when there's nothing to sample.
FullscreenChanged moves the toolbars between the outer box and the overlay. For layer-shell fullscreen="all" it fires twice for the same state: once from our explicit call after setting up the surfaces, and again from the window's "fullscreened" notify once the compositor reports the surface fullscreen. The second run called outer_box.remove() on toolbars already moved to the overlay, tripping two gtk_box_remove assertions at startup. Track whether the toolbars are currently overlaid and only move them when the requested state actually differs.
In fullscreen="all" the per-monitor annotation surfaces live on the Overlay layer. The color chooser and annotation-size dialogs are normal toplevels, so they opened *behind* those surfaces and were invisible — the custom colour palette looked like it never opened. Promote both dialogs to their own Overlay layer-shell surface when layershell_all_active(), with Exclusive keyboard so their inputs (palette, hex entry, spin button, Enter/Escape) work. transient_for a layer-shell surface is meaningless, so it's skipped in that mode. Outside fullscreen="all" the dialogs are unchanged. layershell_all_active() is now pub(crate) so the toolbars module can reuse it.
In fullscreen="all" every surface is pinned to a fixed per-monitor slice via the layout view, so zooming/panning makes no sense there. The render-time transform already ignored them, but the input handlers still mutated the zoom/offset state, which could take effect during the brief window before the layout view is applied — scrolling (with or without modifiers) appeared to zoom the primary monitor. Short-circuit set_zoom_scale, set_drag_offset and reset_size when layershell_all_active(), covering every entry point (scroll, pinch, middle-drag pan, arrow-key pan, and the 1:1/fit toolbar buttons).
`GtkColorChooserDialog` doesn't work well on the Overlay layer-shell surface used in fullscreen="all": its Select/Cancel live in a header bar that isn't rendered, so the dialog could only be dismissed with Esc and a custom colour couldn't be confirmed; and it wraps the chooser in a scrolled window, so the taller custom-editor view scrolled (hiding the alpha slider behind a scrollbar). Build the picker ourselves for that mode: a bare GtkColorChooserWidget (sizes to its content, no scrolled window) in an Overlay layer-shell window with in-content Cancel/Select buttons. Confirms on Select or a double-clicked swatch, cancels on Cancel or Esc. Non-fullscreen="all" keeps the stock dialog.
Replace the separate annotation-size dialog with controls directly in the style toolbar: a minus and plus button (±0.1), a live value readout, and a reset button that restores the configured default. Scrolling over the value still fine-tunes by ±0.01; the scroll controller now uses DISCRETE so one wheel notch is exactly one step instead of jumping erratically with smooth scrolling. The value still propagates via ToolbarEvent::AnnotationSizeChanged, so nothing downstream changes. Removes the AnnotationSizeDialog component, its input/output enums, the controller field, the show_annotation_dialog method, and the ShowAnnotationDialog/AnnotationDialogFinished messages.
|
Thank you for this PR. I can't speak for all of my maintainer colleagues of course, but personally I don't think that we are going to merge this into main. My gut feeling is that Satty is not the correct place to solve this problem. This should be solved in Wayland, and it has been discussed in the past already: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/work_items/99 Yes, that approach is (obviously) very veeeeeeerrrry sllllllllllloooooooooow, but it's the right one. My suggestion for anyone looking for a solution for this is to direct any effort towards the Wayland item mentioned above. If it was a tiny change providing a workaround, I probably would not mind adding it to Satty. But it is not a tiny change. It would add considerable complexity, and brings along more changes required to even make it work. Take the need for a custom colour dialog, for example. Complexity we'd have to support once we merge. I prefer living with a known issue here. That said, I think we might be looking into cherry-picking b6a1d3a, because that's actually a simplification and looks quite useful. |
|
Ok, perfectly understandable, if you want I can make a PR with the tool size UI. Regarding workaround options... The first thing I tried was a single window spanning all monitors (7040x1440@-1920x0 in my case). But past a certain point the window rendering itself becomes inconsistent, colors shift so IDK. Maybe there is a way of making it work I'm not aware of, I'm really not an expert of rust or desktop applications in general, just a poor guy missing flameshot level ergonomics for screenshots after moving to hypland ahahahah So I will keep my fork and continue doing some improvements from time to time based on my needs and time available and will use a local build on my machines. Really like the project, hope it will survive the maintainers crisis |
Thank you for understanding. Yes, if you don't mind, a PR would be nice. Just a small heads-up: we may revamp this again, so it may just be temporary. The S-M-L plus annotation size is possibly not the greatest UX, but we haven't really decided on anything yet, so I think we'd merge the commit for the time being. |
|
Yeah... I also noticed that, me personally I REALLY loved the scrolling with a circle around the cursor as a preview to change the tool size without a specific numer on flameshot, really simple and intuitive. I'm on vacation 20-28 as soon as I have time when I get back I will try to implement (vibe code) it on my fork, will try to do it on both my layer-shell and master so in case you like it you can consider merging. Also regarding the color menu it could be an in place popover instead of a separate window. Just a thought... |
Disclaimer
The whole PR was vibe-coded with Opus 4.8
Summary
fullscreen = "all"previously only ever covered a single monitor on Wayland. The implementation relied on GDK'sFullscreenMode::AllMonitors, which is a no-op on the Wayland backend — and a single Wayland surface cannot span multiple outputs by design.This PR makes
fullscreen = "all"actually span every monitor on Wayland by using thewlr-layer-shellprotocol: the app's root window becomes the primary layer-shell surface, and one additional layer-shell surface is created per remaining output. Each surface renders its own native-scale slice of the screenshot, so together they cover the whole layout 1:1.Behaviour is unchanged on X11 (still uses GDK's native all-monitor fullscreen) and it falls back gracefully to a single screen on compositors without layer-shell and for single-monitor setups.
Related to #118 — this introduces
wlr-layer-shellsupport to the codebase, a prerequisite for the layer-shell / live-annotation ideas discussed there.How it works
FemtoVGAreato a fixed image slice at native scale (image_origin+ image-pixels-per-device-pixel) instead of the usual fit/center.SketchBoardcreates one more layer-shell window per other monitor.New dependency
gtk4-layer-shell(Rust crate0.7, targeting gtk4-rs 0.10). Requires thegtk4-layer-shellC library at build and runtime.Testing
Verified on Hyprland with a 3-monitor layout (1920×1080 @ -1920,0 · 2560×1440 @ 0,0 · 2560×1440 @ 2560,0):
cargo testpasses,cargo clippyclean.Known limitations (initial version)