The initial Slint problem was larger than thought. Hopefully a more universal fix.

This commit is contained in:
Storm Dragon committed 2026-11-01 15:48:33 -05:00
1 parent d0fdce7285
commit 1c12bc6636
10 files changed
+294 -6

No files matched your search

+2 -1
View File
@@ -152,7 +152,8 @@ func (handler *accessibilityEventHandler) handle(event atspi.Event) {
"active", check.Active, "showing", check.Showing, "iconified", check.Iconified,
"accessible_bounds", check.Bounds,
"x_bounds", [4]int{client.X, client.Y, client.Width, client.Height},
"x_outer_bounds", [4]int{client.OuterX, client.OuterY, client.OuterWidth, client.OuterHeight})
"x_outer_bounds", [4]int{client.OuterX, client.OuterY, client.OuterWidth, client.OuterHeight},
"x_frame_bounds", [4]int{client.FrameX, client.FrameY, client.FrameWidth, client.FrameHeight})
} else {
logger.Debug("ignored accessibility event outside X11 foreground", "type", event.Type,
"bus", event.Window.BusName, "window_path", event.Window.Path,
+3 -1
View File
@@ -46,8 +46,10 @@ func foregroundWindowMatches(client xorg.Foreground, pid int, bounds Rectangle)
}
inner := Rectangle{X: int32(client.X), Y: int32(client.Y), Width: int32(client.Width), Height: int32(client.Height)}
outer := Rectangle{X: int32(client.OuterX), Y: int32(client.OuterY), Width: int32(client.OuterWidth), Height: int32(client.OuterHeight)}
frame := Rectangle{X: int32(client.FrameX), Y: int32(client.FrameY), Width: int32(client.FrameWidth), Height: int32(client.FrameHeight)}
return bounds == inner || bounds == outer ||
scaledWindowMatches(bounds, inner) || scaledWindowMatches(bounds, outer)
frame.Valid() && bounds == frame ||
scaledWindowMatches(bounds, inner) || scaledWindowMatches(bounds, outer) || scaledWindowMatches(bounds, frame)
}
// Some Chromium windows report AT-SPI extents in logical pixels while X11
+27
View File
@@ -44,6 +44,33 @@ func TestForegroundWindowCorrelationSteamBigPictureScale(t *testing.T) {
}
}
func TestForegroundReparentedFrameCorrelation(t *testing.T) {
client := xorg.Foreground{Known: true, Window: 45, PID: 123,
X: 2, Y: 18, Width: 1912, Height: 974, OuterX: 2, OuterY: 18, OuterWidth: 1912, OuterHeight: 974,
FrameX: 0, FrameY: 0, FrameWidth: 1916, FrameHeight: 994}
for _, test := range []struct {
name string
pid int
bounds Rectangle
want bool
}{
{"actual WM frame", 123, Rectangle{0, 0, 1916, 994}, true},
{"client still accepted", 123, Rectangle{2, 18, 1912, 974}, true},
{"other process same frame size", 124, Rectangle{0, 0, 1916, 994}, false},
{"same process other frame", 123, Rectangle{1920, 0, 1916, 994}, false},
{"same process smaller dialog", 123, Rectangle{20, 30, 400, 300}, false},
{"scaled frame same origin", 123, Rectangle{0, 0, 958, 497}, true},
{"scaled frame wrong origin", 123, Rectangle{1, 0, 958, 497}, false},
{"scaled frame wrong aspect", 123, Rectangle{0, 0, 958, 400}, false},
} {
t.Run(test.name, func(t *testing.T) {
if got := foregroundWindowMatches(client, test.pid, test.bounds); got != test.want {
t.Fatalf("match = %v, want %v", got, test.want)
}
})
}
}
func TestPreferencesForegroundIdentity(t *testing.T) {
// Slint/i3 reports GTK's frame and the X11 client with different borders.
client := xorg.Foreground{Known: true, Window: 45, PID: 123,
@@ -22,6 +22,9 @@ type Foreground struct {
PropertyPID int
X, Y, Width, Height int
OuterX, OuterY, OuterWidth, OuterHeight int
// Frame bounds come from the client's enclosing X11 window, independently
// of _NET_FRAME_EXTENTS. Zero dimensions mean no unambiguous frame exists.
FrameX, FrameY, FrameWidth, FrameHeight int
}
// ForegroundReader has no goroutine. Its native event-loop owner opens, reads,
@@ -60,7 +63,8 @@ func (reader *ForegroundReader) Read() Foreground {
pid = reader.clientPID
}
return Foreground{Known: value.known != 0, Window: window, PID: pid, PropertyPID: propertyPID,
X: int(value.x), Y: int(value.y), Width: int(value.width), Height: int(value.height), OuterX: int(value.outerX), OuterY: int(value.outerY), OuterWidth: int(value.outerWidth), OuterHeight: int(value.outerHeight)}
X: int(value.x), Y: int(value.y), Width: int(value.width), Height: int(value.height), OuterX: int(value.outerX), OuterY: int(value.outerY), OuterWidth: int(value.outerWidth), OuterHeight: int(value.outerHeight),
FrameX: int(value.frameX), FrameY: int(value.frameY), FrameWidth: int(value.frameWidth), FrameHeight: int(value.frameHeight)}
}
func (reader *ForegroundReader) Close() {
if reader != nil {
+52
View File
@@ -175,6 +175,57 @@ PointerPosition cthulhu_read_pointer(Display *display) {
if (cthulhu_x11_end_read(display)) result = (PointerPosition){0};
return result;
}
// Resolve only the client's immediate enclosing frame. Some WMs omit or leave
// stale _NET_FRAME_EXTENTS, while GTK reports the real reparenting frame.
// A container with another managed client is not this client's decoration.
// The caller holds the Xlib error guard throughout these read-only queries.
static void cthulhu_read_frame_bounds(Display *display, Window window, Foreground *result) {
Window root, parent, *children = NULL;
unsigned int count = 0;
if (!XQueryTree(display, window, &root, &parent, &children, &count)) return;
if (children) XFree(children);
if (parent == None || parent == root) return;
XWindowAttributes attributes;
if (!XGetWindowAttributes(display, parent, &attributes) ||
attributes.class != InputOutput || attributes.override_redirect ||
attributes.map_state != IsViewable) return;
Atom stateAtom = XInternAtom(display, "WM_STATE", False);
Atom type = None; int format = 0; unsigned long items = 0;
unsigned char *data = cthulhu_window_property(display, parent, stateAtom, &type, &format, &items);
if (data) XFree(data);
if (type != None) return;
Window treeRoot, frameParent;
children = NULL;
if (!XQueryTree(display, parent, &treeRoot, &frameParent, &children, &count)) return;
int unambiguous = count <= 64, foundClient = 0;
for (unsigned int index = 0; unambiguous && index < count; index++) {
if (children[index] == window) { foundClient = 1; continue; }
data = cthulhu_window_property(display, children[index], stateAtom, &type, &format, &items);
if (data) XFree(data);
if (type != None) unambiguous = 0;
}
if (children) XFree(children);
if (!unambiguous || !foundClient) return;
int x, y;
Window child;
if (!XTranslateCoordinates(display, parent, root, 0, 0, &x, &y, &child)) return;
int border = attributes.border_width;
x -= border; y -= border;
int width = attributes.width + 2 * border, height = attributes.height + 2 * border;
// Decorations must enclose the client and remain bounded like the EWMH
// extents. Never turn an unrelated workspace into a frame match.
int left = result->x - x, top = result->y - y;
int right = x + width - result->x - result->width;
int bottom = y + height - result->y - result->height;
if (left < 0 || top < 0 || right < 0 || bottom < 0 ||
left >= 1024 || top >= 1024 || right >= 1024 || bottom >= 1024) return;
result->frameX = x; result->frameY = y;
result->frameWidth = width; result->frameHeight = height;
}
// Read-only metadata connection, owned by the caller's native event thread.
// All requests which can race window destruction share the Xlib error guard.
Foreground read_foreground(Display *display) {
@@ -227,6 +278,7 @@ Foreground read_foreground(Display *display) {
}
}
if (data) XFree(data);
cthulhu_read_frame_bounds(display, window, &result);
data=cthulhu_window_property(display,window,XInternAtom(display,"_NET_WM_PID",False),&type,&format,&items);
if (data && type==XA_CARDINAL && format==32 && items==1) result.pid=(int)((unsigned long *)data)[0];
if (data) XFree(data);
+1
View File
@@ -27,6 +27,7 @@ typedef struct {
typedef struct { int pressed, code, symbol, textSymbol, modifiers, locks; unsigned long timestamp; char text[64]; } CommandKey;
typedef struct {
int known, pid, x, y, width, height, outerX, outerY, outerWidth, outerHeight;
int frameX, frameY, frameWidth, frameHeight;
unsigned long window;
} Foreground;
typedef struct { int known, pid, x, y; unsigned long window; } PointerPosition;
@@ -0,0 +1,95 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package xorg
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"runtime"
"testing"
"time"
)
func TestNativeReparentedFrameForeground(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_FRAME_FOREGROUND") != "1" {
t.Skip("requires private X11 reparented-window fixture")
}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
fixture := exec.CommandContext(ctx, "/usr/bin/python3", "-u", "../../../tests/x11_frame_fixture.py")
input, err := fixture.StdinPipe()
if err != nil {
t.Fatal(err)
}
output, err := fixture.StdoutPipe()
if err != nil {
t.Fatal(err)
}
fixture.Stderr = os.Stderr
if err := fixture.Start(); err != nil {
t.Fatal(err)
}
defer func() { input.Close(); fixture.Wait() }()
scanner := bufio.NewScanner(output)
if !scanner.Scan() {
t.Fatal("frame fixture did not start")
}
var windows map[string]uint64
if err := json.Unmarshal(scanner.Bytes(), &windows); err != nil {
t.Fatal(err)
}
reader := OpenForegroundReader()
if reader == nil {
t.Fatal("private X11 connection unavailable")
}
defer reader.Close()
for _, test := range []struct {
action string
client string
frame [4]int
}{
{"client", "client", [4]int{0, 0, 1916, 994}},
{"zero-extents", "client", [4]int{0, 0, 1916, 994}},
{"stale-extents", "client", [4]int{0, 0, 1916, 994}},
{"resize", "client", [4]int{0, 0, 1000, 700}},
{"move", "client", [4]int{-30, 40, 1916, 994}},
{"border", "client", [4]int{0, 0, 1920, 998}},
{"other", "other", [4]int{2000, 0, 500, 400}},
{"plain", "plain", [4]int{}},
{"shared", "shared", [4]int{}},
{"nested", "nested", [4]int{}},
{"oversized", "oversized", [4]int{}},
{"overflow", "overflow", [4]int{}},
{"empty", "empty", [4]int{}},
{"client", "client", [4]int{0, 0, 1916, 994}},
} {
t.Run(test.action, func(t *testing.T) {
if _, err := fmt.Fprintln(input, test.action); err != nil {
t.Fatal(err)
}
if !scanner.Scan() || scanner.Text() != "ready" {
t.Fatal("fixture command failed")
}
got := reader.Read()
wantPID := fixture.Process.Pid
if test.client == "empty" {
wantPID = 0
}
if !got.Known || got.Window != windows[test.client] || got.PID != wantPID {
t.Fatalf("foreground identity changed: %+v", got)
}
frame := [4]int{got.FrameX, got.FrameY, got.FrameWidth, got.FrameHeight}
if frame != test.frame {
t.Fatalf("frame = %v; want %v (foreground: %+v)", frame, test.frame, got)
}
})
}
}