git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit0a68510a7049f33eaa3e5d2cabe728074b0bf8c0
parent92c655071f
authorLucas Galante <[email protected]>
date2026-09-08 22:21
fix(scenefx): hit-test a clipped surface at ratio 1, not surface/geometry

feab593 taught scene_node_at_iterator to map a dest-scaled surface's
node-local point back into surface-local coordinates, using the ratio of
the surface's current size to dst. That ratio is right only while the
whole surface is displayed. A clipped surface — how a CSD toplevel is
cropped to its xdg geometry, shedding the client-side shadow margins
(apply_surface_clip) — shows just the clip, at dst == clip size: a crop
at ratio 1. Its surface size still differs from dst by the margins, so
the mapping inflated every pointer coordinate on such a window by
surface / geometry.

On Inkscape's start screen (GTK3, 752x718 surface cropped to a 700x666
geometry) that put clicks 7-8% below and right of the cursor, growing
toward the far edges, and left the bottom and right bands dead: the
inflated point fell outside the surface, so the region check failed and
no event was delivered at all. Every floating CSD window under the
compositor's crop behaved the same; tiled windows drop their margins, so
nothing showed there.

Measure the ratio against the displayed region instead: the clip's
extent when one is set (clamped to the surface, as the commit path
clamps it), the whole surface otherwise. The surface callback adds the
clip origin itself. The grid patch — the one buffer that rests scaled,
and never clipped — is unchanged, and a clipped buffer that is also
dest-scaled (mid-zoom) resolves to clip / (clip * zoom) as it should.

Verified in a scale-2 shadow with a GTK3 header-bar probe reporting the
coordinates it receives: before, x came back as 1.13x and y as 1.15x
(exactly 452/400 and 399/347) and three of five probes in the lower half
produced no event; after, every probe reads back 1:1 to the pixel.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 scenefx/types/scene/wlr_scene.c | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/scenefx/types/scene/wlr_scene.c b/scenefx/types/scene/wlr_scene.c
index 85906c8..bb1aa42 100644
--- a/scenefx/types/scene/wlr_scene.c
+++ b/scenefx/types/scene/wlr_scene.c
@@ -2367,12 +2367,40 @@ static bool scene_node_at_iterator(struct wlr_scene_node *node,
 		// deltas hard enough to fling the item across the canvas.
 		// Buffers displayed at their natural size (every window at rest)
 		// scale by exactly 1 here and are untouched.
+		//
+		// The ratio is between dst and the DISPLAYED region of the
+		// surface, which is the whole surface only while no clip is set.
+		// A clipped surface (wlr_scene_subsurface_tree_set_clip — how a
+		// CSD toplevel is cropped to its xdg geometry, shedding the
+		// client-side shadow margins) shows just the clip, at dst ==
+		// clip size, so it is a crop at ratio 1, not a scale — yet its
+		// surface size still differs from dst by the margins. Measuring
+		// against the whole surface inflated every pointer coordinate on
+		// such a window by (surface / geometry): a GTK3 dialog 718px
+		// tall cropped to 666 received clicks 8% below the cursor, more
+		// the further down, and the bottom band was dead because the
+		// inflated point fell outside the surface. The clip's origin is
+		// added by the surface callback below; only the extent matters
+		// here, clamped to the surface as the commit path clamps it.
 		if (scene_buffer->dst_width > 0 && scene_buffer->dst_height > 0) {
 			struct wlr_scene_surface *scene_surface =
 				wlr_scene_surface_try_from_buffer(scene_buffer);
 			if (scene_surface != NULL && scene_surface->surface != NULL) {
 				int sw = scene_surface->surface->current.width;
 				int sh = scene_surface->surface->current.height;
+				const struct wlr_box *clip = &scene_surface->clip;
+				if (!wlr_box_empty(clip)) {
+					if (clip->width < sw - clip->x) {
+						sw = clip->width;
+					} else {
+						sw -= clip->x;
+					}
+					if (clip->height < sh - clip->y) {
+						sh = clip->height;
+					} else {
+						sh -= clip->y;
+					}
+				}
 				if (sw > 0 && sh > 0 && (sw != scene_buffer->dst_width ||
 						sh != scene_buffer->dst_height)) {
 					rx = rx * sw / scene_buffer->dst_width;