GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(engine): report in-surface popover rects to the compositor
Since Phase 6x (279ffc9) popovers draw into the app's own surface, which
the compositor composites as one buffer — so its window chrome (the
overview resize ring) drew straight over an open menu, and no scene
stacking could fix that from outside.
The engine now sends the union of the open popover rects over the new
zcce_toplevel_v1.set_popover_region request (manager v7), from the same
active_popovers walk that already feeds the input region; the compositor
clips its ring beneath the rect. Sent from the main loop on change only,
with a clear when the last popover closes — the protocol requires the
clear, or the chrome stays cut where a menu used to be.
The manager is now bound for EVERY app, not only utility/grid ones, with
the range topped at 7; the send is gated on the NEGOTIATED version >= 7
(manager numbering — the toplevel resource inherits its bind version), so
an app on an older compositor simply never sends and nothing dies on an
unknown opcode.
The protocol XML is the compositor's copy with one deliberate divergence
KEPT: the enum="river_output_v1.presentation_mode" attribute is stripped,
because wayland_scanner cannot resolve a cross-protocol enum it is not
given. Re-syncing the file wholesale reintroduces it and breaks every
client build — it did today.
Co-Authored-By: Claude Fable 5 <[email protected]>
protocol/cce-window-management-v1.xml | 27 ++++++++++--
src/backend/window_runner.rs | 78 +++++++++++++++++++++++++++++++----
2 files changed, 94 insertions(+), 11 deletions(-)
diff --git a/protocol/cce-window-management-v1.xml b/protocol/cce-window-management-v1.xml
index 1bcfd5b..d5cffdd 100644
--- a/protocol/cce-window-management-v1.xml
+++ b/protocol/cce-window-management-v1.xml
@@ -28,7 +28,7 @@
"should", "should not", "recommended", "may", and "optional" in this
document are to be interpreted as described in IETF RFC 2119.
</description>
- <interface name="zcce_window_manager_v1" version="6">
+ <interface name="zcce_window_manager_v1" version="7">
<description summary="window manager global interface">
This global interface should only be advertised to the window manager
process. Only one window management client may be active at a time. The
@@ -838,7 +838,7 @@
This event will be followed by a render_start event after all other new
state has been sent by the server.
</description>
- <arg name="hint" type="uint" enum="zcce_output_v1.presentation_mode" summary="presentation hint"/>
+ <arg name="hint" type="uint" summary="presentation hint"/>
</event>
<event name="identifier" since="4">
<description summary="unique window identifier">
@@ -1490,7 +1490,7 @@
</description>
</event>
</interface>
- <interface name="zcce_toplevel_v1" version="4">
+ <interface name="zcce_toplevel_v1" version="5">
<description summary="toplevel window management controls">
An interface to control and listen to CCE-specific window management states
for a client surface.
@@ -1579,6 +1579,27 @@
</description>
<arg name="state" type="uint" summary="1 if floating, 0 otherwise"/>
</event>
+ <request name="set_popover_region" since="5">
+ <description summary="hint where an in-surface popover is drawn">
+ Tell the compositor that the client is drawing a popover (menu,
+ dropdown, context menu) into its own surface at this rect, in
+ surface-local logical coordinates. The compositor keeps its
+ window chrome — the overview resize ring — from drawing over
+ that rect, so the menu reads as being in front of it. With more
+ than one popover open, send the union. A zero width or height
+ clears the hint; closing the last popover must send that clear,
+ or the chrome stays cut where the menu used to be.
+
+ This is a rendering hint only: it moves no window, changes no
+ input routing, and carries no promise the compositor draws
+ anything differently at all.
+ </description>
+ <arg name="x" type="int"/>
+ <arg name="y" type="int"/>
+ <arg name="width" type="int"/>
+ <arg name="height" type="int"/>
+ </request>
+
<event name="grid_patch" since="4">
<description summary="render this patch of the desktop grid">
Instructs the grid client what region of the virtual desktop to
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index f35670b..b959880 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -3320,6 +3320,9 @@ pub struct EngineState<A: Application> {
/// True while the previous frame ran with a nonzero margin — lets the
/// per-frame geometry publish reset state exactly once on deactivation.
pub overflow_was_active: bool,
+ /// The popover-union rect last sent via zcce set_popover_region, logical
+ /// surface px; None once a clear has been sent (or never anything).
+ pub sent_popover_region: Option<(i32, i32, i32, i32)>,
pub exit: bool,
pub redraw: bool,
@@ -3513,6 +3516,59 @@ impl<A: Application> EngineState<A> {
}
}
+ /// Report the union of the open popover rects to the compositor
+ /// (zcce set_popover_region, manager v7), so its window chrome — the
+ /// overview resize ring — stays out from under an in-surface menu. Sent
+ /// only on change, and a clear is sent when the last popover closes;
+ /// rects are clamped to the surface in logical px, the coordinate space
+ /// the protocol specifies. Popovers animate, so this runs every loop —
+ /// the change gate is what keeps it quiet.
+ fn send_popover_region(&mut self) {
+ let Some(tl) = &self.cce_toplevel else { return };
+ // Version gate on the MANAGER numbering the resource carries (the
+ // toplevel inherits its bind version): 7 is where the request
+ // appeared. An older compositor would kill the client on the
+ // unknown opcode.
+ if tl.version() < 7 {
+ return;
+ }
+ let mut union: Option<(f32, f32, f32, f32)> = None;
+ if let Some(ctx) = self.inner.as_ref().unwrap().ui_context() {
+ for (_id, ptr) in ctx.tree.iter_registered() {
+ unsafe {
+ let Some(w) = ptr.as_ref() else { continue };
+ if !w.visible() {
+ continue;
+ }
+ let Some((px, py, pw, ph)) = w.popover_rect() else { continue };
+ let (x0, y0) = (px.max(0.0), py.max(0.0));
+ let x1 = (px + pw).min(self.logical_width);
+ let y1 = (py + ph).min(self.logical_height);
+ if x1 <= x0 || y1 <= y0 {
+ continue;
+ }
+ union = Some(match union {
+ None => (x0, y0, x1, y1),
+ Some((ux0, uy0, ux1, uy1)) => {
+ (ux0.min(x0), uy0.min(y0), ux1.max(x1), uy1.max(y1))
+ }
+ });
+ }
+ }
+ }
+ let next = union.map(|(x0, y0, x1, y1)| {
+ (x0 as i32, y0 as i32, (x1 - x0).ceil() as i32, (y1 - y0).ceil() as i32)
+ });
+ if next == self.sent_popover_region {
+ return;
+ }
+ match next {
+ Some((x, y, w, h)) => tl.set_popover_region(x, y, w, h),
+ None => tl.set_popover_region(0, 0, 0, 0),
+ }
+ self.sent_popover_region = next;
+ }
+
/// The cursor for the pointer at (lx, ly): the app's
/// [`Application::cursor_icon`] override, else the standard-CSD edge
/// cursors (status bars and non-standard-CSD apps fall back to Default).
@@ -5093,6 +5149,7 @@ fn run_session<'l, A: Application>(
frame_logical: (0.0, 0.0),
applied_margin: 0.0,
overflow_was_active: false,
+ sent_popover_region: None,
exit: false,
redraw: false,
frame_callback_pending: false,
@@ -5203,14 +5260,18 @@ fn run_session<'l, A: Application>(
}
let wants_utility = engine_state.inner.as_ref().unwrap().utility();
let wants_grid = engine_state.inner.as_ref().unwrap().grid();
- if wants_utility || wants_grid {
- // Declared BEFORE the initial commit so the mode is set by the
- // time the compositor maps (and would otherwise restore) the
- // window. Manager version 5 is where set_utility appeared, 6 is
- // where the grid role did; on an older compositor the
- // declaration is skipped and the app runs as a plain floating
- // window rather than dying on an unknown opcode.
- let version = if wants_grid { 6..=6 } else { 5..=5 };
+ {
+ // Bound for EVERY app now, not just utility/grid ones: the
+ // toplevel also carries the popover-region hint (manager v7),
+ // which any app with a dropdown wants. Role declarations go
+ // BEFORE the initial commit so the mode is set by the time the
+ // compositor maps the window. Version floors: set_utility
+ // appeared at manager 5, the grid role at 6; the range tops at 7
+ // so a newer compositor grants the hint and an older one simply
+ // yields a lower-versioned toplevel — the hint send is gated on
+ // version() >= 7 (send_popover_region), and on a pre-5
+ // compositor the bind fails and the app runs plain.
+ let version = if wants_grid { 6..=7 } else { 5..=7 };
match globals.bind::<crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1, _, _>(&qh, version, ()) {
Ok(cce_wm) => {
let toplevel = cce_wm.get_cce_toplevel(&surface, &qh, ());
@@ -5403,6 +5464,7 @@ fn run_session<'l, A: Application>(
engine_state.publish_window_geometry();
engine_state.overflow_was_active = engine_state.applied_margin > 0.0;
}
+ engine_state.send_popover_region();
}
if let Some(ref mut pk) = engine_state.pressed_key {