The initial Slint problem was larger than thought. Hopefully a more universal fix.
This commit is contained in:
1 parent
d0fdce7285
commit
1c12bc6636
10 files changed
+294
-6
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user