atagen

monolith

public
Star0
Watch0

Commit 2888d5881262

tarball

atagen <boss@atagen.co>2026-10-19 07:56 UTC

5 files changed+186-35
popups: draw and hit-test popups above every window

scene.rs drew a window's popups with the window, so a menu from the left tile sat under the tile to its right and was cropped or clipped along with its window. Popups now draw in a pass of their own above all windows, and surface_under and window_at look at them first in the same order, so the surface a menu shows is the one that gets the click.
Modified file: crates/monolith/src/input.rs+3 additions
@@ -172,6 +172,9 @@ fn focus_under_pointer(state: &mut State) {
172172 }
173173
174174 fn window_at(state: &State, pos: Point<f64, Logical>) -> Option<WindowId> {
175+ if let Some((root, ..)) = state.window_popup_under(pos) {
176+ return state.window_id(&root);
177+ }
175178 let (w, _) = state.wm.space.element_under(pos)?;
176179 state.window_id(&crate::wm::root(w)?)
177180 }
Modified file: crates/monolith/src/layers.rs+48 additions−2 deletions
@@ -6,7 +6,10 @@ use crate::state::State;
66 use monolith_layout::{Insets, OutputId};
77 use monolith_render_vk::VulkanRenderer;
88 use smithay::backend::renderer::element::AsRenderElements;
9-use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
9+use smithay::backend::renderer::element::Kind;
10+use smithay::backend::renderer::element::surface::{
11+ WaylandSurfaceRenderElement, render_elements_from_surface_tree,
12+};
1013 use smithay::desktop::{LayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output};
1114 use smithay::output::Output;
1215 use smithay::reexports::wayland_server::Resource;
@@ -213,6 +216,24 @@ impl State {
213216 })
214217 }
215218
219+ /// The popup of a layer surface below the windows under `pos`.
220+ pub fn lower_popup_under(
221+ &self,
222+ pos: Point<f64, Logical>,
223+ ) -> Option<(WlSurface, Point<f64, Logical>)> {
224+ let output = self.wm.space.output_under(pos).next()?;
225+ let origin = self.wm.space.output_geometry(output)?.loc;
226+ let map = layer_map_for_output(output);
227+ [Layer::Bottom, Layer::Background]
228+ .into_iter()
229+ .find_map(|layer| {
230+ map.layers_on(layer).rev().find_map(|l| {
231+ let at = origin + map.layer_geometry(l)?.loc;
232+ crate::popups::hit(crate::popups::placed(l.wl_surface(), at), pos)
233+ })
234+ })
235+ }
236+
216237 /// The layer surface under `pos` on `layers`, with its surface origin.
217238 pub fn layer_under(
218239 &self,
@@ -239,12 +260,37 @@ pub fn elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, output: &Out
239260 let Some(geo) = map.layer_geometry(l) else {
240261 continue;
241262 };
263+ let at = geo.loc.to_physical(1);
242264 let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
243- l.render_elements(renderer, geo.loc.to_physical(1), Scale::from(1.0), 1.0);
265+ if matches!(layer, Layer::Bottom | Layer::Background) {
266+ render_elements_from_surface_tree(
267+ renderer,
268+ l.wl_surface(),
269+ at,
270+ 1.0,
271+ 1.0,
272+ Kind::Unspecified,
273+ )
274+ } else {
275+ l.render_elements(renderer, at, Scale::from(1.0), 1.0)
276+ };
244277 out.extend(elems.into_iter().map(Draw::Surface));
245278 }
246279 }
247280
281+/// The popups of the layer surfaces below the windows. Those surfaces draw
282+/// without them, so a menu is not hidden by the window it opens over.
283+pub fn lower_popups(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, output: &Output) {
284+ let map = layer_map_for_output(output);
285+ for layer in [Layer::Bottom, Layer::Background] {
286+ for l in map.layers_on(layer).rev() {
287+ if let Some(geo) = map.layer_geometry(l) {
288+ crate::popups::elements(out, renderer, l.wl_surface(), geo.loc, 1.0);
289+ }
290+ }
291+ }
292+}
293+
248294 /// With `covered`, a fullscreen window hides everything below the overlay
249295 /// layer, so only overlay surfaces are asked to draw.
250296 pub fn send_frames(output: &Output, elapsed: Duration, covered: bool) {
Modified file: crates/monolith/src/popups.rs+88 additions−4 deletions
@@ -1,17 +1,22 @@
11 //! xdg popups, from their grabs to where they are placed.
22
33 use crate::focus::Focus;
4+use crate::scene::Draw;
45 use crate::state::State;
5-use monolith_layout::Rect;
6+use monolith_layout::{Rect, WindowId};
7+use monolith_render_vk::VulkanRenderer;
8+use smithay::backend::renderer::element::Kind;
9+use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
10+use smithay::desktop::utils::under_from_surface_tree;
611 use smithay::desktop::{
7- PopupKeyboardGrab, PopupKind, PopupPointerGrab, PopupUngrabStrategy, WindowSurfaceType,
8- find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output,
12+ PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy,
13+ WindowSurfaceType, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output,
914 };
1015 use smithay::input::Seat;
1116 use smithay::input::pointer::Focus as GrabFocus;
1217 use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
1318 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
14-use smithay::utils::{Logical, Point, Rectangle, Serial};
19+use smithay::utils::{Logical, Physical, Point, Rectangle, Serial};
1520 use smithay::wayland::shell::xdg::PopupSurface;
1621
1722 impl State {
@@ -115,3 +120,82 @@ impl State {
115120 popup.with_pending_state(|s| s.geometry = s.positioner.get_unconstrained_geometry(target));
116121 }
117122 }
123+
124+/// The popup surfaces of `root` with where their origins are, newest first.
125+/// `geometry_at` is where root's window geometry sits, which for a layer
126+/// surface is where the surface itself sits.
127+pub fn placed(
128+ root: &WlSurface,
129+ geometry_at: Point<i32, Logical>,
130+) -> Vec<(WlSurface, Point<i32, Logical>)> {
131+ PopupManager::popups_for_surface(root)
132+ .map(|(popup, at)| {
133+ (
134+ popup.wl_surface().clone(),
135+ geometry_at + at - popup.geometry().loc,
136+ )
137+ })
138+ .collect()
139+}
140+
141+/// The surface of `popups` under `pos`, with its origin.
142+pub fn hit(
143+ popups: Vec<(WlSurface, Point<i32, Logical>)>,
144+ pos: Point<f64, Logical>,
145+) -> Option<(WlSurface, Point<f64, Logical>)> {
146+ popups.into_iter().find_map(|(popup, at)| {
147+ let (s, p) =
148+ under_from_surface_tree(&popup, pos - at.to_f64(), (0, 0), WindowSurfaceType::ALL)?;
149+ Some((s, (p + at).to_f64()))
150+ })
151+}
152+
153+/// Draws `root`'s popups with its window geometry at `geometry_at`.
154+pub fn elements(
155+ out: &mut Vec<Draw>,
156+ renderer: &mut VulkanRenderer,
157+ root: &WlSurface,
158+ geometry_at: Point<i32, Logical>,
159+ alpha: f32,
160+) {
161+ for (popup, at) in placed(root, geometry_at) {
162+ let at = Point::<i32, Physical>::from((at.x, at.y));
163+ out.extend(
164+ render_elements_from_surface_tree(renderer, &popup, at, 1.0, alpha, Kind::Unspecified)
165+ .into_iter()
166+ .map(Draw::Surface),
167+ );
168+ }
169+}
170+
171+impl State {
172+ /// The window covering the output under `pos` while it is fullscreen.
173+ pub fn fullscreen_at(&self, pos: Point<f64, Logical>) -> Option<WindowId> {
174+ self.wm.layout.outputs().find_map(|(o, out)| {
175+ let a = out.area;
176+ a.contains(pos.x, pos.y)
177+ .then(|| self.wm.layout.arrange(o).fullscreen)
178+ .flatten()
179+ })
180+ }
181+
182+ /// The popup of a window under `pos`, frontmost window first, with the
183+ /// window's root surface. A fullscreen window hides the popups of the
184+ /// windows below it.
185+ pub fn window_popup_under(
186+ &self,
187+ pos: Point<f64, Logical>,
188+ ) -> Option<(WlSurface, WlSurface, Point<f64, Logical>)> {
189+ let fullscreen = self.fullscreen_at(pos);
190+ self.wm.space.elements().rev().find_map(|window| {
191+ let root = crate::wm::root(window).filter(|_| window.toplevel().is_some())?;
192+ let id = self.window_id(&root)?;
193+ if fullscreen.is_some_and(|f| f != id) {
194+ return None;
195+ }
196+ let (_, rect, _) = self.wm.get(id)?.placed?;
197+ let (surface, at) = hit(placed(&root, whole(rect).loc), pos)?;
198+ Some((root, surface, at))
199+ })
200+ }
201+}
Modified file: crates/monolith/src/scene.rs+36 additions−25 deletions
@@ -15,11 +15,10 @@ use smithay::backend::renderer::element::{AsRenderElements, Element, Id, Kind, R
1515 use smithay::backend::renderer::utils::with_renderer_surface_state;
1616 use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
1717 use smithay::backend::renderer::{ContextId, Renderer, Texture};
18-use smithay::desktop::{PopupManager, Window};
18+use smithay::desktop::Window;
1919 use smithay::output::Output;
2020 use smithay::utils::user_data::UserDataMap;
2121 use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
22-use smithay::wayland::compositor::{TraversalAction, with_surface_tree_downward};
2322 use smithay::wayland::shell::wlr_layer::Layer;
2423 use smithay::xwayland::X11Surface;
2524
@@ -121,6 +120,25 @@ pub fn build(
121120 crate::layers::elements(&mut out, renderer, output, Layer::Top);
122121 }
123122 }
123+ let covering = arr.fullscreen.filter(|_| fullscreen_at_rest);
124+ for &(id, r, _, _) in tiled.iter().chain(&above).rev() {
125+ if covering.is_some_and(|f| f != id) {
126+ continue;
127+ }
128+ let Some(root) = windows.get(id).and_then(|s| s.window.toplevel()) else {
129+ continue;
130+ };
131+ let Some(((x, y), _)) = pixels(r) else {
132+ continue;
133+ };
134+ let alpha = anim.effect(id, now).map_or(1.0, |e| e.progress);
135+ crate::popups::elements(&mut out, renderer, root.wl_surface(), (x, y).into(), alpha);
136+ }
137+ if let Some(output) = output
138+ && !fullscreen_at_rest
139+ {
140+ crate::layers::lower_popups(&mut out, renderer, output);
141+ }
124142 for (g, frame, r) in fading.into_iter().rev() {
125143 out.extend(ghost_element(frame, r, anim.ghost_effect(g, now), deco));
126144 }
@@ -133,7 +151,7 @@ pub fn build(
133151 window_elements(&mut out, renderer, &state.window, r, effect, gliding);
134152 let clip = framed.then(|| corner_clip(r, deco)).flatten();
135153 if let Some(clip) = clip {
136- clip_window(&mut out, first, &state.window, clip);
154+ clip_window(&mut out, first, clip);
137155 }
138156 if framed && deco.border > 0 {
139157 let color = if focused == Some(id) {
@@ -180,8 +198,17 @@ fn window_elements(
180198 let geo = window.geometry();
181199 let loc = Point::<i32, Physical>::from((x - geo.loc.x, y - geo.loc.y));
182200 let alpha = effect.map_or(1.0, |e| e.progress);
183- let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
184- window.render_elements(renderer, loc, Scale::from(1.0), alpha);
201+ let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> = match window.toplevel() {
202+ Some(t) => render_elements_from_surface_tree(
203+ renderer,
204+ t.wl_surface(),
205+ loc,
206+ 1.0,
207+ alpha,
208+ Kind::Unspecified,
209+ ),
210+ None => window.render_elements(renderer, loc, Scale::from(1.0), alpha),
211+ };
185212 if let Some(effect) = effect {
186213 effect_elements(out, renderer, window, elems, ((x, y), (w, h)), effect);
187214 return;
@@ -320,7 +347,7 @@ fn texture(
320347 }
321348
322349 /// A window in a spawn transition. Its root surface goes through the effect
323-/// shader, clamped to fill its rect, and its subsurfaces and popups fade with it.
350+/// shader, clamped to fill its rect, and its subsurfaces fade with it.
324351 fn effect_elements(
325352 out: &mut Vec<Draw>,
326353 renderer: &mut VulkanRenderer,
@@ -471,26 +498,10 @@ fn corner_clip(r: Rect, deco: &Decoration) -> Option<Clip> {
471498 })
472499 }
473500
474-/// Clips what a window drew from `first` on, leaving its popups whole.
475-fn clip_window(out: &mut [Draw], first: usize, window: &Window, clip: Clip) {
476- let mut popups = std::collections::HashSet::new();
477- if let Some(root) = crate::wm::root(window) {
478- for (popup, _) in PopupManager::popups_for_surface(&root) {
479- with_surface_tree_downward(
480- popup.wl_surface(),
481- (),
482- |_, _, _| TraversalAction::DoChildren(()),
483- |s, _, _| {
484- popups.insert(Id::from_wayland_resource(s));
485- },
486- |_, _, _| true,
487- );
488- }
489- }
501+/// Clips what a window drew from `first` on.
502+fn clip_window(out: &mut [Draw], first: usize, clip: Clip) {
490503 for draw in &mut out[first..] {
491- if !popups.contains(draw.id()) {
492- wrap(draw, clip);
493- }
504+ wrap(draw, clip);
494505 }
495506 }
496507
Modified file: crates/monolith/src/state.rs+11 additions−4 deletions
@@ -472,10 +472,7 @@ impl State {
472472 return Some((s, (p + geo.loc).to_f64()));
473473 }
474474 }
475- let fullscreen = self.wm.layout.outputs().any(|(o, out)| {
476- let a = out.area;
477- a.contains(pos.x, pos.y) && self.wm.layout.arrange(o).fullscreen.is_some()
478- });
475+ let fullscreen = self.fullscreen_at(pos).is_some();
479476 let above: &[Layer] = if fullscreen {
480477 &[Layer::Overlay]
481478 } else {
@@ -484,6 +481,16 @@ impl State {
484481 let layer = |layers| self.layer_under(pos, layers).map(|(_, s, p)| (s, p));
485482 layer(above)
486483 .or_else(|| {
484+ self.window_popup_under(pos)
485+ .map(|(_, surface, origin)| (surface, origin))
486+ })
487+ .or_else(|| {
488+ if fullscreen {
489+ return None;
490+ }
491+ self.lower_popup_under(pos)
492+ })
493+ .or_else(|| {
487494 self.wm
488495 .space
489496 .element_under(pos)