Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: world-anchored grid client — protocol, patch manager, fallback swap
The desktop grid can now be rendered by a cce-ui client (cce-grid)
instead of the compositor's rect/bevel pools, without ever putting the
client in the pan/zoom loop:
- Protocol (both this XML and cce-ui's copy, manager v6 / toplevel v4):
set_grid declares the surface the grid layer; grid_patch(serial, x,
y, w, h, scale) tells it what virtual region to render at what
resolution; ack_grid_patch + the client's next commit latch the
anchor, so an in-flight older buffer is never shown at a new anchor.
- Patch manager (update_grid_patches, start of arrange_views): issues a
patch when none covers the viewport with a quarter-view margin or the
buffer resolution drifts 2x from the zoom; one in flight per window;
period-aligned, margin shrunk to an 8k buffer cap, resolution
quantized to powers of two so zoom wobbles don't re-render.
- Grid windows are input-transparent at the scene.at level (the same
disable-and-retry trick as circular windows), excluded from focus,
overview fit, displacement, the switcher, and session save/restore.
- The compositor cell lattice yields to a live grid client
(grid_cells_enabled from the arrange plan, redraw forced on swap);
the gap backdrop always draws — it is the safety net beyond patch
edges during fast pans, and the whole grid returns if the client
dies.
Shadow-verified end to end: declare -> map -> patch -> ack -> latch ->
client pixels on screen (frame diff against the fallback), and the
fallback swaps back in when the client is killed.
Co-Authored-By: Claude Fable 5 <[email protected]>
protocol/cce-window-management-v1.xml | 48 ++++++++++++-
src/server/cce_window_management.rs | 90 +++++++++++++++++++++++++
src/server/output.rs | 7 +-
src/server/scene.rs | 9 +++
src/server/seat.rs | 2 +-
src/server/window.rs | 29 +++++++-
src/server/window_manager.rs | 123 ++++++++++++++++++++++++++++++++--
src/server/xdg_toplevel.rs | 9 +++
8 files changed, 304 insertions(+), 13 deletions(-)
diff --git a/protocol/cce-window-management-v1.xml b/protocol/cce-window-management-v1.xml
index ce824a2..f930381 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="5">
+ <interface name="zcce_window_manager_v1" version="6">
<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
@@ -1490,7 +1490,7 @@
</description>
</event>
</interface>
- <interface name="zcce_toplevel_v1" version="3">
+ <interface name="zcce_toplevel_v1" version="4">
<description summary="toplevel window management controls">
An interface to control and listen to CCE-specific window management states
for a client surface.
@@ -1548,11 +1548,55 @@
Request the compositor to return the window to normal tiling mode.
</description>
</request>
+ <request name="set_grid" since="4">
+ <description summary="declare the desktop-grid surface">
+ Declare this surface the desktop-grid layer. The compositor anchors
+ the surface to the virtual desktop: it is positioned and scaled with
+ the camera every frame, exactly like window content, so the client is
+ never part of the pan/zoom loop. The surface becomes
+ input-transparent, is stacked above the wallpaper and below all
+ windows, is excluded from focus, session save and the overview, and
+ the compositor stops dictating sizes to it.
+
+ The compositor tells the client WHAT to render via grid_patch
+ events; the surface stays unmapped-invisible until the first
+ acknowledged patch is committed.
+ </description>
+ </request>
+ <request name="ack_grid_patch" since="4">
+ <description summary="acknowledge a grid patch">
+ Acknowledge a grid_patch event. The next buffer the client commits
+ after this request is taken to be rendered for the acknowledged
+ patch: the compositor latches that patch's anchor rectangle at that
+ commit, so an in-flight older buffer is never shown at a new
+ anchor.
+ </description>
+ <arg name="serial" type="uint" summary="the serial from the grid_patch event"/>
+ </request>
<event name="floating_state">
<description summary="floating status event">
Sent by the compositor to inform the client of its current floating status.
</description>
<arg name="state" type="uint" summary="1 if floating, 0 otherwise"/>
</event>
+ <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
+ render, and at what resolution. x/y/width/height are virtual-surface
+ coordinates; scale is buffer pixels per virtual unit, so the
+ expected buffer size is (width * scale) x (height * scale). The
+ client renders the region, sends ack_grid_patch with the serial, and
+ commits the new buffer.
+
+ A newer grid_patch supersedes an unacknowledged older one; the
+ client should render only the latest.
+ </description>
+ <arg name="serial" type="uint" summary="serial to pass to ack_grid_patch"/>
+ <arg name="x" type="fixed" summary="patch origin x, virtual units"/>
+ <arg name="y" type="fixed" summary="patch origin y, virtual units"/>
+ <arg name="width" type="fixed" summary="patch width, virtual units"/>
+ <arg name="height" type="fixed" summary="patch height, virtual units"/>
+ <arg name="scale" type="fixed" summary="buffer px per virtual unit"/>
+ </event>
</interface>
</protocol>
diff --git a/src/server/cce_window_management.rs b/src/server/cce_window_management.rs
index e2c41f2..f81e1e7 100644
--- a/src/server/cce_window_management.rs
+++ b/src/server/cce_window_management.rs
@@ -24,6 +24,52 @@ impl CceWindowManagement {
}
}
+/// Grid patch event opcode on zcce_toplevel_v1 (floating_state is 0).
+const ZCCE_TOPLEVEL_V1_GRID_PATCH: u32 = 1;
+
+/// Toplevel interface version that carries the grid requests/events.
+const GRID_SINCE: i32 = 4;
+
+fn to_fixed(v: f64) -> i32 {
+ (v * 256.0).round() as i32
+}
+
+impl CceWindowManagement {
+ /// Post a grid_patch event to the toplevel resource of `window_key`, if
+ /// the client bound a new-enough version. Returns whether it was sent.
+ pub unsafe fn send_grid_patch(
+ &self,
+ window_key: SlotMapKey,
+ serial: u32,
+ patch: crate::policy::api::GridPatch,
+ ) -> bool {
+ for &res in &self.toplevels {
+ if res.is_null() {
+ continue;
+ }
+ let data = ffi::wl_resource_get_user_data(res) as *mut CceToplevelData;
+ if data.is_null() || (*data).window_key != window_key {
+ continue;
+ }
+ if ffi::wl_resource_get_version(res) < GRID_SINCE {
+ return false;
+ }
+ ffi::wl_resource_post_event(
+ res,
+ ZCCE_TOPLEVEL_V1_GRID_PATCH,
+ serial,
+ to_fixed(patch.x),
+ to_fixed(patch.y),
+ to_fixed(patch.w),
+ to_fixed(patch.h),
+ to_fixed(patch.scale),
+ );
+ return true;
+ }
+ false
+ }
+}
+
struct CceToplevelData {
server: *mut Server,
window_key: SlotMapKey,
@@ -306,6 +352,48 @@ unsafe extern "C" fn toplevel_unset_utility(
}
}
+unsafe extern "C" fn toplevel_set_grid(
+ _client: *mut ffi::wl_client,
+ resource: *mut ffi::wl_resource,
+) {
+ let data = ffi::wl_resource_get_user_data(resource) as *mut CceToplevelData;
+ if data.is_null() {
+ return;
+ }
+ let server = (*data).server;
+ let window_key = (*data).window_key;
+ if let Some(window) = resolve_window(server, window_key) {
+ log::info!("[Grid] set_grid declared");
+ (*window).grid_declared = true;
+ (*server).wm.dirty_windowing();
+ }
+}
+
+unsafe extern "C" fn toplevel_ack_grid_patch(
+ _client: *mut ffi::wl_client,
+ resource: *mut ffi::wl_resource,
+ serial: u32,
+) {
+ let data = ffi::wl_resource_get_user_data(resource) as *mut CceToplevelData;
+ if data.is_null() {
+ return;
+ }
+ let server = (*data).server;
+ let window_key = (*data).window_key;
+ if let Some(window) = resolve_window(server, window_key) {
+ // Only the latest outstanding patch can be acked; a stale serial
+ // (superseded patch) is ignored — the client should already be
+ // rendering the newer one.
+ if let Some((pending_serial, patch)) = (*window).grid_patch_pending {
+ log::info!("[Grid] ack #{serial} (pending #{pending_serial})");
+ if pending_serial == serial {
+ (*window).grid_patch_pending = None;
+ (*window).grid_patch_acked = Some((serial, patch));
+ }
+ }
+ }
+}
+
static CCE_TOPLEVEL_INTERFACE: ffi::zcce_toplevel_v1_interface = ffi::zcce_toplevel_v1_interface {
destroy: Some(toplevel_destroy),
set_floating: Some(toplevel_set_floating),
@@ -319,6 +407,8 @@ static CCE_TOPLEVEL_INTERFACE: ffi::zcce_toplevel_v1_interface = ffi::zcce_tople
unset_popup: Some(toplevel_unset_popup),
set_utility: Some(toplevel_set_utility),
unset_utility: Some(toplevel_unset_utility),
+ set_grid: Some(toplevel_set_grid),
+ ack_grid_patch: Some(toplevel_ack_grid_patch),
};
unsafe fn resolve_window(server: *mut Server, key: SlotMapKey) -> Option<*mut Window> {
diff --git a/src/server/output.rs b/src/server/output.rs
index 256a56c..01d7ca8 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -914,10 +914,13 @@ impl Output {
}
if force {
- // Backdrop in the gap color, then the cell lattice.
+ // Backdrop in the gap color, then the cell lattice. The
+ // backdrop always draws — while a grid client is live it
+ // is the safety net beyond the patch edges during fast
+ // pans; the CELLS yield to the client's rendering.
get_rect(frame.backdrop_w, frame.backdrop_h, grid.gap_color.0.as_ptr(), 0, 0, 0, 0);
- if let Some(cells) = &frame.cells {
+ if let Some(cells) = frame.cells.as_ref().filter(|_| wm.grid_cells_enabled) {
// scenefx fade-inset wire encoding: inset px * 1000
// + fade-mode index; 0 disables the fade.
use crate::policy::api::GridFadeMode;
diff --git a/src/server/scene.rs b/src/server/scene.rs
index a56315a..cadfcd8 100644
--- a/src/server/scene.rs
+++ b/src/server/scene.rs
@@ -146,6 +146,15 @@ impl Scene {
if let Some(scene_node_data) = SceneNodeData::from_node(node) {
if let SceneNodeDataVal::Window(window) = scene_node_data.data {
+ // The grid layer is input-transparent: every input path
+ // (clicks, hover, overview background-exit) sees what is
+ // underneath it, exactly as if it were the backdrop.
+ if (*window).is_grid() {
+ let tree_node = (*window).tree as *mut ffi::wlr_scene_node;
+ ffi::wlr_scene_node_set_enabled(tree_node, false);
+ disabled_nodes.push(tree_node);
+ continue;
+ }
if (*window).rendering_requested.circular {
// Check if outside the circle
let w = (*window).box_geom.width as f64 * (*window).scale;
diff --git a/src/server/seat.rs b/src/server/seat.rs
index 18b3798..31c7afd 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1512,7 +1512,7 @@ unsafe fn displace_covered(
if !matches!((*w).state, crate::window::WindowState::Mapped) {
continue;
}
- if (*w).is_status_bar() || (*w).is_wallpaper() {
+ if (*w).is_status_bar() || (*w).is_wallpaper() || (*w).is_grid() {
continue;
}
let mode = wm.get_mode_for_window(w);
diff --git a/src/server/window.rs b/src/server/window.rs
index 70013da..3731886 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -426,6 +426,17 @@ pub struct Window {
pub saved_virtual_x: f64,
pub saved_virtual_y: f64,
pub was_tiled: bool,
+ /// Declared the desktop-grid layer via zcce_toplevel_v1.set_grid (the
+ /// app_id "cce-grid" convention also maps the role; the flag makes the
+ /// declaration explicit and app_id-independent).
+ pub grid_declared: bool,
+ /// Grid windows: patch sent to the client, awaiting ack_grid_patch.
+ pub grid_patch_pending: Option<(u32, crate::policy::api::GridPatch)>,
+ /// Acked patch awaiting the client's next commit (the rendered buffer).
+ pub grid_patch_acked: Option<(u32, crate::policy::api::GridPatch)>,
+ /// The patch the CURRENT buffer covers — what arrange anchors to.
+ pub grid_patch_current: Option<crate::policy::api::GridPatch>,
+ pub grid_patch_serial: u32,
pub saved_floating_width: i32,
pub saved_floating_height: i32,
pub saved_floating_virtual_x: f64,
@@ -463,9 +474,16 @@ impl Window {
}
pub unsafe fn role(&self) -> crate::policy::api::WindowRole {
+ if self.grid_declared {
+ return crate::policy::api::WindowRole::Grid;
+ }
crate::policy::api::WindowRole::from_app_id(self.get_app_id_string().as_deref())
}
+ pub unsafe fn is_grid(&self) -> bool {
+ self.role() == crate::policy::api::WindowRole::Grid
+ }
+
pub unsafe fn is_status_bar(&self) -> bool {
self.role() == crate::policy::api::WindowRole::StatusBar
}
@@ -643,6 +661,11 @@ impl Window {
saved_virtual_x: 0.0,
saved_virtual_y: 0.0,
was_tiled: false,
+ grid_declared: false,
+ grid_patch_pending: None,
+ grid_patch_acked: None,
+ grid_patch_current: None,
+ grid_patch_serial: 0,
saved_floating_width: 0,
saved_floating_height: 0,
saved_floating_virtual_x: 0.0,
@@ -913,7 +936,11 @@ impl Window {
return;
}
let app_id_str = self.get_app_id_string().unwrap_or_default();
- if app_id_str.is_empty() || app_id_str.starts_with("cce-status") || app_id_str == "cce-wallpaper" {
+ if app_id_str.is_empty()
+ || app_id_str.starts_with("cce-status")
+ || app_id_str == "cce-wallpaper"
+ || app_id_str == "cce-grid"
+ {
return;
}
let title_str = self.get_title_string().unwrap_or_default();
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 8a5aa5a..29dc9d0 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -81,6 +81,9 @@ pub struct WindowManager {
pub desk_pan_y: f64,
pub desk_zoom: f64,
pub mode: WindowManagerMode,
+ /// From the last arrange plan: false while a live grid client covers
+ /// the desktop, so `draw_grid` keeps only the backdrop (and labels).
+ pub grid_cells_enabled: bool,
pub layout: crate::config::Layout,
pub mode_rules: Vec<crate::config::ModeRule>,
pub keybinds: Vec<crate::config::Keybind>,
@@ -272,6 +275,7 @@ impl WindowManager {
self.pending_screenshot = None;
self.pending_ipc_reply = None;
self.mode = WindowManagerMode::Normal;
+ self.grid_cells_enabled = true;
self.restore_queue = Vec::new();
self.last_window_states = Vec::new();
self.pending_placements = Vec::new();
@@ -366,11 +370,12 @@ impl WindowManager {
self.global = ffi::wl_global_create(
(*server).wl_server,
&ffi::zcce_window_manager_v1_interface,
- // 5 = set_utility exists on toplevels; clients feature-gate on
+ // 6 = grid support (toplevel v4: set_grid/grid_patch/ack);
+ // 5 = set_utility exists on toplevels. Clients feature-gate on
// the negotiated version, so one launched into an older
- // compositor degrades to a plain floating window instead of
- // dying on an unknown opcode.
- 5,
+ // compositor degrades gracefully instead of dying on an
+ // unknown opcode.
+ 6,
self as *mut WindowManager as *mut _,
Some(bind),
);
@@ -613,7 +618,10 @@ impl WindowManager {
if w.is_null() || (*w).closed || matches!((*w).state, crate::window::WindowState::Closing | crate::window::WindowState::Init) {
continue;
}
- if (*w).is_status_bar() || (*w).is_wallpaper() {
+ // The grid layer is owned by its systemd unit and anchored by
+ // live patches — saving/restoring it would spawn a duplicate
+ // and dictate a stale geometry.
+ if (*w).is_status_bar() || (*w).is_wallpaper() || (*w).is_grid() {
continue;
}
// A Utility window owns its geometry entirely; saving it would
@@ -1222,12 +1230,14 @@ impl WindowManager {
let overview_eligible = !(*w).minimized
&& !is_status
&& !is_wallpaper
+ && !(*w).is_grid()
&& visible
&& resolved_mode != crate::tiling::TilingMode::Popup
&& resolved_mode != crate::tiling::TilingMode::Overlay;
let focus_cyclable = rendered.contains(&(w as usize))
&& !(*w).minimized
&& !is_status
+ && !(*w).is_grid()
&& !(*w).is_overlay_ui();
windows.push(ActionWindow {
id: WindowId((*w).ref_key),
@@ -1930,7 +1940,97 @@ impl WindowManager {
+ /// Issue grid_patch events to grid clients whose current patch no
+ /// longer comfortably covers the viewport (or whose buffer resolution
+ /// has drifted more than 2x from the zoom). One patch in flight per
+ /// window; a failed send (no toplevel resource yet, old client) simply
+ /// retries on a later pass.
+ pub unsafe fn update_grid_patches(&mut self) {
+ let mut out_box: Option<ffi::wlr_box> = None;
+ let outputs_list = &(*self.server).om.outputs as *const ffi::wl_list as *mut WlList;
+ let mut curr_out = (*outputs_list).next;
+ while curr_out != outputs_list {
+ let output = crate::container_of!(curr_out, crate::output::Output, link);
+ if (*output).sent.state == crate::output::OutputStateValue::Enabled {
+ out_box = Some((*output).sent.box_layout());
+ break;
+ }
+ curr_out = (*curr_out).next;
+ }
+ let Some(out) = out_box else { return };
+ let zoom = crate::policy::background::sanitized_zoom(self.desk_zoom);
+ let vw = out.width as f64 / zoom;
+ let vh = out.height as f64 / zoom;
+ let (vx, vy) = (self.desk_pan_x, self.desk_pan_y);
+ // Buffer px per virtual unit: zoom quantized to a power of two so
+ // small zoom wobbles don't re-render the world.
+ let q = (2f64).powf(zoom.log2().round()).clamp(0.125, 2.0);
+ let period = self.layout.desktop_grid_scale
+ + (self.layout.desktop_gap_width as f64).max(0.0);
+ let covers = |p: &crate::policy::api::GridPatch| -> bool {
+ let mx = vw * 0.25;
+ let my = vh * 0.25;
+ p.x <= vx - mx
+ && p.y <= vy - my
+ && p.x + p.w >= vx + vw + mx
+ && p.y + p.h >= vy + vh + my
+ && (zoom / p.scale) > 0.5
+ && (zoom / p.scale) < 2.01
+ };
+ for &w in self.windows.iter() {
+ if w.is_null() || (*w).closed || !(*w).is_grid() {
+ continue;
+ }
+ if !matches!((*w).state, crate::window::WindowState::Mapped) {
+ continue;
+ }
+ if (*w).grid_patch_current.as_ref().map_or(false, &covers) {
+ continue;
+ }
+ if let Some((_, pending)) = &(*w).grid_patch_pending {
+ if covers(pending) {
+ continue;
+ }
+ }
+ if let Some((serial, acked)) = &(*w).grid_patch_acked {
+ // Rendered but not yet committed: give it a frame.
+ let _ = (serial, acked);
+ continue;
+ }
+ // One viewport of margin per side, shrunk if the buffer would
+ // exceed the cap; then period-aligned outward so the client
+ // draws whole cells.
+ const MAX_BUF: f64 = 8192.0;
+ let m = (((MAX_BUF / q) - vw) / (2.0 * vw)).clamp(0.0, 1.0)
+ .min((((MAX_BUF / q) - vh) / (2.0 * vh)).clamp(0.0, 1.0));
+ let x0 = ((vx - m * vw) / period).floor() * period;
+ let y0 = ((vy - m * vh) / period).floor() * period;
+ let x1 = ((vx + (1.0 + m) * vw) / period).ceil() * period;
+ let y1 = ((vy + (1.0 + m) * vh) / period).ceil() * period;
+ let patch = crate::policy::api::GridPatch {
+ x: x0,
+ y: y0,
+ w: x1 - x0,
+ h: y1 - y0,
+ scale: q,
+ };
+ (*w).grid_patch_serial = (*w).grid_patch_serial.wrapping_add(1);
+ let serial = (*w).grid_patch_serial;
+ if (*self.server)
+ .cce_window_management
+ .send_grid_patch((*w).ref_key, serial, patch)
+ {
+ log::info!("[Grid] sent patch #{serial}: {:.0},{:.0} {:.0}x{:.0} @{:.3}",
+ patch.x, patch.y, patch.w, patch.h, patch.scale);
+ (*w).grid_patch_pending = Some((serial, patch));
+ } else {
+ log::info!("[Grid] patch #{serial} not sent (no toplevel resource yet)");
+ }
+ }
+ }
+
pub unsafe fn arrange_views(&mut self) {
+ self.update_grid_patches();
self.update_restore_placeholders();
if arrange_debug() {
log::debug!("Monolithic arrange_views triggered. Windows: {}", self.windows.count());
@@ -2054,6 +2154,7 @@ impl WindowManager {
was_tiled: (*win_ptr).was_tiled,
saved_floating_size: ((*win_ptr).saved_floating_width, (*win_ptr).saved_floating_height),
saved_floating_virtual: ((*win_ptr).saved_floating_virtual_x, (*win_ptr).saved_floating_virtual_y),
+ grid_patch: (*win_ptr).grid_patch_current,
});
win_ptrs.push(win_ptr);
}
@@ -2100,6 +2201,14 @@ impl WindowManager {
ffi::wlr_scene_node_set_enabled((*output).background_rect as *mut ffi::wlr_scene_node, plan.background_rect_enabled);
}
}
+ if self.grid_cells_enabled != plan.grid_cells_enabled {
+ self.grid_cells_enabled = plan.grid_cells_enabled;
+ // The cell pools redraw only on structure changes; force one so
+ // the swap (client grid <-> compositor cells) is immediate.
+ for &output in &active_outputs {
+ (*output).grid_force_redraw_frames = 3;
+ }
+ }
for (&win_ptr, wp) in win_ptrs.iter().zip(plan.windows.iter()) {
if let Some(enabled) = wp.scene_enabled {
@@ -2440,7 +2549,7 @@ impl WindowManager {
let app_id = (*w).get_app_id_string();
let is_status_bar = app_id.as_deref().map_or(false, |id| id.starts_with("cce-status"));
let is_wallpaper = app_id.as_deref() == Some("cce-wallpaper");
- !is_status_bar && !is_wallpaper
+ !is_status_bar && !is_wallpaper && !(*w).is_grid()
};
let candidate = |w: *mut Window| crate::policy::focus::FocusCandidate {
id: crate::policy::api::WindowId((*w).ref_key),
@@ -2480,7 +2589,7 @@ impl WindowManager {
}
match (*w).get_app_id_string().as_deref() {
Some(id) if id.starts_with("cce-status") => false,
- Some("cce-wallpaper") | Some("cce-cloud") => false,
+ Some("cce-wallpaper") | Some("cce-cloud") | Some("cce-grid") => false,
_ => true,
}
}
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index 751e7d0..0959fd4 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -531,6 +531,15 @@ unsafe extern "C" fn handle_ack_configure(
unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
let toplevel = crate::container_of!(listener, XdgToplevel, commit);
let window = (*toplevel).window;
+ // Grid-patch latch: the first commit after ack_grid_patch carries the
+ // buffer rendered for that patch — anchor to it from this commit on.
+ // Latching here (not at ack time) means an in-flight older buffer is
+ // never shown at the new anchor.
+ if let Some((serial, patch)) = (*window).grid_patch_acked.take() {
+ log::info!("[Grid] latched patch #{serial} on commit");
+ (*window).grid_patch_current = Some(patch);
+ (*(*window).server).wm.dirty_windowing();
+ }
let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
let old_geometry = (*toplevel).geometry;
let mut new_geometry = std::mem::zeroed();