Merge e264045642b9b7340e24f3d336fb638cd69c5375 into 007c134bb83968897ef9efe3d134f65ad337aa79
85 files · +13289 -149
M .mockery.yml +20 -0
| @@ -182,3 +182,23 @@ packages: | |||||
| 182 | dir: internal/adapters/screencast | 182 | dir: internal/adapters/screencast | ||
| 183 | pkgname: screencast | 183 | pkgname: screencast | ||
| 184 | filename: pipewire_opener_mock_test.go | 184 | filename: pipewire_opener_mock_test.go | ||
| 185 | + | ||||
| 186 | + | github.com/bnema/neferwl/internal/adapters/xwayland: | |||
| 187 | + | interfaces: | |||
| 188 | + | starter: | |||
| 189 | + | config: | |||
| 190 | + | dir: internal/adapters/xwayland | |||
| 191 | + | pkgname: xwayland | |||
| 192 | + | filename: starter_mock_test.go | |||
| 193 | + | github.com/bnema/neferwl/internal/adapters/wayland/xwm: | |||
| 194 | + | interfaces: | |||
| 195 | + | host: | |||
| 196 | + | config: | |||
| 197 | + | dir: internal/adapters/wayland/xwm | |||
| 198 | + | pkgname: xwm | |||
| 199 | + | filename: host_mock_test.go | |||
| 200 | + | fdSource: | |||
| 201 | + | config: | |||
| 202 | + | dir: internal/adapters/wayland/xwm | |||
| 203 | + | pkgname: xwm | |||
| 204 | + | filename: fdsource_mock_test.go | |||
M Makefile +9 -2
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | .PHONY: build test vet race mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install | 1 | + | .PHONY: build test vet race e2e-xwayland mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install |
| 2 | 2 | ||||
| 3 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | 3 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | ||
| 4 | TTY_TIMEOUT ?= 0 | 4 | TTY_TIMEOUT ?= 0 | ||
| @@ -26,6 +26,7 @@ test: | |||||
| 26 | CGO_ENABLED=0 go test ./... | 26 | CGO_ENABLED=0 go test ./... | ||
| 27 | vet: | 27 | vet: | ||
| 28 | CGO_ENABLED=0 go vet ./... | 28 | CGO_ENABLED=0 go vet ./... | ||
| 29 | + | CGO_ENABLED=0 go vet -tags xwayland ./internal/app | |||
| 29 | race: | 30 | race: | ||
| 30 | # cgo is enabled only for the race test binary. | 31 | # cgo is enabled only for the race test binary. | ||
| 31 | CGO_ENABLED=1 go test -race ./... | 32 | CGO_ENABLED=1 go test -race ./... | ||
| @@ -69,7 +70,8 @@ adapter-imports: | |||||
| 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | ||
| 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 71 | perf-check: | 72 | perf-check: | ||
| 72 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing)$$' -count=1 | 73 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations)$$' -count=1 |
| 74 | + | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |||
| 73 | check: vet test arch fakes-check log-check perf-check | 75 | check: vet test arch fakes-check log-check perf-check | ||
| 74 | 76 | ||||
| 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 77 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | ||
| @@ -77,6 +79,11 @@ check: vet test arch fakes-check log-check perf-check | |||||
| 77 | color-check: | 79 | color-check: | ||
| 78 | CGO_ENABLED=0 go test -tags colorcheck ./internal/app -run '^TestColorCheck' -count=1 -v | 80 | CGO_ENABLED=0 go test -tags colorcheck ./internal/app -run '^TestColorCheck' -count=1 -v | ||
| 79 | 81 | ||||
| 82 | + | # End-to-end test of the built-in XWM: a real Xwayland and an X11 probe on a | |||
| 83 | + | # headless NeferWL. Needs Xwayland and a GPU; not part of check. | |||
| 84 | + | e2e-xwayland: | |||
| 85 | + | CGO_ENABLED=1 go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v | |||
| 86 | + | ||||
| 80 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | 87 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | ||
| 81 | # come from the module proxy in prepare(); the build itself runs offline. | 88 | # come from the module proxy in prepare(); the build itself runs offline. | ||
| 82 | # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | 89 | # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | ||
M README.md +1 -1
| @@ -149,7 +149,7 @@ See [Configuration](docs/config.md) for all keys, actions, bindings and automati | |||||
| 149 | - Explicit sync carries client fences to the GPU and KMS, enabled by default with no flag. HDR10 on capable displays includes configurable SDR brightness. | 149 | - Explicit sync carries client fences to the GPU and KMS, enabled by default with no flag. HDR10 on capable displays includes configurable SDR brightness. | ||
| 150 | - Separate workspaces and fractional scale per output; workspaces move when a monitor is unplugged and return when it reconnects. | 150 | - Separate workspaces and fractional scale per output; workspaces move when a monitor is unplugged and return when it reconnects. | ||
| 151 | - Hidden apps get frame callbacks once per second and the suspended state, whether on another workspace, in a hidden stash, off screen or under fullscreen. Captured windows keep their output's refresh rate. | 151 | - Hidden apps get frame callbacks once per second and the suspended state, whether on another workspace, in a hidden stash, off screen or under fullscreen. Captured windows keep their output's refresh rate. | ||
| 152 | - | - Steam and other X11 clients run through xwayland-satellite; Wine can run natively on Wayland. | 152 | + | - Steam and other X11 clients run through xwayland-satellite, or through the experimental built-in window manager (`xwayland = builtin`; shows menus, popups and tooltips; no clipboard or XSETTINGS yet). Wine can run natively on Wayland. |
| 153 | 153 | ||||
| 154 | ### Windows and workspaces | 154 | ### Windows and workspaces | ||
| 155 | 155 | ||||
M docs/config.md +3 -1
| @@ -27,7 +27,7 @@ See [examples/config](../examples/config) for every key with its default. | |||||
| 27 | | `terminal` | `$TERMINAL` or `foot` | Terminal command | | 27 | | `terminal` | `$TERMINAL` or `foot` | Terminal command | | ||
| 28 | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | | 28 | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | | ||
| 29 | | `startup` | none | Commands run at session start, separated by `;` | | 29 | | `startup` | none | Commands run at session start, separated by `;` | | ||
| 30 | - | | `xwayland` | `xwayland-satellite` | X11 support; `off` disables it | | 30 | + | | `xwayland` | `satellite` | `off`, `satellite`, `builtin` or a satellite binary path | |
| 31 | | `background` | `#111111` | Background color | | 31 | | `background` | `#111111` | Background color | | ||
| 32 | | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps | | 32 | | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps | | ||
| 33 | | `border.width` | `2` | Border width in pixels; `0` hides borders | | 33 | | `border.width` | `2` | Border width in pixels; `0` hides borders | | ||
| @@ -103,6 +103,8 @@ destination output, even if that output has no windows. Same-screen workspace | |||||
| 103 | switches and mouse focus do not trigger a flash. | 103 | switches and mouse focus do not trigger a flash. | ||
| 104 | It runs when `animations = on`. | 104 | It runs when `animations = on`. | ||
| 105 | 105 | ||||
| 106 | + | `xwayland = satellite` uses `xwayland-satellite` from `PATH`; a custom path also selects satellite. `off` disables X11. `builtin` (experimental): neferwl starts `Xwayland` itself and manages X11 windows as its window manager: windows tile, take focus, close with `close-window` (`WM_DELETE_WINDOW`) and show their title and class. Menus, popups and tooltips (override-redirect windows) are drawn above their window at the right place and never appear as windows in `neferwl state`. Clipboard and XSETTINGS are not supported; use `satellite` if you need them. | |||
| 107 | + | ||||
| 106 | ## Automatic terminal | 108 | ## Automatic terminal | ||
| 107 | 109 | ||||
| 108 | The command comes from `terminal`, then `$TERMINAL`, then `foot`. | 110 | The command comes from `terminal`, then `$TERMINAL`, then `foot`. | ||
M docs/desktop.md +3 -1
| @@ -52,12 +52,14 @@ Candidate popups are not drawn yet. | |||||
| 52 | 52 | ||||
| 53 | ## X11 apps | 53 | ## X11 apps | ||
| 54 | 54 | ||||
| 55 | - | X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. | 55 | + | By default, X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. |
| 56 | 56 | ||||
| 57 | ```text | 57 | ```text | ||
| 58 | xwayland = off # no X11 support | 58 | xwayland = off # no X11 support | ||
| 59 | ``` | 59 | ``` | ||
| 60 | 60 | ||||
| 61 | + | With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and manages X11 windows: they tile, take focus and close like Wayland windows. Menus, popups and tooltips of X11 apps are shown above their window and do not take the keyboard focus. Clipboard and XSETTINGS are not supported; use `satellite` if you need them. | |||
| 62 | + | ||||
| 61 | ## Session locking | 63 | ## Session locking | ||
| 62 | 64 | ||||
| 63 | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL hides the desktop and blocks input. | 65 | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL hides the desktop and blocks input. | ||
M docs/dev/headless.md +16 -0
| @@ -62,6 +62,22 @@ WAYLAND_DISPLAY=<name> go run ./examples/testime -preedit nihon -commit 日本 | |||||
| 62 | 62 | ||||
| 63 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | 63 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | ||
| 64 | 64 | ||||
| 65 | + | ## X11 check | |||
| 66 | + | ||||
| 67 | + | `make e2e-xwayland` runs the end-to-end test of the built-in window manager (`xwayland = builtin`). It is behind the `xwayland` build tag, so `go test ./...` and `make check` only compile it (`go vet -tags xwayland`). It needs `Xwayland` on `PATH` and a GPU, and skips without Xwayland. | |||
| 68 | + | ||||
| 69 | + | The test starts a headless NeferWL with a real `Xwayland` on the first free X display (a live session's `:0` stays untouched) and runs `internal/app/testdata/x11probe`, a small X11 client (`-class`, `-title`, `-size`, `-sleep`) that paints its window green, prints `mapped WxH` and `focus`, and exits 0 on `WM_DELETE_WINDOW`. With `-override` it also maps a magenta override-redirect window (`-override-at`, `-override-size`, `-override-type menu|tooltip`, `-override-for`, `-override-shape`, `-override-dismiss-on-focusout`) and prints `override mapped`, `override unmapped` or `dismissed`. It checks: | |||
| 70 | + | ||||
| 71 | + | - the state lists one visible `X11Probe` window with the probe's pid, and the screenshot is mostly green inside its tile; | |||
| 72 | + | - the probe receives focus; | |||
| 73 | + | - a second probe tiles beside the first; | |||
| 74 | + | - `key Super+q` closes the focused probe within 2 seconds and the state drops to one window; | |||
| 75 | + | - an override-redirect menu is drawn magenta above the green toplevel at its X position, is not listed in the state, leaves focus on the toplevel and disappears from the screenshot once the probe unmaps it; | |||
| 76 | + | - a menu is dismissed when its owner gets `FocusOut` because another X11 window took the focus; | |||
| 77 | + | - a menu with an empty input shape is still drawn and still not a window. | |||
| 78 | + | ||||
| 79 | + | Run it by hand with `go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v`. | |||
| 80 | + | ||||
| 65 | ## Colour check | 81 | ## Colour check | ||
| 66 | 82 | ||||
| 67 | `make color-check` renders `examples/testpattern` on a headless output and checks the colours, in SDR and in HDR (`--headless-hdr`). It needs a GPU and `/dev/udmabuf`, and is not part of `make check`. | 83 | `make color-check` renders `examples/testpattern` on a headless output and checks the colours, in SDR and in HDR (`--headless-hdr`). It needs a GPU and `/dev/udmabuf`, and is not part of `make check`. | ||
M go.mod +4 -3
| @@ -3,10 +3,11 @@ module github.com/bnema/neferwl | |||||
| 3 | go 1.27 | 3 | go 1.27 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | + | bnema.dev/xturbo v0.1.0 | |||
| 6 | github.com/bnema/go-wayland-bindings v0.1.0 | 7 | github.com/bnema/go-wayland-bindings v0.1.0 | ||
| 7 | github.com/bnema/kvconf v0.1.2 | 8 | github.com/bnema/kvconf v0.1.2 | ||
| 8 | - | github.com/bnema/purego v0.13.0-bnema.1 | 9 | + | github.com/bnema/purego v0.13.0-bnema.2 |
| 9 | - | github.com/bnema/purego-libwayland v0.11.0 | 10 | + | github.com/bnema/purego-libwayland v0.12.0 |
| 10 | github.com/bnema/purego-pipewire v0.2.0 | 11 | github.com/bnema/purego-pipewire v0.2.0 | ||
| 11 | github.com/bnema/purego-vulkan v0.6.0 | 12 | github.com/bnema/purego-vulkan v0.6.0 | ||
| 12 | github.com/bnema/wlturbo v0.5.0 | 13 | github.com/bnema/wlturbo v0.5.0 | ||
| @@ -16,7 +17,7 @@ require ( | |||||
| 16 | github.com/rs/zerolog v1.35.1 | 17 | github.com/rs/zerolog v1.35.1 | ||
| 17 | github.com/stretchr/testify v1.12.1 | 18 | github.com/stretchr/testify v1.12.1 | ||
| 18 | golang.org/x/image v0.46.0 | 19 | golang.org/x/image v0.46.0 | ||
| 19 | - | golang.org/x/sys v0.48.0 | 20 | + | golang.org/x/sys v0.49.0 |
| 20 | golang.org/x/term v0.46.0 | 21 | golang.org/x/term v0.46.0 | ||
| 21 | ) | 22 | ) | ||
| 22 | 23 | ||||
M go.sum +8 -6 generated file: collapsed
M internal/adapters/config/config.go +8 -4
| @@ -167,7 +167,7 @@ func Defaults() ports.Config { | |||||
| 167 | // Hint mode is experimental: off unless the config turns it on. | 167 | // Hint mode is experimental: off unless the config turns it on. | ||
| 168 | c.Hint.Enable = false | 168 | c.Hint.Enable = false | ||
| 169 | c.Hint.TextClick = ports.ModCtrl | ports.ModShift | 169 | c.Hint.TextClick = ports.ModCtrl | ports.ModShift | ||
| 170 | - | c.Xwayland = "xwayland-satellite" | 170 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"} |
| 171 | c.Background.Color = "#111111" | 171 | c.Background.Color = "#111111" | ||
| 172 | c.Floating.Dim = 0.3 | 172 | c.Floating.Dim = 0.3 | ||
| 173 | c.Stash.Width = 80 | 173 | c.Stash.Width = 80 | ||
| @@ -726,11 +726,15 @@ func set(c *ports.Config, key, v string) error { | |||||
| 726 | case "xwayland": | 726 | case "xwayland": | ||
| 727 | switch { | 727 | switch { | ||
| 728 | case v == "off": | 728 | case v == "off": | ||
| 729 | - | c.Xwayland = "" | 729 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandOff} |
| 730 | + | case v == "satellite": | |||
| 731 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"} | |||
| 732 | + | case v == "builtin": | |||
| 733 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland"} | |||
| 730 | case v == "" || strings.ContainsAny(v, " \t"): | 734 | case v == "" || strings.ContainsAny(v, " \t"): | ||
| 731 | - | return fmt.Errorf("must be off or the xwayland-satellite program") | 735 | + | return fmt.Errorf("must be off, satellite, builtin or a satellite program") |
| 732 | default: | 736 | default: | ||
| 733 | - | c.Xwayland = v | 737 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: v} |
| 734 | } | 738 | } | ||
| 735 | case "background": | 739 | case "background": | ||
| 736 | if !color.MatchString(v) { | 740 | if !color.MatchString(v) { | ||
M internal/adapters/config/config_test.go +16 -6
| @@ -340,14 +340,24 @@ func TestWorkspacePrevNextAction(t *testing.T) { | |||||
| 340 | } | 340 | } | ||
| 341 | 341 | ||||
| 342 | func TestXwayland(t *testing.T) { | 342 | func TestXwayland(t *testing.T) { | ||
| 343 | - | if Defaults().Xwayland != "xwayland-satellite" { | 343 | + | if Defaults().Xwayland != (ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"}) { |
| 344 | t.Fatal(Defaults().Xwayland) | 344 | t.Fatal(Defaults().Xwayland) | ||
| 345 | } | 345 | } | ||
| 346 | - | if c, w := parseString(t, "xwayland = off"); c.Xwayland != "" || len(w) != 0 { | 346 | + | for _, tt := range []struct { |
| 347 | - | t.Fatal(c.Xwayland, w) | 347 | + | value string |
| 348 | - | } | 348 | + | want ports.XwaylandConfig |
| 349 | - | if c, w := parseString(t, "xwayland = /opt/xwls"); c.Xwayland != "/opt/xwls" || len(w) != 0 { | 349 | + | }{ |
| 350 | - | t.Fatal(c.Xwayland, w) | 350 | + | {"off", ports.XwaylandConfig{Backend: ports.XwaylandOff}}, |
| 351 | + | {"satellite", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"}}, | |||
| 352 | + | {"builtin", ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland"}}, | |||
| 353 | + | {"/opt/xwls", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "/opt/xwls"}}, | |||
| 354 | + | } { | |||
| 355 | + | t.Run(tt.value, func(t *testing.T) { | |||
| 356 | + | c, w := parseString(t, "xwayland = "+tt.value) | |||
| 357 | + | if c.Xwayland != tt.want || len(w) != 0 { | |||
| 358 | + | t.Fatal(c.Xwayland, w) | |||
| 359 | + | } | |||
| 360 | + | }) | |||
| 351 | } | 361 | } | ||
| 352 | } | 362 | } | ||
| 353 | 363 | ||||
M internal/adapters/vulkan/capture_test.go +3 -0
| @@ -358,6 +358,9 @@ func openFDs(t *testing.T) int { | |||||
| 358 | // slot keeps its sync file for every lease. No mock stands in the measured | 358 | // slot keeps its sync file for every lease. No mock stands in the measured | ||
| 359 | // closure. | 359 | // closure. | ||
| 360 | func TestCaptureCycleAllocations(t *testing.T) { | 360 | func TestCaptureCycleAllocations(t *testing.T) { | ||
| 361 | + | if raceEnabled { | |||
| 362 | + | t.Skip("allocation budgets are not stable under the race runtime") | |||
| 363 | + | } | |||
| 361 | for _, tc := range []struct { | 364 | for _, tc := range []struct { | ||
| 362 | name string | 365 | name string | ||
| 363 | new func(*testing.T) *Renderer | 366 | new func(*testing.T) *Renderer | ||
A internal/adapters/vulkan/norace_test.go +5 -0
| @@ -0,0 +1,5 @@ | |||||
| 1 | + | //go:build !race | |||
| 2 | + | ||||
| 3 | + | package vulkan | |||
| 4 | + | ||||
| 5 | + | const raceEnabled = false | |||
A internal/adapters/vulkan/race_test.go +8 -0
| @@ -0,0 +1,8 @@ | |||||
| 1 | + | //go:build race | |||
| 2 | + | ||||
| 3 | + | package vulkan | |||
| 4 | + | ||||
| 5 | + | // raceEnabled: sync.Pool drops items at random under the race runtime, so the | |||
| 6 | + | // allocation budgets of calls that go through purego cannot hold. perf-check | |||
| 7 | + | // runs them without it. | |||
| 8 | + | const raceEnabled = true | |||
M internal/adapters/vulkan/scene_perf_test.go +6 -0
| @@ -16,6 +16,9 @@ import ( | |||||
| 16 | // A new scene Seq with one of two windows moved is a partial redraw seeded by | 16 | // A new scene Seq with one of two windows moved is a partial redraw seeded by | ||
| 17 | // sceneDelta and allocates nothing either. | 17 | // sceneDelta and allocates nothing either. | ||
| 18 | func TestRenderSteadyStateAllocations(t *testing.T) { | 18 | func TestRenderSteadyStateAllocations(t *testing.T) { | ||
| 19 | + | if raceEnabled { | |||
| 20 | + | t.Skip("allocation budgets are not stable under the race runtime") | |||
| 21 | + | } | |||
| 19 | for _, tc := range []struct { | 22 | for _, tc := range []struct { | ||
| 20 | name string | 23 | name string | ||
| 21 | transform ports.BufferTransform | 24 | transform ports.BufferTransform | ||
| @@ -145,6 +148,9 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||||
| 145 | // keeps one alloc of headroom for drivers whose import path differs. | 148 | // keeps one alloc of headroom for drivers whose import path differs. | ||
| 146 | // Skipped without a GPU or /dev/udmabuf. | 149 | // Skipped without a GPU or /dev/udmabuf. | ||
| 147 | func TestRenderSteadyStateDMABufAllocations(t *testing.T) { | 150 | func TestRenderSteadyStateDMABufAllocations(t *testing.T) { | ||
| 151 | + | if raceEnabled { | |||
| 152 | + | t.Skip("allocation budgets are not stable under the race runtime") | |||
| 153 | + | } | |||
| 148 | r, err := New(80, 32) | 154 | r, err := New(80, 32) | ||
| 149 | if err != nil { | 155 | if err != nil { | ||
| 150 | t.Skipf("Vulkan unavailable: %v", err) | 156 | t.Skipf("Vulkan unavailable: %v", err) | ||
M internal/adapters/wayland/activation.go +3 -3
| @@ -66,7 +66,7 @@ func (a activation) Activate(_ *xdgactivation.ActivationV1, token string, surf * | |||||
| 66 | } | 66 | } | ||
| 67 | var w *window | 67 | var w *window | ||
| 68 | if reason == "" { | 68 | if reason == "" { | ||
| 69 | - | if w = s.windows[state.root().windowID()]; w == nil || w.toplevel == nil || !w.mapped { | 69 | + | if w = s.windows[state.root().windowID()]; w == nil || !w.isToplevel() || !w.mapped { |
| 70 | reason = "not_toplevel" | 70 | reason = "not_toplevel" | ||
| 71 | } | 71 | } | ||
| 72 | } | 72 | } | ||
| @@ -116,8 +116,8 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) { | |||||
| 116 | 116 | ||||
| 117 | // focusClient is the client with the keyboard focus. | 117 | // focusClient is the client with the keyboard focus. | ||
| 118 | func (s *Server) focusClient() server.Client { | 118 | func (s *Server) focusClient() server.Client { | ||
| 119 | - | if w := s.windows[s.seat.focused]; w != nil && w.xdg.resource.Resource.Alive() { | 119 | + | if w := s.windows[s.seat.focused]; w != nil && w.alive() { |
| 120 | - | return w.xdg.resource.Client() | 120 | + | return w.client() |
| 121 | } | 121 | } | ||
| 122 | if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() { | 122 | if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() { | ||
| 123 | return l.resource.Client() | 123 | return l.resource.Client() | ||
M internal/adapters/wayland/constraints.go +14 -13
| @@ -61,11 +61,11 @@ func (h pointerWarp) WarpPointer(r *pointerwarp.WpPointerWarpV1, surf *wayland.S | |||||
| 61 | return | 61 | return | ||
| 62 | } | 62 | } | ||
| 63 | w := s.windows[s.seat.pointerFocus] | 63 | w := s.windows[s.seat.pointerFocus] | ||
| 64 | - | if w == nil || !w.mapped || w.xdg.resource.Client() != r.Client() || s.surfaces[surf.Resource] != w.xdg.surface { | 64 | + | if w == nil || !w.mapped || w.client() != r.Client() || s.surfaces[surf.Resource] != w.surf() { |
| 65 | return | 65 | return | ||
| 66 | } | 66 | } | ||
| 67 | // Surface-local to window-local (core's, from the geometry origin). | 67 | // Surface-local to window-local (core's, from the geometry origin). | ||
| 68 | - | g := w.xdg.geometry | 68 | + | g := w.geometry() |
| 69 | s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)}) | 69 | s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)}) | ||
| 70 | } | 70 | } | ||
| 71 | 71 | ||||
| @@ -80,12 +80,12 @@ func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool { | |||||
| 80 | if s.protected() || c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | 80 | if s.protected() || c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | ||
| 81 | return false | 81 | return false | ||
| 82 | } | 82 | } | ||
| 83 | - | if !w.mapped || !w.xdg.resource.Resource.Alive() { | 83 | + | if !w.mapped || !w.alive() { |
| 84 | return false | 84 | return false | ||
| 85 | } | 85 | } | ||
| 86 | sent := false | 86 | sent := false | ||
| 87 | usec := uint64(c.Time / time.Microsecond) | 87 | usec := uint64(c.Time / time.Microsecond) | ||
| 88 | - | for _, rel := range s.relatives[w.xdg.resource.Client()] { | 88 | + | for _, rel := range s.relatives[w.client()] { |
| 89 | if rel.Resource.Alive() { | 89 | if rel.Resource.Alive() { | ||
| 90 | rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY)) | 90 | rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY)) | ||
| 91 | sent = true | 91 | sent = true | ||
| @@ -272,7 +272,7 @@ func (s *Server) updateConstraint() { | |||||
| 272 | } | 272 | } | ||
| 273 | var want *constraint | 273 | var want *constraint | ||
| 274 | if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused { | 274 | if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused { | ||
| 275 | - | if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) { | 275 | + | if c := s.constraints[w.surf()]; c != nil && !c.defunct && (c.active || c.contains(w)) { |
| 276 | want = c | 276 | want = c | ||
| 277 | } | 277 | } | ||
| 278 | } | 278 | } | ||
| @@ -298,8 +298,8 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 298 | if s.protected() { | 298 | if s.protected() { | ||
| 299 | return | 299 | return | ||
| 300 | } | 300 | } | ||
| 301 | - | x := c.surface.xdg | 301 | + | w := c.surface.toplevelWindow() |
| 302 | - | if x == nil || x.window == nil { | 302 | + | if w == nil { |
| 303 | return | 303 | return | ||
| 304 | } | 304 | } | ||
| 305 | mode := ports.ConstraintConfine | 305 | mode := ports.ConstraintConfine | ||
| @@ -308,8 +308,9 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 308 | } | 308 | } | ||
| 309 | r := c.region | 309 | r := c.region | ||
| 310 | if r.W > 0 && r.H > 0 { | 310 | if r.W > 0 && r.H > 0 { | ||
| 311 | - | r.X -= x.geometry.X | 311 | + | g := w.geometry() |
| 312 | - | r.Y -= x.geometry.Y | 312 | + | r.X -= g.X |
| 313 | + | r.Y -= g.Y | |||
| 313 | } | 314 | } | ||
| 314 | // Core supports one confinement rect, so this bounds the intersection; | 315 | // Core supports one confinement rect, so this bounds the intersection; | ||
| 315 | // contains uses the exact input region to decide activation. | 316 | // contains uses the exact input region to decide activation. | ||
| @@ -333,17 +334,17 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 333 | r = box | 334 | r = box | ||
| 334 | } | 335 | } | ||
| 335 | } | 336 | } | ||
| 336 | - | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | 337 | + | s.emit(ports.PointerConstrained{ID: w.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) |
| 337 | } | 338 | } | ||
| 338 | 339 | ||||
| 339 | // contains reports whether the pointer is in the region: a constraint | 340 | // contains reports whether the pointer is in the region: a constraint | ||
| 340 | // activates only there, so it never warps the pointer. | 341 | // activates only there, so it never warps the pointer. | ||
| 341 | func (c *constraint) contains(w *window) bool { | 342 | func (c *constraint) contains(w *window) bool { | ||
| 342 | r := c.region | 343 | r := c.region | ||
| 343 | - | x, y := w.surfacePoint(w.xdg.server.seat.pointerX, w.xdg.server.seat.pointerY) | 344 | + | x, y := w.surfacePoint(c.surface.server.seat.pointerX, c.surface.server.seat.pointerY) |
| 344 | all, rects := c.surface.effectiveInput() | 345 | all, rects := c.surface.effectiveInput() | ||
| 345 | if !all { | 346 | if !all { | ||
| 346 | - | localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y) | 347 | + | localX, localY := x-float64(w.geometry().X), y-float64(w.geometry().Y) |
| 347 | inside := false | 348 | inside := false | ||
| 348 | for _, input := range rects { | 349 | for _, input := range rects { | ||
| 349 | if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) { | 350 | if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) { | ||
| @@ -363,5 +364,5 @@ func (c *constraint) contains(w *window) bool { | |||||
| 363 | 364 | ||||
| 364 | // locked reports whether the window's pointer is locked. | 365 | // locked reports whether the window's pointer is locked. | ||
| 365 | func (s *Server) locked(w *window) bool { | 366 | func (s *Server) locked(w *window) bool { | ||
| 366 | - | return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface | 367 | + | return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.surf() |
| 367 | } | 368 | } | ||
M internal/adapters/wayland/decoration.go +1 -1
| @@ -80,7 +80,7 @@ func (d *decoration) UnsetMode(r *xdgdecoration.ZxdgToplevelDecorationV1) | |||||
| 80 | func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) { | 80 | func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) { | ||
| 81 | r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide)) | 81 | r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide)) | ||
| 82 | for _, w := range d.s.windows { | 82 | for _, w := range d.s.windows { | ||
| 83 | - | if w.toplevel == d.toplevel && w.hasLast && w.toplevel.Resource.Alive() && w.xdg.resource.Resource.Alive() { | 83 | + | if w.toplevel == d.toplevel && w.hasLast && w.toplevelAlive() && w.alive() { |
| 84 | w.sendConfigure() | 84 | w.sendConfigure() | ||
| 85 | return | 85 | return | ||
| 86 | } | 86 | } | ||
M internal/adapters/wayland/dialog.go +1 -1
| @@ -77,6 +77,6 @@ func (w *window) setModal(modal bool) { | |||||
| 77 | } | 77 | } | ||
| 78 | w.modal = modal | 78 | w.modal = modal | ||
| 79 | if w.mapped { | 79 | if w.mapped { | ||
| 80 | - | w.xdg.server.emit(ports.WindowModal{ID: w.id, Modal: modal}) | 80 | + | w.srv().emit(ports.WindowModal{ID: w.id, Modal: modal}) |
| 81 | } | 81 | } | ||
| 82 | } | 82 | } | ||
M internal/adapters/wayland/dmabuf.go +3 -2
| @@ -223,10 +223,11 @@ func (g *dmabufGlobal) fullscreenOutput(surf *surface) (string, bool) { | |||||
| 223 | return "", false | 223 | return "", false | ||
| 224 | } | 224 | } | ||
| 225 | root := toplevelRoot(surf) | 225 | root := toplevelRoot(surf) | ||
| 226 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen || g.server.invisible(surf) { | 226 | + | w := root.toplevelWindow() |
| 227 | + | if w == nil || !w.hasLast || !w.last.Fullscreen || g.server.invisible(surf) { | |||
| 227 | return "", false | 228 | return "", false | ||
| 228 | } | 229 | } | ||
| 229 | - | return root.xdg.window.last.Output, true | 230 | + | return w.last.Output, true |
| 230 | } | 231 | } | ||
| 231 | 232 | ||||
| 232 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | 233 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | ||
M internal/adapters/wayland/extforeigntoplevel.go +1 -1
| @@ -72,7 +72,7 @@ func (*extToplevelList) Destroy(*ext.ExtForeignToplevelListV1) {} | |||||
| 72 | func (s *Server) mappedToplevels() []ports.WindowID { | 72 | func (s *Server) mappedToplevels() []ports.WindowID { | ||
| 73 | ids := make([]ports.WindowID, 0, len(s.windows)) | 73 | ids := make([]ports.WindowID, 0, len(s.windows)) | ||
| 74 | for id, w := range s.windows { | 74 | for id, w := range s.windows { | ||
| 75 | - | if w.toplevel != nil && w.mapped { | 75 | + | if w.isToplevel() && w.mapped { |
| 76 | ids = append(ids, id) | 76 | ids = append(ids, id) | ||
| 77 | } | 77 | } | ||
| 78 | } | 78 | } | ||
M internal/adapters/wayland/foreigntoplevel.go +6 -6
| @@ -44,7 +44,7 @@ func registerForeignToplevel(d *server.Display, s *Server) error { | |||||
| 44 | r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) } | 44 | r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) } | ||
| 45 | ids := make([]ports.WindowID, 0, len(s.windows)) | 45 | ids := make([]ports.WindowID, 0, len(s.windows)) | ||
| 46 | for id, w := range s.windows { | 46 | for id, w := range s.windows { | ||
| 47 | - | if w.toplevel != nil && w.mapped { | 47 | + | if w.isToplevel() && w.mapped { |
| 48 | ids = append(ids, id) | 48 | ids = append(ids, id) | ||
| 49 | } | 49 | } | ||
| 50 | } | 50 | } | ||
| @@ -186,7 +186,7 @@ func toplevelState(w *window, version int32) []byte { | |||||
| 186 | 186 | ||||
| 187 | // toplevelChanged updates every manager after w mapped or changed. | 187 | // toplevelChanged updates every manager after w mapped or changed. | ||
| 188 | func (s *Server) toplevelChanged(w *window) { | 188 | func (s *Server) toplevelChanged(w *window) { | ||
| 189 | - | if w.toplevel == nil || !w.mapped { | 189 | + | if !w.isToplevel() || !w.mapped { |
| 190 | return | 190 | return | ||
| 191 | } | 191 | } | ||
| 192 | for _, m := range s.toplevelManagers { | 192 | for _, m := range s.toplevelManagers { | ||
| @@ -220,7 +220,7 @@ func (s *Server) refreshToplevels() { | |||||
| 220 | for _, l := range s.extToplevelLists { | 220 | for _, l := range s.extToplevelLists { | ||
| 221 | for id, h := range l.handles { | 221 | for id, h := range l.handles { | ||
| 222 | w := s.windows[id] | 222 | w := s.windows[id] | ||
| 223 | - | if h.closed || w == nil || w.toplevel == nil || !w.mapped { | 223 | + | if h.closed || w == nil || !w.isToplevel() || !w.mapped { |
| 224 | l.closed(id) | 224 | l.closed(id) | ||
| 225 | } | 225 | } | ||
| 226 | } | 226 | } | ||
| @@ -231,7 +231,7 @@ func (s *Server) refreshToplevels() { | |||||
| 231 | for _, m := range s.toplevelManagers { | 231 | for _, m := range s.toplevelManagers { | ||
| 232 | for id, h := range m.handles { | 232 | for id, h := range m.handles { | ||
| 233 | w := s.windows[id] | 233 | w := s.windows[id] | ||
| 234 | - | if h.req.closed || w == nil || w.toplevel == nil || !w.mapped { | 234 | + | if h.req.closed || w == nil || !w.isToplevel() || !w.mapped { |
| 235 | m.closed(id) | 235 | m.closed(id) | ||
| 236 | } | 236 | } | ||
| 237 | } | 237 | } | ||
| @@ -253,7 +253,7 @@ func (t *toplevelRequests) window() *window { | |||||
| 253 | if t.s.protected() || t.closed { | 253 | if t.s.protected() || t.closed { | ||
| 254 | return nil | 254 | return nil | ||
| 255 | } | 255 | } | ||
| 256 | - | if w := t.s.windows[t.id]; w != nil && w.toplevel != nil && w.mapped && w.toplevel.Resource.Alive() { | 256 | + | if w := t.s.windows[t.id]; w != nil && w.toplevelAlive() && w.mapped { |
| 257 | return w | 257 | return w | ||
| 258 | } | 258 | } | ||
| 259 | return nil | 259 | return nil | ||
| @@ -274,7 +274,7 @@ func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.S | |||||
| 274 | } | 274 | } | ||
| 275 | func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) { | 275 | func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) { | ||
| 276 | if w := t.window(); w != nil { | 276 | if w := t.window(); w != nil { | ||
| 277 | - | w.toplevel.SendClose() | 277 | + | w.sendClose() |
| 278 | } | 278 | } | ||
| 279 | } | 279 | } | ||
| 280 | func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) { | 280 | func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) { | ||
M internal/adapters/wayland/frames.go +10 -6
| @@ -183,7 +183,7 @@ func (s *Server) pacePass(flipped map[string]bool, reports []ports.OutputPresent | |||||
| 183 | func toplevelRoot(surf *surface) *surface { | 183 | func toplevelRoot(surf *surface) *surface { | ||
| 184 | surf = surf.root() | 184 | surf = surf.root() | ||
| 185 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | 185 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | ||
| 186 | - | surf = surf.xdg.window.popup.parent.xdg.surface.root() | 186 | + | surf = surf.xdg.window.popup.parent.surf().root() |
| 187 | } | 187 | } | ||
| 188 | return surf | 188 | return surf | ||
| 189 | } | 189 | } | ||
| @@ -193,7 +193,8 @@ func toplevelRoot(surf *surface) *surface { | |||||
| 193 | // for a capture session is invisible. | 193 | // for a capture session is invisible. | ||
| 194 | func (s *Server) invisible(surf *surface) bool { | 194 | func (s *Server) invisible(surf *surface) bool { | ||
| 195 | root := toplevelRoot(surf) | 195 | root := toplevelRoot(surf) | ||
| 196 | - | return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible | 196 | + | w := root.toplevelWindow() |
| 197 | + | return w != nil && w.hasLast && !w.last.Visible | |||
| 197 | } | 198 | } | ||
| 198 | 199 | ||||
| 199 | // suspended reports that nothing draws the toplevel tree, not even a capture | 200 | // suspended reports that nothing draws the toplevel tree, not even a capture | ||
| @@ -201,10 +202,11 @@ func (s *Server) invisible(surf *surface) bool { | |||||
| 201 | // frame callbacks of its output so the capture keeps moving. | 202 | // frame callbacks of its output so the capture keeps moving. | ||
| 202 | func (s *Server) suspended(surf *surface) bool { | 203 | func (s *Server) suspended(surf *surface) bool { | ||
| 203 | root := toplevelRoot(surf) | 204 | root := toplevelRoot(surf) | ||
| 204 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast { | 205 | + | w := root.toplevelWindow() |
| 206 | + | if w == nil || !w.hasLast { | |||
| 205 | return false | 207 | return false | ||
| 206 | } | 208 | } | ||
| 207 | - | return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured | 209 | + | return !w.last.Visible && !w.last.Captured |
| 208 | } | 210 | } | ||
| 209 | 211 | ||||
| 210 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | 212 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | ||
| @@ -212,11 +214,13 @@ func (s *Server) suspended(surf *surface) bool { | |||||
| 212 | func (s *Server) frameOutput(surf *surface) string { | 214 | func (s *Server) frameOutput(surf *surface) string { | ||
| 213 | surf = toplevelRoot(surf) | 215 | surf = toplevelRoot(surf) | ||
| 214 | switch { | 216 | switch { | ||
| 215 | - | case surf.xdg != nil && surf.xdg.window != nil: | 217 | + | case surf.x11win != nil && surf.x11win.isOverride(): |
| 218 | + | return s.overrideOutput(surf.x11win) | |||
| 219 | + | case surf.toplevelWindow() != nil: | |||
| 216 | if s.suspended(surf) { | 220 | if s.suspended(surf) { | ||
| 217 | return suspendedFrameQueue | 221 | return suspendedFrameQueue | ||
| 218 | } | 222 | } | ||
| 219 | - | return surf.xdg.window.last.Output | 223 | + | return surf.toplevelWindow().last.Output |
| 220 | case surf.layer != nil && surf.layer.output != nil: | 224 | case surf.layer != nil && surf.layer.output != nil: | ||
| 221 | return surf.layer.output.name() | 225 | return surf.layer.output.name() | ||
| 222 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | 226 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | ||
M internal/adapters/wayland/globals.go +10 -1
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | func registerGlobals(d *server.Display, o Options, s *Server) error { | 13 | func registerGlobals(d *server.Display, o Options, s *Server) error { | ||
| 14 | for _, register := range []func() error{ | 14 | for _, register := range []func() error{ | ||
| 15 | func() error { return registerXDG(d, s) }, | 15 | func() error { return registerXDG(d, s) }, | ||
| 16 | + | func() error { return registerXwaylandShell(d, s) }, | |||
| 16 | func() error { return registerLayer(d, s) }, | 17 | func() error { return registerLayer(d, s) }, | ||
| 17 | func() error { | 18 | func() error { | ||
| 18 | // Advertise only with the production owner gate and authoritative | 19 | // Advertise only with the production owner gate and authoritative | ||
| @@ -131,8 +132,16 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |||||
| 131 | state.identity = c.server.nextSurface | 132 | state.identity = c.server.nextSurface | ||
| 132 | state.wl = w | 133 | state.wl = w | ||
| 133 | c.server.surfaces[w.Resource] = state | 134 | c.server.surfaces[w.Resource] = state | ||
| 135 | + | c.server.surfaceByIdentity[state.identity] = state | |||
| 134 | state.sendScale() | 136 | state.sendScale() | ||
| 135 | - | w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) } | 137 | + | w.OnDestroy = func() { |
| 138 | + | delete(c.server.surfaces, w.Resource) | |||
| 139 | + | delete(c.server.surfaceByIdentity, state.identity) | |||
| 140 | + | state.Destroy(w) | |||
| 141 | + | if state.kind == roleX11 && c.server.xwm != nil { | |||
| 142 | + | c.server.xwm.SurfaceGone(state.identity) | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 136 | } | 145 | } | ||
| 137 | } | 146 | } | ||
| 138 | func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) { | 147 | func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) { | ||
M internal/adapters/wayland/imagecopy.go +1 -1
| @@ -351,7 +351,7 @@ func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok | |||||
| 351 | case srcWindow: | 351 | case srcWindow: | ||
| 352 | // Core's state gives the size; before it, the last configure. | 352 | // Core's state gives the size; before it, the last configure. | ||
| 353 | w := c.s.windows[src.window] | 353 | w := c.s.windows[src.window] | ||
| 354 | - | if w == nil || !w.mapped || w.toplevel == nil || w.gen != src.gen { | 354 | + | if w == nil || !w.mapped || !w.isToplevel() || w.gen != src.gen { |
| 355 | return nil, image.Rectangle{}, false, false | 355 | return nil, image.Rectangle{}, false, false | ||
| 356 | } | 356 | } | ||
| 357 | name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height} | 357 | name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height} | ||
M internal/adapters/wayland/outputs.go +72 -1
| @@ -42,6 +42,8 @@ type output struct { | |||||
| 42 | global *server.Global | 42 | global *server.Global | ||
| 43 | resources []*wayland.Output | 43 | resources []*wayland.Output | ||
| 44 | xdgOutputs []*xdgoutput.ZxdgOutputV1 | 44 | xdgOutputs []*xdgoutput.ZxdgOutputV1 | ||
| 45 | + | // xdgOutputsX are the private xdg-outputs of the Xwayland client (X space). | |||
| 46 | + | xdgOutputsX []*xdgoutput.ZxdgOutputV1 | |||
| 45 | } | 47 | } | ||
| 46 | 48 | ||||
| 47 | func (o *output) name() string { return o.place.Info.Name } | 49 | func (o *output) name() string { return o.place.Info.Name } | ||
| @@ -89,6 +91,36 @@ func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) { | |||||
| 89 | } | 91 | } | ||
| 90 | } | 92 | } | ||
| 91 | 93 | ||||
| 94 | + | // sendXDGX sends the private xdg-output state: the layout position moved to | |||
| 95 | + | // X space and the logical size. | |||
| 96 | + | func (o *output) sendXDGX(s *Server, x *xdgoutput.ZxdgOutputV1, withName bool) { | |||
| 97 | + | ox, oy := s.xspace.Origin() | |||
| 98 | + | x.SendLogicalPosition(int32(o.place.X-ox), int32(o.place.Y-oy)) | |||
| 99 | + | x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height)) | |||
| 100 | + | if withName && x.Version() >= 2 { | |||
| 101 | + | x.SendName(o.name()) | |||
| 102 | + | x.SendDescription(o.description()) | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // republishXDGX resends every private xdg-output after a layout change, so | |||
| 107 | + | // Xwayland resizes the root and its RandR outputs. | |||
| 108 | + | func (s *Server) republishXDGX() { | |||
| 109 | + | for _, o := range s.outputs { | |||
| 110 | + | for _, x := range o.xdgOutputsX { | |||
| 111 | + | o.sendXDGX(s, x, false) | |||
| 112 | + | if x.Version() < 3 { | |||
| 113 | + | x.SendDone() | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | for _, r := range o.resources { | |||
| 117 | + | if r.Client() == s.xwayland.client && r.Version() >= 2 { | |||
| 118 | + | r.SendDone() | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | ||||
| 92 | // outputOf returns the output a wl_output resource belongs to, or nil for a | 124 | // outputOf returns the output a wl_output resource belongs to, or nil for a | ||
| 93 | // removed one. | 125 | // removed one. | ||
| 94 | func (s *Server) outputOf(r *wayland.Output) *output { | 126 | func (s *Server) outputOf(r *wayland.Output) *output { | ||
| @@ -162,6 +194,10 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||||
| 162 | changed := !slices.Equal(s.outputPlaces, c.Outputs) | 194 | changed := !slices.Equal(s.outputPlaces, c.Outputs) | ||
| 163 | focusChanged := s.focusedOutput != c.Focused | 195 | focusChanged := s.focusedOutput != c.Focused | ||
| 164 | s.outputPlaces = append(ports.Layout(nil), c.Outputs...) | 196 | s.outputPlaces = append(ports.Layout(nil), c.Outputs...) | ||
| 197 | + | xChanged := s.xspace.Set(s.outputPlaces) | |||
| 198 | + | if s.xwm != nil { | |||
| 199 | + | s.xwm.LayoutChanged(xChanged) | |||
| 200 | + | } | |||
| 165 | defer func() { | 201 | defer func() { | ||
| 166 | if changed { | 202 | if changed { | ||
| 167 | s.refreshOutputManagers() | 203 | s.refreshOutputManagers() | ||
| @@ -244,6 +280,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||||
| 244 | for _, surf := range s.surfaces { | 280 | for _, surf := range s.surfaces { | ||
| 245 | surf.sendScale() | 281 | surf.sendScale() | ||
| 246 | } | 282 | } | ||
| 283 | + | if changed { | |||
| 284 | + | s.republishXDGX() | |||
| 285 | + | } | |||
| 247 | s.notifyColorFeedback() | 286 | s.notifyColorFeedback() | ||
| 248 | // Layer surfaces sized from the output follow its new logical size. | 287 | // Layer surfaces sized from the output follow its new logical size. | ||
| 249 | adopted := false | 288 | adopted := false | ||
| @@ -279,10 +318,42 @@ type outputHandler struct{} | |||||
| 279 | 318 | ||||
| 280 | func (outputHandler) Release(*wayland.Output) {} | 319 | func (outputHandler) Release(*wayland.Output) {} | ||
| 281 | 320 | ||||
| 321 | + | // registerXDGOutput registers the public zxdg_output_manager_v1 and a second, | |||
| 322 | + | // private one for Xwayland whose outputs are in X space (see globalVisible). | |||
| 282 | func registerXDGOutput(d *server.Display, s *Server) error { | 323 | func registerXDGOutput(d *server.Display, s *Server) error { | ||
| 283 | - | return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) { | 324 | + | var err error |
| 325 | + | s.xdgPublic, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |||
| 284 | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s}) | 326 | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s}) | ||
| 285 | }) | 327 | }) | ||
| 328 | + | if err != nil { | |||
| 329 | + | return err | |||
| 330 | + | } | |||
| 331 | + | s.xdgPrivate, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |||
| 332 | + | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManagerX{s}) | |||
| 333 | + | }) | |||
| 334 | + | return err | |||
| 335 | + | } | |||
| 336 | + | ||||
| 337 | + | type xdgOutputManagerX struct{ server *Server } | |||
| 338 | + | ||||
| 339 | + | func (xdgOutputManagerX) Destroy(*xdgoutput.ZxdgOutputManagerV1) {} | |||
| 340 | + | func (m xdgOutputManagerX) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) { | |||
| 341 | + | x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{}) | |||
| 342 | + | if err != nil { | |||
| 343 | + | return | |||
| 344 | + | } | |||
| 345 | + | o := m.server.outputOf(wl) | |||
| 346 | + | if o == nil { | |||
| 347 | + | return | |||
| 348 | + | } | |||
| 349 | + | o.xdgOutputsX = append(o.xdgOutputsX, x) | |||
| 350 | + | x.OnDestroy = func() { o.xdgOutputsX = removeItem(o.xdgOutputsX, x) } | |||
| 351 | + | o.sendXDGX(m.server, x, true) | |||
| 352 | + | if r.Version() < 3 || wl.Version() < 2 { | |||
| 353 | + | x.SendDone() | |||
| 354 | + | } else { | |||
| 355 | + | wl.SendDone() | |||
| 356 | + | } | |||
| 286 | } | 357 | } | ||
| 287 | 358 | ||||
| 288 | type xdgOutputManager struct{ server *Server } | 359 | type xdgOutputManager struct{ server *Server } | ||
M internal/adapters/wayland/popup.go +2 -2
| @@ -31,7 +31,7 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||||
| 31 | var parent *window | 31 | var parent *window | ||
| 32 | if parentXDG != nil { | 32 | if parentXDG != nil { | ||
| 33 | for _, w := range s.windows { | 33 | for _, w := range s.windows { | ||
| 34 | - | if w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource { | 34 | + | if w.xdg != nil && w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource { |
| 35 | parent = w | 35 | parent = w | ||
| 36 | } | 36 | } | ||
| 37 | } | 37 | } | ||
| @@ -166,7 +166,7 @@ func (p *popup) configure(c ports.ConfigurePopup) { | |||||
| 166 | func (s *Server) topGrab(client server.Client, parent *window) bool { | 166 | func (s *Server) topGrab(client server.Client, parent *window) bool { | ||
| 167 | var top *window | 167 | var top *window | ||
| 168 | for _, w := range s.windows { | 168 | for _, w := range s.windows { | ||
| 169 | - | if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) { | 169 | + | if w.popup != nil && w.popup.grab && !w.popup.done && w.client() == client && (top == nil || w.id > top.id) { |
| 170 | top = w | 170 | top = w | ||
| 171 | } | 171 | } | ||
| 172 | } | 172 | } | ||
M internal/adapters/wayland/scale.go +5 -3
| @@ -21,12 +21,14 @@ func (s *Server) outputOfSurface(surf *surface) *output { | |||||
| 21 | surf = surf.root() | 21 | surf = surf.root() | ||
| 22 | // Popups are on their parent's output. | 22 | // Popups are on their parent's output. | ||
| 23 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | 23 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | ||
| 24 | - | surf = surf.xdg.window.popup.parent.xdg.surface.root() | 24 | + | surf = surf.xdg.window.popup.parent.surf().root() |
| 25 | } | 25 | } | ||
| 26 | name := "" | 26 | name := "" | ||
| 27 | switch { | 27 | switch { | ||
| 28 | - | case surf.xdg != nil && surf.xdg.window != nil: | 28 | + | case surf.x11win != nil && surf.x11win.isOverride(): |
| 29 | - | name = surf.xdg.window.last.Output | 29 | + | name = s.overrideOutput(surf.x11win) |
| 30 | + | case surf.toplevelWindow() != nil: | |||
| 31 | + | name = surf.toplevelWindow().last.Output | |||
| 30 | case surf.layer != nil && surf.layer.output != nil: | 32 | case surf.layer != nil && surf.layer.output != nil: | ||
| 31 | return surf.layer.output | 33 | return surf.layer.output | ||
| 32 | case surf.lock != nil: | 34 | case surf.lock != nil: | ||
M internal/adapters/wayland/screencast.go +1 -1
| @@ -146,7 +146,7 @@ func (s *Server) openCast(cmd ports.CastCommand) { | |||||
| 146 | gone := ports.CaptureReasonOutputGone | 146 | gone := ports.CaptureReasonOutputGone | ||
| 147 | if cmd.Window != 0 { | 147 | if cmd.Window != 0 { | ||
| 148 | gone = ports.CaptureReasonWindowGone | 148 | gone = ports.CaptureReasonWindowGone | ||
| 149 | - | if w := s.windows[cmd.Window]; w != nil && w.mapped && w.toplevel != nil { | 149 | + | if w := s.windows[cmd.Window]; w != nil && w.mapped && w.isToplevel() { |
| 150 | src = &captureSource{kind: srcWindow, window: cmd.Window, gen: w.gen} | 150 | src = &captureSource{kind: srcWindow, window: cmd.Window, gen: w.gen} | ||
| 151 | open.Window = cmd.Window | 151 | open.Window = cmd.Window | ||
| 152 | } | 152 | } | ||
M internal/adapters/wayland/seat.go +10 -9
| @@ -276,10 +276,10 @@ func (s *Server) setModifiers(c ports.SetModifiers) { | |||||
| 276 | } | 276 | } | ||
| 277 | 277 | ||||
| 278 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | 278 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | ||
| 279 | - | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | 279 | + | if w == nil || !w.mapped || !w.alive() { |
| 280 | return nil | 280 | return nil | ||
| 281 | } | 281 | } | ||
| 282 | - | return s.clientKeyboards(w.xdg.resource.Client()) | 282 | + | return s.clientKeyboards(w.client()) |
| 283 | } | 283 | } | ||
| 284 | 284 | ||||
| 285 | // clientKeyboards returns c's wl_keyboards without allocating. The list holds | 285 | // clientKeyboards returns c's wl_keyboards without allocating. The list holds | ||
| @@ -300,8 +300,8 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke | |||||
| 300 | // already dead. Admission is enforced before entering or delivering. | 300 | // already dead. Admission is enforced before entering or delivering. | ||
| 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | ||
| 302 | } | 302 | } | ||
| 303 | - | if w := s.windows[id]; w != nil && w.mapped { | 303 | + | if w := s.windows[id]; w != nil && w.mapped && !w.isOverride() { |
| 304 | - | if surf := w.xdg.surfaceResource(); surf != nil { | 304 | + | if surf := w.surfaceResource(); surf != nil { |
| 305 | return surf, s.windowKeyboards(w) | 305 | return surf, s.windowKeyboards(w) | ||
| 306 | } | 306 | } | ||
| 307 | } | 307 | } | ||
| @@ -319,8 +319,8 @@ func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Poi | |||||
| 319 | if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) { | 319 | if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) { | ||
| 320 | return l.resource.Client(), s.clientPointers(l.resource.Client()), true | 320 | return l.resource.Client(), s.clientPointers(l.resource.Client()), true | ||
| 321 | } | 321 | } | ||
| 322 | - | if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() { | 322 | + | if w := s.windows[id]; w != nil && w.mapped && w.alive() { |
| 323 | - | return w.xdg.resource.Client(), s.windowPointers(w), true | 323 | + | return w.client(), s.windowPointers(w), true |
| 324 | } | 324 | } | ||
| 325 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | 325 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | ||
| 326 | return l.resource.Client(), s.layerPointers(l), true | 326 | return l.resource.Client(), s.layerPointers(l), true | ||
| @@ -335,7 +335,7 @@ func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surfa | |||||
| 335 | return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y | 335 | return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y | ||
| 336 | } | 336 | } | ||
| 337 | if w := s.windows[id]; w != nil { | 337 | if w := s.windows[id]; w != nil { | ||
| 338 | - | if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() { | 338 | + | if surf := w.surfaceResource(); surf != nil && surf.Resource.Alive() { |
| 339 | x, y := w.surfacePoint(x, y) | 339 | x, y := w.surfacePoint(x, y) | ||
| 340 | return surf, s.windowPointers(w), x, y | 340 | return surf, s.windowPointers(w), x, y | ||
| 341 | } | 341 | } | ||
| @@ -455,6 +455,7 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||||
| 455 | } | 455 | } | ||
| 456 | } | 456 | } | ||
| 457 | s.textInputFocus() | 457 | s.textInputFocus() | ||
| 458 | + | s.x11Focus() | |||
| 458 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | 459 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | ||
| 459 | s.updateConstraint() | 460 | s.updateConstraint() | ||
| 460 | } | 461 | } | ||
| @@ -543,10 +544,10 @@ func pointerFrame(p *wayland.Pointer) { | |||||
| 543 | } | 544 | } | ||
| 544 | } | 545 | } | ||
| 545 | func (s *Server) windowPointers(w *window) []*wayland.Pointer { | 546 | func (s *Server) windowPointers(w *window) []*wayland.Pointer { | ||
| 546 | - | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | 547 | + | if w == nil || !w.mapped || !w.alive() { |
| 547 | return nil | 548 | return nil | ||
| 548 | } | 549 | } | ||
| 549 | - | return s.clientPointers(w.xdg.resource.Client()) | 550 | + | return s.clientPointers(w.client()) |
| 550 | } | 551 | } | ||
| 551 | 552 | ||||
| 552 | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | 553 | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | ||
M internal/adapters/wayland/server.go +40 -16
| @@ -16,6 +16,7 @@ import ( | |||||
| 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | ||
| 17 | "github.com/bnema/neferwl/internal/adapters/clock" | 17 | "github.com/bnema/neferwl/internal/adapters/clock" | ||
| 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | ||
| 19 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 19 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | 20 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | ||
| 20 | "github.com/bnema/neferwl/internal/ports" | 21 | "github.com/bnema/neferwl/internal/ports" | ||
| 21 | "github.com/bnema/purego-libwayland/server" | 22 | "github.com/bnema/purego-libwayland/server" | ||
| @@ -160,13 +161,21 @@ type Server struct { | |||||
| 160 | securityStop int | 161 | securityStop int | ||
| 161 | securityWG sync.WaitGroup | 162 | securityWG sync.WaitGroup | ||
| 162 | securityLive atomic.Int32 | 163 | securityLive atomic.Int32 | ||
| 163 | - | display *server.Display | 164 | + | xwayland xwaylandClient // display owner |
| 164 | - | env procEnv | 165 | + | // xdgPublic and xdgPrivate are the two zxdg_output_manager_v1 globals: |
| 165 | - | name string | 166 | + | // layout space for clients, X space for Xwayland. |
| 166 | - | cleanup func() | 167 | + | xdgPublic, xdgPrivate *server.Global |
| 167 | - | log zerowrap.Logger | 168 | + | // xwm manages the X11 windows of the Xwayland client; x11Slots maps its |
| 168 | - | logDamage bool | 169 | + | // slots to their windows. Display goroutine only. |
| 169 | - | channels Channels | 170 | + | xwm *xwm.XWM |
| 171 | + | x11Slots map[xwm.Slot]*window | |||
| 172 | + | display *server.Display | |||
| 173 | + | env procEnv | |||
| 174 | + | name string | |||
| 175 | + | cleanup func() | |||
| 176 | + | log zerowrap.Logger | |||
| 177 | + | logDamage bool | |||
| 178 | + | channels Channels | |||
| 170 | // awaiting holds frame callbacks by output name, due at its next | 179 | // awaiting holds frame callbacks by output name, due at its next | ||
| 171 | // frame (frameDue, or its page flip). | 180 | // frame (frameDue, or its page flip). | ||
| 172 | awaiting map[string][]*wayland.Callback | 181 | awaiting map[string][]*wayland.Callback | ||
| @@ -214,8 +223,11 @@ type Server struct { | |||||
| 214 | // foreignExports are the live xdg-foreign handles (foreign.go). | 223 | // foreignExports are the live xdg-foreign handles (foreign.go). | ||
| 215 | foreignExports map[string]*foreignExport | 224 | foreignExports map[string]*foreignExport | ||
| 216 | surfaces map[*server.Resource]*surface | 225 | surfaces map[*server.Resource]*surface | ||
| 217 | - | buffers map[*server.Resource]clientBuffer | 226 | + | // surfaceByIdentity finds a surface by its identity (xwayland_shell_v1 |
| 218 | - | dmabuf *dmabufGlobal | 227 | + | // pairing); kept with surfaces. |
| 228 | + | surfaceByIdentity map[uint64]*surface | |||
| 229 | + | buffers map[*server.Resource]clientBuffer | |||
| 230 | + | dmabuf *dmabufGlobal | |||
| 219 | // hdrOutputs records confirmed DRM modesets; absent outputs are SDR. | 231 | // hdrOutputs records confirmed DRM modesets; absent outputs are SDR. | ||
| 220 | hdrOutputs map[string]ports.OutputHDR | 232 | hdrOutputs map[string]ports.OutputHDR | ||
| 221 | colorID uint64 | 233 | colorID uint64 | ||
| @@ -281,6 +293,7 @@ type Server struct { | |||||
| 281 | workspaceIDs *workspaceid.IDs | 293 | workspaceIDs *workspaceid.IDs | ||
| 282 | outputHeads ports.OutputHeads | 294 | outputHeads ports.OutputHeads | ||
| 283 | outputPlaces ports.Layout | 295 | outputPlaces ports.Layout | ||
| 296 | + | xspace xwm.Space // the one layout <-> X mapping, shared with the XWM | |||
| 284 | managementSerial uint32 | 297 | managementSerial uint32 | ||
| 285 | nextOutputApply uint64 | 298 | nextOutputApply uint64 | ||
| 286 | outputReplies map[uint64]*outputConfiguration | 299 | outputReplies map[uint64]*outputConfiguration | ||
| @@ -366,8 +379,10 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 366 | logDamage: opts.LogDamage, | 379 | logDamage: opts.LogDamage, | ||
| 367 | channels: ch, | 380 | channels: ch, | ||
| 368 | surfaces: make(map[*server.Resource]*surface), | 381 | surfaces: make(map[*server.Resource]*surface), | ||
| 382 | + | surfaceByIdentity: make(map[uint64]*surface), | |||
| 369 | buffers: make(map[*server.Resource]clientBuffer), | 383 | buffers: make(map[*server.Resource]clientBuffer), | ||
| 370 | windows: make(map[ports.WindowID]*window), | 384 | windows: make(map[ports.WindowID]*window), | ||
| 385 | + | x11Slots: make(map[xwm.Slot]*window), | |||
| 371 | layers: make(map[ports.WindowID]*layerSurface), | 386 | layers: make(map[ports.WindowID]*layerSurface), | ||
| 372 | nextWindow: 1, | 387 | nextWindow: 1, | ||
| 373 | eventReady: make(chan struct{}, 1), | 388 | eventReady: make(chan struct{}, 1), | ||
| @@ -498,6 +513,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 498 | if err = s.openPaceTimer(); err != nil { | 513 | if err = s.openPaceTimer(); err != nil { | ||
| 499 | return abort(err) | 514 | return abort(err) | ||
| 500 | } | 515 | } | ||
| 516 | + | d.SetGlobalFilter(s.globalVisible) | |||
| 501 | if err = registerGlobals(d, opts, s); err != nil { | 517 | if err = registerGlobals(d, opts, s); err != nil { | ||
| 502 | return abort(err) | 518 | return abort(err) | ||
| 503 | } | 519 | } | ||
| @@ -585,6 +601,12 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 585 | } | 601 | } | ||
| 586 | s.castInventoryChanged() | 602 | s.castInventoryChanged() | ||
| 587 | err := s.display.Run(ctx) | 603 | err := s.display.Run(ctx) | ||
| 604 | + | // The display is gone: only the X connection is left to close. | |||
| 605 | + | if s.xwm != nil { | |||
| 606 | + | s.xwm.CloseConn() | |||
| 607 | + | s.xwm = nil | |||
| 608 | + | } | |||
| 609 | + | s.forgetXwayland() | |||
| 588 | s.stopSecurityContexts() | 610 | s.stopSecurityContexts() | ||
| 589 | wg.Wait() | 611 | wg.Wait() | ||
| 590 | return err | 612 | return err | ||
| @@ -774,9 +796,11 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 774 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | 796 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | ||
| 775 | w.popup.dismiss() | 797 | w.popup.dismiss() | ||
| 776 | } | 798 | } | ||
| 799 | + | case ports.PlaceWindow: | |||
| 800 | + | s.x11Place(c) | |||
| 777 | case ports.ConfigureWindow: | 801 | case ports.ConfigureWindow: | ||
| 778 | w := s.windows[c.ID] | 802 | w := s.windows[c.ID] | ||
| 779 | - | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() { | 803 | + | if w == nil || !w.toplevelAlive() || !w.alive() { |
| 780 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | 804 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | ||
| 781 | return | 805 | return | ||
| 782 | } | 806 | } | ||
| @@ -787,16 +811,16 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 787 | scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible | 811 | scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible | ||
| 788 | w.last, w.hasLast = c, true | 812 | w.last, w.hasLast = c, true | ||
| 789 | if visibilityChanged { | 813 | if visibilityChanged { | ||
| 790 | - | s.relocateCallbacks(w.xdg.surface) | 814 | + | s.relocateCallbacks(w.surf()) |
| 791 | s.dropFeedbacks(time.Now()) | 815 | s.dropFeedbacks(time.Now()) | ||
| 792 | s.wakePacer() // reconsider FIFO barriers at the new period | 816 | s.wakePacer() // reconsider FIFO barriers at the new period | ||
| 793 | } | 817 | } | ||
| 794 | w.sendConfigure() | 818 | w.sendConfigure() | ||
| 795 | s.toplevelChanged(w) | 819 | s.toplevelChanged(w) | ||
| 796 | - | if scanoutChanged && w.xdg.surface != nil { | 820 | + | if scanoutChanged && w.surf() != nil { |
| 797 | - | s.dmabuf.resendSurface(w.xdg.surface) | 821 | + | s.dmabuf.resendSurface(w.surf()) |
| 798 | } | 822 | } | ||
| 799 | - | if surf := w.xdg.surface; surf != nil && c.Output != "" { | 823 | + | if surf := w.surf(); surf != nil && c.Output != "" { |
| 800 | surf.sendTreeScale() | 824 | surf.sendTreeScale() | ||
| 801 | } | 825 | } | ||
| 802 | if scanoutChanged { | 826 | if scanoutChanged { | ||
| @@ -817,10 +841,10 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 817 | s.setShortcutsInhibit(c) | 841 | s.setShortcutsInhibit(c) | ||
| 818 | case ports.CloseWindow: | 842 | case ports.CloseWindow: | ||
| 819 | w := s.windows[c.ID] | 843 | w := s.windows[c.ID] | ||
| 820 | - | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | 844 | + | if w == nil || !w.toplevelAlive() { |
| 821 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | 845 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | ||
| 822 | return | 846 | return | ||
| 823 | } | 847 | } | ||
| 824 | - | w.toplevel.SendClose() | 848 | + | w.sendClose() |
| 825 | } | 849 | } | ||
| 826 | } | 850 | } | ||
M internal/adapters/wayland/surface.go +15 -2
| @@ -28,6 +28,9 @@ const ( | |||||
| 28 | roleSessionLock | 28 | roleSessionLock | ||
| 29 | // roleDragIcon is the icon of a data drag (dnd.go). | 29 | // roleDragIcon is the icon of a data drag (dnd.go). | ||
| 30 | roleDragIcon | 30 | roleDragIcon | ||
| 31 | + | // roleX11 is the surface of an X11 window paired through | |||
| 32 | + | // xwayland_shell_v1. | |||
| 33 | + | roleX11 | |||
| 31 | ) | 34 | ) | ||
| 32 | 35 | ||||
| 33 | type surface struct { | 36 | type surface struct { | ||
| @@ -47,8 +50,12 @@ type surface struct { | |||||
| 47 | formatOpaque bool | 50 | formatOpaque bool | ||
| 48 | kind roleKind | 51 | kind roleKind | ||
| 49 | xdg *xdgSurface | 52 | xdg *xdgSurface | ||
| 50 | - | layer *layerSurface | 53 | + | // x11win is the X11 window the surface is associated with; x11Serial is |
| 51 | - | lock *lockSurface | 54 | + | // the serial of xwayland_surface_v1.set_serial, 0 until set. |
| 55 | + | x11win *window | |||
| 56 | + | x11Serial uint64 | |||
| 57 | + | layer *layerSurface | |||
| 58 | + | lock *lockSurface | |||
| 52 | // bufferCommitted is permanent, including commits later discarded by FIFO. | 59 | // bufferCommitted is permanent, including commits later discarded by FIFO. | ||
| 53 | bufferCommitted bool | 60 | bufferCommitted bool | ||
| 54 | server *Server | 61 | server *Server | ||
| @@ -192,6 +199,9 @@ func (s *surface) windowID() ports.WindowID { | |||||
| 192 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | 199 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | ||
| 193 | return s.xdg.window.id | 200 | return s.xdg.window.id | ||
| 194 | } | 201 | } | ||
| 202 | + | if s.x11win != nil && s.x11win.mapped { | |||
| 203 | + | return s.x11win.id | |||
| 204 | + | } | |||
| 195 | if s.layer != nil && s.layer.mapped { | 205 | if s.layer != nil && s.layer.mapped { | ||
| 196 | return s.layer.id | 206 | return s.layer.id | ||
| 197 | } | 207 | } | ||
| @@ -566,6 +576,9 @@ func (s *surface) applyCommit(u *update) { | |||||
| 566 | if s.xdg != nil && s.xdg.window != nil { | 576 | if s.xdg != nil && s.xdg.window != nil { | ||
| 567 | s.xdg.window.afterCommit() | 577 | s.xdg.window.afterCommit() | ||
| 568 | } | 578 | } | ||
| 579 | + | if s.x11win != nil { | |||
| 580 | + | s.x11win.afterCommit() | |||
| 581 | + | } | |||
| 569 | if s.lock != nil { | 582 | if s.lock != nil { | ||
| 570 | // Role callbacks precede content construction; publish lock placement | 583 | // Role callbacks precede content construction; publish lock placement | ||
| 571 | // only once validated current dimensions are available. | 584 | // only once validated current dimensions are available. | ||
A internal/adapters/wayland/window.go +112 -0
| @@ -0,0 +1,112 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 5 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 6 | + | "github.com/bnema/purego-libwayland/server" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // Role-independent accessors of a window. Code outside xdg.go goes through | |||
| 10 | + | // them instead of dereferencing w.xdg, which is nil for windows of another | |||
| 11 | + | // role. | |||
| 12 | + | ||||
| 13 | + | // isToplevel reports whether w is a toplevel (xdg or X11), not a popup. | |||
| 14 | + | func (w *window) isToplevel() bool { return w.toplevel != nil || (w.x11 != nil && !w.x11.override) } | |||
| 15 | + | ||||
| 16 | + | // srv is the server the window belongs to. | |||
| 17 | + | func (w *window) srv() *Server { | |||
| 18 | + | if w.xdg != nil { | |||
| 19 | + | return w.xdg.server | |||
| 20 | + | } | |||
| 21 | + | return w.x11.server | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // surf is the window's surface, nil when it has none. | |||
| 25 | + | func (w *window) surf() *surface { | |||
| 26 | + | switch { | |||
| 27 | + | case w.xdg != nil: | |||
| 28 | + | return w.xdg.surface | |||
| 29 | + | case w.x11 != nil: | |||
| 30 | + | return w.x11.surface | |||
| 31 | + | } | |||
| 32 | + | return nil | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // alive reports whether the window's role object is alive. | |||
| 36 | + | func (w *window) alive() bool { | |||
| 37 | + | switch { | |||
| 38 | + | case w.xdg != nil: | |||
| 39 | + | return w.xdg.resource.Resource.Alive() | |||
| 40 | + | case w.x11 != nil: | |||
| 41 | + | return w.x11.surface != nil && w.x11.surface.wl.Resource.Alive() | |||
| 42 | + | } | |||
| 43 | + | return false | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // client is the window's client; the zero Client when it has no role object. | |||
| 47 | + | func (w *window) client() server.Client { | |||
| 48 | + | switch { | |||
| 49 | + | case w.xdg != nil: | |||
| 50 | + | return w.xdg.resource.Client() | |||
| 51 | + | case w.x11 != nil && w.x11.surface != nil: | |||
| 52 | + | return w.x11.surface.wl.Client() | |||
| 53 | + | } | |||
| 54 | + | return server.Client{} | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // geometry is the committed window geometry; empty means the whole surface. | |||
| 58 | + | func (w *window) geometry() ports.Rect { | |||
| 59 | + | if w.xdg != nil { | |||
| 60 | + | return w.xdg.geometry | |||
| 61 | + | } | |||
| 62 | + | return ports.Rect{} | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // sendConfigure sends w.last to the client. | |||
| 66 | + | func (w *window) sendConfigure() { | |||
| 67 | + | switch { | |||
| 68 | + | case w.xdg != nil: | |||
| 69 | + | w.sendXDGConfigure() | |||
| 70 | + | case w.x11 != nil: | |||
| 71 | + | w.x11.server.x11Configure(w) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // sendClose asks the client to close the window. | |||
| 76 | + | func (w *window) sendClose() { | |||
| 77 | + | switch { | |||
| 78 | + | case w.toplevel != nil: | |||
| 79 | + | w.toplevel.SendClose() | |||
| 80 | + | case w.x11 != nil: | |||
| 81 | + | w.x11.server.x11Close(w) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // toplevelAlive reports whether the window is a toplevel whose protocol | |||
| 86 | + | // object is alive. | |||
| 87 | + | func (w *window) toplevelAlive() bool { | |||
| 88 | + | if w.x11 != nil { | |||
| 89 | + | return w.alive() | |||
| 90 | + | } | |||
| 91 | + | return w.toplevel != nil && w.toplevel.Resource.Alive() | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // surfaceResource is the window's wl_surface, nil when it has none or it died. | |||
| 95 | + | func (w *window) surfaceResource() *wayland.Surface { | |||
| 96 | + | switch { | |||
| 97 | + | case w.xdg != nil: | |||
| 98 | + | return w.xdg.surfaceResource() | |||
| 99 | + | case w.x11 != nil && w.alive(): | |||
| 100 | + | return w.x11.surface.wl | |||
| 101 | + | } | |||
| 102 | + | return nil | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // toplevelWindow is the toplevel window (xdg or X11) the surface is the role | |||
| 106 | + | // surface of, nil for other roles and popups' parents are not followed. | |||
| 107 | + | func (s *surface) toplevelWindow() *window { | |||
| 108 | + | if s.xdg != nil && s.xdg.window != nil { | |||
| 109 | + | return s.xdg.window | |||
| 110 | + | } | |||
| 111 | + | return s.x11win | |||
| 112 | + | } | |||
M internal/adapters/wayland/wire_client_test.go +3 -0
| @@ -55,6 +55,7 @@ import ( | |||||
| 55 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | 55 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | ||
| 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | ||
| 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | ||
| 58 | + | wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 58 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | 59 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | ||
| 59 | "github.com/bnema/purego-libwayland/server" | 60 | "github.com/bnema/purego-libwayland/server" | ||
| 60 | "github.com/bnema/wlturbo" | 61 | "github.com/bnema/wlturbo" | ||
| @@ -253,6 +254,8 @@ var wireSchemas = map[string]wireSchema{ | |||||
| 253 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | 254 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | ||
| 254 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | 255 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | ||
| 255 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | 256 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | ||
| 257 | + | "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil}, | |||
| 258 | + | "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil}, | |||
| 256 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | 259 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | ||
| 257 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | 260 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | ||
| 258 | } | 261 | } | ||
A internal/adapters/wayland/x11override_test.go +107 -0
| @@ -0,0 +1,107 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | const ovWin = 0x500002 | |||
| 12 | + | ||||
| 13 | + | // overrideWindow maps an override-redirect X window and pairs its surface. | |||
| 14 | + | func (r *x11Rig) overrideWindow(win uint32, x, y int16, serial uint64) uint32 { | |||
| 15 | + | r.t.Helper() | |||
| 16 | + | r.peer.createOverride(win, x, y) | |||
| 17 | + | r.peer.mapNotify(win) | |||
| 18 | + | return r.pair(win, serial) | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func (r *x11Rig) state(f func()) { | |||
| 22 | + | r.t.Helper() | |||
| 23 | + | r.s.display.Do(f) | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func TestOverrideMapsOnFirstCommitAndMoves(t *testing.T) { | |||
| 27 | + | r := newX11Rig(t) | |||
| 28 | + | r.overrideWindow(ovWin, 30, 40, x11Serial) | |||
| 29 | + | ||||
| 30 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |||
| 31 | + | if m.X != 30 || m.Y != 40 || m.Width != 100 || m.Height != 50 || m.NoInput { | |||
| 32 | + | t.Fatalf("mapped %+v", m) | |||
| 33 | + | } | |||
| 34 | + | r.state(func() { | |||
| 35 | + | w := r.s.windows[m.ID] | |||
| 36 | + | if w == nil || !w.isOverride() || w.isToplevel() || !w.mapped { | |||
| 37 | + | t.Errorf("override window %+v", w) | |||
| 38 | + | } | |||
| 39 | + | if surf, _ := r.s.focusTarget(m.ID); surf != nil { | |||
| 40 | + | t.Error("override window is a keyboard focus target") | |||
| 41 | + | } | |||
| 42 | + | }) | |||
| 43 | + | ||||
| 44 | + | r.peer.configureNotify(ovWin, 60, 70, 100, 50) | |||
| 45 | + | if mv := awaitEvent[ports.UnmanagedMoved](t, r.events); mv.ID != m.ID || mv.X != 60 || mv.Y != 70 { | |||
| 46 | + | t.Fatalf("moved %+v", mv) | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | r.peer.unmapNotify(ovWin) | |||
| 50 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |||
| 51 | + | t.Fatalf("unmapped %+v", u) | |||
| 52 | + | } | |||
| 53 | + | r.state(func() { | |||
| 54 | + | if r.s.windows[m.ID] != nil { | |||
| 55 | + | t.Error("override window kept after unmap") | |||
| 56 | + | } | |||
| 57 | + | }) | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func TestOverrideSurfaceDestroyUnmaps(t *testing.T) { | |||
| 61 | + | r := newX11Rig(t) | |||
| 62 | + | surf := r.overrideWindow(ovWin, 0, 0, x11Serial) | |||
| 63 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |||
| 64 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |||
| 65 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 66 | + | t.Fatal(err) | |||
| 67 | + | } | |||
| 68 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |||
| 69 | + | t.Fatalf("unmapped %+v", u) | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func TestOverrideTakesPointerAndOutput(t *testing.T) { | |||
| 74 | + | r := newX11Rig(t) | |||
| 75 | + | r.commands <- ports.SetOutputs{Outputs: ports.Layout{ | |||
| 76 | + | {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 77 | + | {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 78 | + | }, Focused: "A"} | |||
| 79 | + | r.overrideWindow(ovWin, 100, 10, x11Serial) | |||
| 80 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |||
| 81 | + | r.state(func() { | |||
| 82 | + | w := r.s.windows[m.ID] | |||
| 83 | + | if got := r.s.frameOutput(w.x11.surface); got != "A" { | |||
| 84 | + | t.Errorf("frame output %q, want A", got) | |||
| 85 | + | } | |||
| 86 | + | if surf, _, _, _ := r.s.pointerSurface(m.ID, 1, 1); surf == nil { | |||
| 87 | + | t.Error("pointer cannot enter the override surface") | |||
| 88 | + | } | |||
| 89 | + | }) | |||
| 90 | + | r.peer.configureNotify(ovWin, 2000, 10, 100, 50) | |||
| 91 | + | awaitEvent[ports.UnmanagedMoved](t, r.events) | |||
| 92 | + | r.state(func() { | |||
| 93 | + | if got := r.s.frameOutput(r.s.windows[m.ID].x11.surface); got != "B" { | |||
| 94 | + | t.Errorf("frame output %q after move, want B", got) | |||
| 95 | + | } | |||
| 96 | + | }) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func TestOverrideMovedUnchangedAllocations(t *testing.T) { | |||
| 100 | + | s := &Server{windows: map[ports.WindowID]*window{}, x11Slots: map[xwm.Slot]*window{}} | |||
| 101 | + | w := &window{id: 1, x11: &x11Window{override: true, rect: ports.Rect{X: 1, Y: 2, W: 3, H: 4}}} | |||
| 102 | + | s.x11Slots[1] = w | |||
| 103 | + | h := hostImpl{s} | |||
| 104 | + | if n := testing.AllocsPerRun(1000, func() { h.OverrideMoved(1, 1, 2, 3, 4) }); n != 0 { | |||
| 105 | + | t.Fatalf("%v allocs", n) | |||
| 106 | + | } | |||
| 107 | + | } | |||
A internal/adapters/wayland/x11peer_test.go +332 -0
| @@ -0,0 +1,332 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "io" | |||
| 6 | + | "os" | |||
| 7 | + | "sync" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "golang.org/x/sys/unix" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // x11Peer is a scripted X server speaking the real wire protocol over a | |||
| 15 | + | // socketpair: setup, extensions, atoms, properties, geometry. It records every | |||
| 16 | + | // request. It is a protocol peer, not a double of a project interface. | |||
| 17 | + | type x11Peer struct { | |||
| 18 | + | t *testing.T | |||
| 19 | + | f *os.File | |||
| 20 | + | mu sync.Mutex | |||
| 21 | + | ||||
| 22 | + | seq uint16 | |||
| 23 | + | reqs []x11Req | |||
| 24 | + | atoms map[string]uint32 | |||
| 25 | + | nextAtom uint32 | |||
| 26 | + | props map[uint32]map[uint32][]byte // window -> atom -> typed data (type, format, data) | |||
| 27 | + | propMeta map[uint32]map[uint32][2]uint32 | |||
| 28 | + | done chan struct{} | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | type x11Req struct { | |||
| 32 | + | major, minor uint8 | |||
| 33 | + | raw []byte | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func (r x11Req) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) } | |||
| 37 | + | ||||
| 38 | + | const ( | |||
| 39 | + | x11PeerRoot = 0x100 | |||
| 40 | + | x11Win = 0x500001 | |||
| 41 | + | // Opcodes the tests look for. | |||
| 42 | + | x11OpConfigureWindow = 12 | |||
| 43 | + | x11OpSetInputFocus = 42 | |||
| 44 | + | x11OpKillClient = 113 | |||
| 45 | + | ) | |||
| 46 | + | ||||
| 47 | + | func startX11Peer(t *testing.T) (int, *x11Peer) { | |||
| 48 | + | t.Helper() | |||
| 49 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 50 | + | if err != nil { | |||
| 51 | + | t.Fatal(err) | |||
| 52 | + | } | |||
| 53 | + | p := &x11Peer{ | |||
| 54 | + | t: t, f: os.NewFile(uintptr(fds[1]), "x11peer"), atoms: map[string]uint32{}, nextAtom: 1000, | |||
| 55 | + | props: map[uint32]map[uint32][]byte{}, propMeta: map[uint32]map[uint32][2]uint32{}, done: make(chan struct{}), | |||
| 56 | + | } | |||
| 57 | + | t.Cleanup(func() { p.f.Close(); <-p.done }) | |||
| 58 | + | go p.serve() | |||
| 59 | + | return fds[0], p | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func (p *x11Peer) setupReply() []byte { | |||
| 63 | + | vendor := []byte("x11pr\x00\x00\x00") | |||
| 64 | + | b := make([]byte, 40+len(vendor)+40) | |||
| 65 | + | b[0], b[2] = 1, 11 | |||
| 66 | + | binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4)) | |||
| 67 | + | binary.LittleEndian.PutUint32(b[12:], 0x200000) | |||
| 68 | + | binary.LittleEndian.PutUint32(b[16:], 0x1fffff) | |||
| 69 | + | binary.LittleEndian.PutUint16(b[24:], 5) | |||
| 70 | + | b[28] = 1 | |||
| 71 | + | copy(b[40:], vendor) | |||
| 72 | + | scr := b[40+len(vendor):] | |||
| 73 | + | binary.LittleEndian.PutUint32(scr, x11PeerRoot) | |||
| 74 | + | binary.LittleEndian.PutUint32(scr[32:], 0x22) | |||
| 75 | + | scr[38] = 24 | |||
| 76 | + | return b | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | func (p *x11Peer) serve() { | |||
| 80 | + | defer close(p.done) | |||
| 81 | + | var hdr [12]byte | |||
| 82 | + | if _, err := io.ReadFull(p.f, hdr[:]); err != nil { | |||
| 83 | + | return | |||
| 84 | + | } | |||
| 85 | + | if _, err := p.f.Write(p.setupReply()); err != nil { | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | for { | |||
| 89 | + | var h [4]byte | |||
| 90 | + | if _, err := io.ReadFull(p.f, h[:]); err != nil { | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | raw := h[:] | |||
| 94 | + | n := int(binary.LittleEndian.Uint16(h[2:])) * 4 | |||
| 95 | + | if n == 0 { | |||
| 96 | + | var l [4]byte | |||
| 97 | + | if _, err := io.ReadFull(p.f, l[:]); err != nil { | |||
| 98 | + | return | |||
| 99 | + | } | |||
| 100 | + | raw = append(raw, l[:]...) | |||
| 101 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |||
| 102 | + | } | |||
| 103 | + | rest := make([]byte, n-len(raw)) | |||
| 104 | + | if _, err := io.ReadFull(p.f, rest); err != nil { | |||
| 105 | + | return | |||
| 106 | + | } | |||
| 107 | + | raw = append(append([]byte(nil), raw...), rest...) | |||
| 108 | + | p.mu.Lock() | |||
| 109 | + | p.seq++ | |||
| 110 | + | r := x11Req{h[0], h[1], raw} | |||
| 111 | + | p.reqs = append(p.reqs, r) | |||
| 112 | + | p.answer(r, raw[len(raw)-len(rest):]) | |||
| 113 | + | p.mu.Unlock() | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func (p *x11Peer) reply(data uint8, body []byte) { | |||
| 118 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |||
| 119 | + | b := make([]byte, 32+4*words) | |||
| 120 | + | b[0], b[1] = 1, data | |||
| 121 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 122 | + | binary.LittleEndian.PutUint32(b[4:], uint32(words)) | |||
| 123 | + | copy(b[8:], body) | |||
| 124 | + | _, _ = p.f.Write(b) | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func x11le32(v ...uint32) []byte { | |||
| 128 | + | b := make([]byte, 0, 4*len(v)) | |||
| 129 | + | for _, x := range v { | |||
| 130 | + | b = binary.LittleEndian.AppendUint32(b, x) | |||
| 131 | + | } | |||
| 132 | + | return b | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | var x11Exts = map[string]uint8{"BIG-REQUESTS": 133, "XC-MISC": 136, "Composite": 142} | |||
| 136 | + | ||||
| 137 | + | func (p *x11Peer) answer(r x11Req, body []byte) { | |||
| 138 | + | switch { | |||
| 139 | + | case r.major == 98: // QueryExtension | |||
| 140 | + | major, ok := x11Exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |||
| 141 | + | present := uint8(0) | |||
| 142 | + | if ok { | |||
| 143 | + | present = 1 | |||
| 144 | + | } | |||
| 145 | + | p.reply(0, []byte{present, major, 0, 0}) | |||
| 146 | + | case r.major == 16: // InternAtom | |||
| 147 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |||
| 148 | + | a, ok := p.atoms[name] | |||
| 149 | + | if !ok { | |||
| 150 | + | p.nextAtom++ | |||
| 151 | + | a = p.nextAtom | |||
| 152 | + | p.atoms[name] = a | |||
| 153 | + | } | |||
| 154 | + | p.reply(0, x11le32(a)) | |||
| 155 | + | case r.major == 43: // GetInputFocus (Sync) | |||
| 156 | + | p.reply(1, x11le32(0)) | |||
| 157 | + | case r.major == 15: // QueryTree | |||
| 158 | + | b := make([]byte, 24) | |||
| 159 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |||
| 160 | + | p.reply(0, b) | |||
| 161 | + | case r.major == 14: // GetGeometry | |||
| 162 | + | b := make([]byte, 24) | |||
| 163 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |||
| 164 | + | binary.LittleEndian.PutUint16(b[8:], 300) | |||
| 165 | + | binary.LittleEndian.PutUint16(b[10:], 200) | |||
| 166 | + | p.reply(24, b) | |||
| 167 | + | case r.major == 18: // ChangeProperty | |||
| 168 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 169 | + | format, n := body[12], int(binary.LittleEndian.Uint32(body[16:])) | |||
| 170 | + | p.store(win, prop, binary.LittleEndian.Uint32(body[8:]), uint32(format), body[20:20+n*int(format)/8]) | |||
| 171 | + | case r.major == 20: // GetProperty | |||
| 172 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 173 | + | data, ok := p.props[win][prop] | |||
| 174 | + | b := make([]byte, 24+(len(data)+3)&^3) | |||
| 175 | + | var format uint8 | |||
| 176 | + | if ok { | |||
| 177 | + | meta := p.propMeta[win][prop] | |||
| 178 | + | format = uint8(meta[1]) | |||
| 179 | + | binary.LittleEndian.PutUint32(b[0:], meta[0]) | |||
| 180 | + | binary.LittleEndian.PutUint32(b[8:], uint32(len(data)/(int(format)/8))) | |||
| 181 | + | copy(b[24:], data) | |||
| 182 | + | } | |||
| 183 | + | p.reply(format, b) | |||
| 184 | + | case r.major >= 128: | |||
| 185 | + | switch r.major { | |||
| 186 | + | case 133: | |||
| 187 | + | p.reply(0, x11le32(0x100000)) | |||
| 188 | + | case 136: | |||
| 189 | + | if r.minor == 1 { | |||
| 190 | + | p.reply(0, x11le32(0, 1)) | |||
| 191 | + | } | |||
| 192 | + | case 142: | |||
| 193 | + | if r.minor == 0 { | |||
| 194 | + | p.reply(0, x11le32(0, 4)) | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | func (p *x11Peer) store(win, prop, typ, format uint32, data []byte) { | |||
| 201 | + | if p.props[win] == nil { | |||
| 202 | + | p.props[win], p.propMeta[win] = map[uint32][]byte{}, map[uint32][2]uint32{} | |||
| 203 | + | } | |||
| 204 | + | p.props[win][prop] = append([]byte(nil), data...) | |||
| 205 | + | p.propMeta[win][prop] = [2]uint32{typ, format} | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // atom interns name the way the XWM would have. | |||
| 209 | + | func (p *x11Peer) atom(name string) uint32 { | |||
| 210 | + | p.mu.Lock() | |||
| 211 | + | defer p.mu.Unlock() | |||
| 212 | + | a, ok := p.atoms[name] | |||
| 213 | + | if !ok { | |||
| 214 | + | p.nextAtom++ | |||
| 215 | + | a = p.nextAtom | |||
| 216 | + | p.atoms[name] = a | |||
| 217 | + | } | |||
| 218 | + | return a | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // setProp scripts a window property (typ is an atom number, format 8 or 32). | |||
| 222 | + | func (p *x11Peer) setProp(win, prop, typ, format uint32, data []byte) { | |||
| 223 | + | p.mu.Lock() | |||
| 224 | + | defer p.mu.Unlock() | |||
| 225 | + | p.store(win, prop, typ, format, data) | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | func (p *x11Peer) event(code uint8, fill func(b *[32]byte)) { | |||
| 229 | + | p.mu.Lock() | |||
| 230 | + | defer p.mu.Unlock() | |||
| 231 | + | var b [32]byte | |||
| 232 | + | b[0] = code | |||
| 233 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 234 | + | fill(&b) | |||
| 235 | + | _, _ = p.f.Write(b[:]) | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | func (p *x11Peer) createNotify(win uint32) { | |||
| 239 | + | p.event(16, func(b *[32]byte) { | |||
| 240 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 241 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 242 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |||
| 243 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |||
| 244 | + | }) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | // createOverride announces an override-redirect window of 100x50 at x,y. | |||
| 248 | + | func (p *x11Peer) createOverride(win uint32, x, y int16) { | |||
| 249 | + | p.event(16, func(b *[32]byte) { | |||
| 250 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 251 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 252 | + | binary.LittleEndian.PutUint16(b[12:], uint16(x)) | |||
| 253 | + | binary.LittleEndian.PutUint16(b[14:], uint16(y)) | |||
| 254 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |||
| 255 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |||
| 256 | + | b[22] = 1 | |||
| 257 | + | }) | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | func (p *x11Peer) mapNotify(win uint32) { | |||
| 261 | + | p.event(19, func(b *[32]byte) { | |||
| 262 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 263 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 264 | + | b[12] = 1 | |||
| 265 | + | }) | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | // configureNotify moves and resizes a window (root SubstructureNotify form). | |||
| 269 | + | func (p *x11Peer) configureNotify(win uint32, x, y int16, w, h uint16) { | |||
| 270 | + | p.event(22, func(b *[32]byte) { | |||
| 271 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 272 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 273 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |||
| 274 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |||
| 275 | + | binary.LittleEndian.PutUint16(b[20:], w) | |||
| 276 | + | binary.LittleEndian.PutUint16(b[22:], h) | |||
| 277 | + | b[24] = 1 | |||
| 278 | + | }) | |||
| 279 | + | } | |||
| 280 | + | ||||
| 281 | + | func (p *x11Peer) windowEvent(code uint8, win uint32) { | |||
| 282 | + | p.event(code, func(b *[32]byte) { | |||
| 283 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 284 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 285 | + | }) | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | func (p *x11Peer) mapRequest(win uint32) { p.windowEvent(20, win) } | |||
| 289 | + | func (p *x11Peer) unmapNotify(win uint32) { p.windowEvent(18, win) } | |||
| 290 | + | func (p *x11Peer) destroyNotify(win uint32) { p.windowEvent(17, win) } | |||
| 291 | + | ||||
| 292 | + | func (p *x11Peer) propertyNotify(win, atom uint32) { | |||
| 293 | + | p.event(28, func(b *[32]byte) { | |||
| 294 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 295 | + | binary.LittleEndian.PutUint32(b[8:], atom) | |||
| 296 | + | }) | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | func (p *x11Peer) clientMessage(win uint32, typ string, data ...uint32) { | |||
| 300 | + | t := p.atom(typ) | |||
| 301 | + | p.event(33, func(b *[32]byte) { | |||
| 302 | + | b[1] = 32 | |||
| 303 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 304 | + | binary.LittleEndian.PutUint32(b[8:], t) | |||
| 305 | + | for i, d := range data { | |||
| 306 | + | binary.LittleEndian.PutUint32(b[12+4*i:], d) | |||
| 307 | + | } | |||
| 308 | + | }) | |||
| 309 | + | } | |||
| 310 | + | ||||
| 311 | + | func (p *x11Peer) requests() []x11Req { | |||
| 312 | + | p.mu.Lock() | |||
| 313 | + | defer p.mu.Unlock() | |||
| 314 | + | return append([]x11Req(nil), p.reqs...) | |||
| 315 | + | } | |||
| 316 | + | ||||
| 317 | + | // waitReq waits for a request after the first skip ones that satisfies match. | |||
| 318 | + | func (p *x11Peer) waitReq(skip int, match func(x11Req) bool) (x11Req, int) { | |||
| 319 | + | p.t.Helper() | |||
| 320 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 321 | + | for time.Now().Before(deadline) { | |||
| 322 | + | reqs := p.requests() | |||
| 323 | + | for i := skip; i < len(reqs); i++ { | |||
| 324 | + | if match(reqs[i]) { | |||
| 325 | + | return reqs[i], i + 1 | |||
| 326 | + | } | |||
| 327 | + | } | |||
| 328 | + | time.Sleep(time.Millisecond) | |||
| 329 | + | } | |||
| 330 | + | p.t.Fatal("x11 peer: expected request never arrived") | |||
| 331 | + | return x11Req{}, 0 | |||
| 332 | + | } | |||
A internal/adapters/wayland/x11window.go +439 -0
| @@ -0,0 +1,439 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 5 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // x11Window is the X11 role of a window: the XWM slot, the window's X id and | |||
| 12 | + | // the wl_surface Xwayland paired with it. Display goroutine only. | |||
| 13 | + | type x11Window struct { | |||
| 14 | + | server *Server | |||
| 15 | + | slot xwm.Slot | |||
| 16 | + | xid uint32 | |||
| 17 | + | surface *surface | |||
| 18 | + | info xwm.WindowInfo | |||
| 19 | + | // ready is set once the XWM read the window's properties (Ready). | |||
| 20 | + | ready bool | |||
| 21 | + | // override marks an override-redirect window: tracked and drawn, never a | |||
| 22 | + | // toplevel nor keyboard focus. rect is its layout rect, noInput keeps the | |||
| 23 | + | // pointer off it. | |||
| 24 | + | override bool | |||
| 25 | + | noInput bool | |||
| 26 | + | rect ports.Rect | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | // xwaylandRestricted are the globals only the Xwayland client may see. | |||
| 30 | + | func xwaylandRestricted(iface string) bool { | |||
| 31 | + | return iface == "xwayland_shell_v1" || iface == "zwp_xwayland_keyboard_grab_manager_v1" | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // globalVisible is the display's global filter: the Xwayland-only globals | |||
| 35 | + | // are visible to, and bindable by, the Xwayland client alone. | |||
| 36 | + | func (s *Server) globalVisible(c server.Client, g *server.Global) bool { | |||
| 37 | + | if g == s.xdgPrivate { | |||
| 38 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |||
| 39 | + | } | |||
| 40 | + | if g == s.xdgPublic { | |||
| 41 | + | return c != s.xwayland.client | |||
| 42 | + | } | |||
| 43 | + | if xwaylandRestricted(g.Interface()) { | |||
| 44 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |||
| 45 | + | } | |||
| 46 | + | return true | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | func registerXwaylandShell(d *server.Display, s *Server) error { | |||
| 50 | + | return xwaylandshell.NewXwaylandShellV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 51 | + | _, _ = xwaylandshell.NewXwaylandShellV1(c, int32(v), id, xwaylandShell{s}) | |||
| 52 | + | }) | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | type xwaylandShell struct{ s *Server } | |||
| 56 | + | ||||
| 57 | + | func (xwaylandShell) Destroy(*xwaylandshell.XwaylandShellV1) {} | |||
| 58 | + | ||||
| 59 | + | func (h xwaylandShell) GetXwaylandSurface(r *xwaylandshell.XwaylandShellV1, id uint32, w *wayland.Surface) { | |||
| 60 | + | surf := h.s.surfaceOf(w) | |||
| 61 | + | if surf == nil { | |||
| 62 | + | return | |||
| 63 | + | } | |||
| 64 | + | if surf.kind != roleNone { | |||
| 65 | + | r.PostError(uint32(xwaylandshell.XwaylandShellV1ErrorRole), "surface already has a role") | |||
| 66 | + | return | |||
| 67 | + | } | |||
| 68 | + | if _, err := xwaylandshell.NewXwaylandSurfaceV1(r.Client(), r.Version(), id, xwaylandSurface{h.s, surf}); err != nil { | |||
| 69 | + | return | |||
| 70 | + | } | |||
| 71 | + | surf.kind = roleX11 | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | type xwaylandSurface struct { | |||
| 75 | + | s *Server | |||
| 76 | + | surf *surface | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | func (x xwaylandSurface) SetSerial(r *xwaylandshell.XwaylandSurfaceV1, lo, hi uint32) { | |||
| 80 | + | serial := uint64(hi)<<32 | uint64(lo) | |||
| 81 | + | switch { | |||
| 82 | + | case x.surf.x11Serial != 0: | |||
| 83 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated), "serial already set") | |||
| 84 | + | case serial == 0: | |||
| 85 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial), "serial is zero") | |||
| 86 | + | default: | |||
| 87 | + | x.surf.x11Serial = serial | |||
| 88 | + | if x.s.xwm != nil { | |||
| 89 | + | x.s.xwm.SetSerial(x.surf.identity, serial) | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func (xwaylandSurface) Destroy(*xwaylandshell.XwaylandSurfaceV1) {} | |||
| 95 | + | ||||
| 96 | + | // installXWM makes x the server's window manager. Display goroutine only. | |||
| 97 | + | func (s *Server) installXWM(x *xwm.XWM) { | |||
| 98 | + | s.teardownXWM() | |||
| 99 | + | s.xwm = x | |||
| 100 | + | x.UseSpace(&s.xspace) | |||
| 101 | + | if err := x.Attach(s.display, hostImpl{s}); err != nil { | |||
| 102 | + | s.log.Warn().Err(err).Msg("X11 window management disabled") | |||
| 103 | + | s.xwm = nil | |||
| 104 | + | x.CloseConn() | |||
| 105 | + | return | |||
| 106 | + | } | |||
| 107 | + | // Serials sent before the XWM was attached pair now. | |||
| 108 | + | for _, surf := range s.surfaces { | |||
| 109 | + | if surf.kind == roleX11 && surf.x11Serial != 0 && surf.x11win == nil { | |||
| 110 | + | x.SetSerial(surf.identity, surf.x11Serial) | |||
| 111 | + | } | |||
| 112 | + | } | |||
| 113 | + | x.LayoutChanged(true) | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | // teardownXWM closes the window manager, dropping its windows. Idempotent. | |||
| 117 | + | func (s *Server) teardownXWM() { | |||
| 118 | + | if x := s.xwm; x != nil { | |||
| 119 | + | s.xwm = nil | |||
| 120 | + | x.Close() | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // x11Configure forwards the latest core configure of an X11 window. | |||
| 125 | + | func (s *Server) x11Configure(w *window) { | |||
| 126 | + | if s.xwm != nil { | |||
| 127 | + | s.xwm.Configure(w.x11.slot, w.last) | |||
| 128 | + | } | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // x11Close asks an X11 window to close. | |||
| 132 | + | func (s *Server) x11Close(w *window) { | |||
| 133 | + | if s.xwm != nil { | |||
| 134 | + | s.xwm.CloseWindow(w.x11.slot) | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // x11Place forwards core's position of an X11 window's client rect. | |||
| 139 | + | func (s *Server) x11Place(c ports.PlaceWindow) { | |||
| 140 | + | if w := s.windows[c.ID]; w != nil && w.x11 != nil && s.xwm != nil { | |||
| 141 | + | s.xwm.Place(w.x11.slot, c.X, c.Y) | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // x11Focus tells the XWM which X window holds the keyboard focus, none when a | |||
| 146 | + | // native window does. | |||
| 147 | + | func (s *Server) x11Focus() { | |||
| 148 | + | if s.xwm == nil { | |||
| 149 | + | return | |||
| 150 | + | } | |||
| 151 | + | var slot xwm.Slot | |||
| 152 | + | if w := s.windows[s.seat.focused]; w != nil && w.x11 != nil { | |||
| 153 | + | slot = w.x11.slot | |||
| 154 | + | } | |||
| 155 | + | s.xwm.Focus(slot) | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | // hostImpl is the server as the XWM sees it. | |||
| 159 | + | type hostImpl struct{ s *Server } | |||
| 160 | + | ||||
| 161 | + | func (h hostImpl) window(slot xwm.Slot) *window { return h.s.x11Slots[slot] } | |||
| 162 | + | ||||
| 163 | + | func (h hostImpl) Created(slot xwm.Slot, xid uint32) ports.WindowID { | |||
| 164 | + | s := h.s | |||
| 165 | + | w := &window{id: s.nextWindow} | |||
| 166 | + | w.x11 = &x11Window{server: s, slot: slot, xid: xid} | |||
| 167 | + | s.nextWindow++ | |||
| 168 | + | s.windows[w.id] = w | |||
| 169 | + | s.x11Slots[slot] = w | |||
| 170 | + | return w.id | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | func (h hostImpl) Ready(slot xwm.Slot, info xwm.WindowInfo) { | |||
| 174 | + | w := h.window(slot) | |||
| 175 | + | if w == nil { | |||
| 176 | + | return | |||
| 177 | + | } | |||
| 178 | + | w.x11.info, w.x11.ready = info, true | |||
| 179 | + | w.title, w.appID, w.modal = info.Title, info.AppID, info.Modal | |||
| 180 | + | w.parent = h.window(info.Parent) | |||
| 181 | + | // A surface associated and committed earlier maps now. | |||
| 182 | + | w.x11.tryMap() | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | func (h hostImpl) Associate(slot xwm.Slot, surfaceID uint64) { | |||
| 186 | + | w := h.window(slot) | |||
| 187 | + | surf := h.s.surfaceByIdentity[surfaceID] | |||
| 188 | + | if w == nil || surf == nil || surf.kind != roleX11 { | |||
| 189 | + | return | |||
| 190 | + | } | |||
| 191 | + | x := w.x11 | |||
| 192 | + | if x.surface != nil && x.surface != surf { | |||
| 193 | + | // A new surface replaces the shown one: core sees a remap. | |||
| 194 | + | w.unmap() | |||
| 195 | + | } | |||
| 196 | + | x.drop() | |||
| 197 | + | x.surface, surf.x11win = surf, w | |||
| 198 | + | surf.role = func(buffer bool) { | |||
| 199 | + | switch { | |||
| 200 | + | case surf.destroyed: | |||
| 201 | + | w.unmap() | |||
| 202 | + | if x.surface == surf { | |||
| 203 | + | x.surface = nil | |||
| 204 | + | } | |||
| 205 | + | surf.x11win = nil | |||
| 206 | + | case buffer: | |||
| 207 | + | x.tryMap() | |||
| 208 | + | case w.mapped: | |||
| 209 | + | w.unmap() | |||
| 210 | + | } | |||
| 211 | + | } | |||
| 212 | + | // The buffer may have been committed before the pairing. | |||
| 213 | + | x.tryMap() | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | func (h hostImpl) Dissociate(slot xwm.Slot, destroyed bool) { | |||
| 217 | + | w := h.window(slot) | |||
| 218 | + | if w == nil { | |||
| 219 | + | return | |||
| 220 | + | } | |||
| 221 | + | w.unmap() | |||
| 222 | + | w.x11.drop() | |||
| 223 | + | w.x11.ready = false | |||
| 224 | + | if destroyed { | |||
| 225 | + | for _, c := range h.s.windows { | |||
| 226 | + | if c.parent == w { | |||
| 227 | + | c.parent = nil | |||
| 228 | + | } | |||
| 229 | + | } | |||
| 230 | + | delete(h.s.windows, w.id) | |||
| 231 | + | delete(h.s.x11Slots, slot) | |||
| 232 | + | h.s.foreignToplevelGone(w) | |||
| 233 | + | } | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | func (h hostImpl) Title(slot xwm.Slot, title string) { | |||
| 237 | + | if w := h.window(slot); w != nil && w.title != title { | |||
| 238 | + | w.title = title | |||
| 239 | + | h.s.toplevelChanged(w) | |||
| 240 | + | if w.mapped { | |||
| 241 | + | h.s.castInventoryChanged() | |||
| 242 | + | } | |||
| 243 | + | } | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | func (h hostImpl) AppID(slot xwm.Slot, appID string) { | |||
| 247 | + | w := h.window(slot) | |||
| 248 | + | if w == nil || w.appID == appID { | |||
| 249 | + | return | |||
| 250 | + | } | |||
| 251 | + | w.appID = appID | |||
| 252 | + | if w.mapped { | |||
| 253 | + | h.s.emit(ports.WindowAppID{ID: w.id, AppID: appID}) | |||
| 254 | + | h.s.toplevelChanged(w) | |||
| 255 | + | h.s.castInventoryChanged() | |||
| 256 | + | } | |||
| 257 | + | } | |||
| 258 | + | ||||
| 259 | + | func (h hostImpl) Parent(slot xwm.Slot, parent xwm.Slot) { | |||
| 260 | + | if w := h.window(slot); w != nil { | |||
| 261 | + | w.setParent(h.window(parent)) | |||
| 262 | + | } | |||
| 263 | + | } | |||
| 264 | + | ||||
| 265 | + | func (h hostImpl) Fullscreen(slot xwm.Slot, on bool) { | |||
| 266 | + | w := h.window(slot) | |||
| 267 | + | if w == nil { | |||
| 268 | + | return | |||
| 269 | + | } | |||
| 270 | + | w.x11.info.Fullscreen = on | |||
| 271 | + | if w.mapped { | |||
| 272 | + | h.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: on}) | |||
| 273 | + | } | |||
| 274 | + | } | |||
| 275 | + | ||||
| 276 | + | // drop unlinks the window from its surface. | |||
| 277 | + | func (x *x11Window) drop() { | |||
| 278 | + | if surf := x.surface; surf != nil { | |||
| 279 | + | surf.x11win, surf.role = nil, nil | |||
| 280 | + | x.surface = nil | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | // tryMap maps the window once the XWM knows it and its surface shows a buffer. | |||
| 285 | + | func (x *x11Window) tryMap() { | |||
| 286 | + | surf := x.surface | |||
| 287 | + | if surf == nil { | |||
| 288 | + | return | |||
| 289 | + | } | |||
| 290 | + | w := surf.x11win | |||
| 291 | + | if w == nil || w.mapped || !x.ready || surf.destroyed || surf.current == nil { | |||
| 292 | + | return | |||
| 293 | + | } | |||
| 294 | + | s := x.server | |||
| 295 | + | w.mapped = true | |||
| 296 | + | w.gen++ | |||
| 297 | + | w.floating = x.info.Floating | |||
| 298 | + | w.floatW, w.floatH = 0, 0 | |||
| 299 | + | if w.floating { | |||
| 300 | + | w.floatW, w.floatH = x.info.Width, x.info.Height | |||
| 301 | + | } | |||
| 302 | + | var parent ports.WindowID | |||
| 303 | + | if w.parent != nil && w.parent.mapped { | |||
| 304 | + | parent = w.parent.id | |||
| 305 | + | } | |||
| 306 | + | s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1}) | |||
| 307 | + | if x.info.Fullscreen { | |||
| 308 | + | s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true}) | |||
| 309 | + | } | |||
| 310 | + | s.syncInhibitors() | |||
| 311 | + | s.toplevelChanged(w) | |||
| 312 | + | s.castInventoryChanged() | |||
| 313 | + | s.log.Info().Uint64("id", uint64(w.id)).Uint32("xid", x.xid).Str("app_id", w.appID).Bool("floating", w.floating).Msg("x11 window mapped") | |||
| 314 | + | // Called from a commit, the content follows right after; called from the | |||
| 315 | + | // pairing, the surface already shows a buffer and is published now. | |||
| 316 | + | if surf.has { | |||
| 317 | + | surf.resetInputEmission() | |||
| 318 | + | surf.redraw() | |||
| 319 | + | surf.emitInput() | |||
| 320 | + | } | |||
| 321 | + | } | |||
| 322 | + | ||||
| 323 | + | // OverrideMapped starts tracking an override-redirect window. It is announced | |||
| 324 | + | // to core on the paired surface's first buffer commit, so it is never drawn | |||
| 325 | + | // without content. | |||
| 326 | + | func (h hostImpl) OverrideMapped(slot xwm.Slot, xid uint32, surfaceID uint64, x, y, w, h2 int, noInput bool) { | |||
| 327 | + | s := h.s | |||
| 328 | + | surf := s.surfaceByIdentity[surfaceID] | |||
| 329 | + | if surf == nil || surf.kind != roleX11 || surf.x11win != nil { | |||
| 330 | + | return | |||
| 331 | + | } | |||
| 332 | + | if old := s.x11Slots[slot]; old != nil { | |||
| 333 | + | h.OverrideGone(slot) | |||
| 334 | + | } | |||
| 335 | + | win := &window{id: s.nextWindow} | |||
| 336 | + | win.x11 = &x11Window{server: s, slot: slot, xid: xid, override: true, noInput: noInput, rect: ports.Rect{X: x, Y: y, W: w, H: h2}} | |||
| 337 | + | s.nextWindow++ | |||
| 338 | + | s.windows[win.id] = win | |||
| 339 | + | s.x11Slots[slot] = win | |||
| 340 | + | ox := win.x11 | |||
| 341 | + | ox.surface, surf.x11win = surf, win | |||
| 342 | + | surf.role = func(buffer bool) { | |||
| 343 | + | switch { | |||
| 344 | + | case surf.destroyed: | |||
| 345 | + | ox.hide() | |||
| 346 | + | ox.surface = nil | |||
| 347 | + | surf.x11win = nil | |||
| 348 | + | case buffer: | |||
| 349 | + | ox.show() | |||
| 350 | + | case win.mapped: | |||
| 351 | + | ox.hide() | |||
| 352 | + | } | |||
| 353 | + | } | |||
| 354 | + | ox.show() | |||
| 355 | + | } | |||
| 356 | + | ||||
| 357 | + | // OverrideMoved records the new layout rect of an override window and, once | |||
| 358 | + | // shown, tells core. An unchanged rect costs nothing. | |||
| 359 | + | func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) { | |||
| 360 | + | win := h.s.x11Slots[slot] | |||
| 361 | + | if win == nil || win.x11 == nil || !win.x11.override { | |||
| 362 | + | return | |||
| 363 | + | } | |||
| 364 | + | r := ports.Rect{X: x, Y: y, W: w, H: h2} | |||
| 365 | + | if r == win.x11.rect { | |||
| 366 | + | return | |||
| 367 | + | } | |||
| 368 | + | win.x11.rect = r | |||
| 369 | + | if win.mapped { | |||
| 370 | + | h.s.emit(ports.UnmanagedMoved{ID: win.id, X: x, Y: y, Width: w, Height: h2}) | |||
| 371 | + | win.x11.surface.sendTreeScale() | |||
| 372 | + | } | |||
| 373 | + | } | |||
| 374 | + | ||||
| 375 | + | // OverrideGone ends an override window: unmapped, or destroyed. | |||
| 376 | + | func (h hostImpl) OverrideGone(slot xwm.Slot) { | |||
| 377 | + | s := h.s | |||
| 378 | + | win := s.x11Slots[slot] | |||
| 379 | + | if win == nil || win.x11 == nil || !win.x11.override { | |||
| 380 | + | return | |||
| 381 | + | } | |||
| 382 | + | win.x11.hide() | |||
| 383 | + | win.x11.drop() | |||
| 384 | + | delete(s.windows, win.id) | |||
| 385 | + | delete(s.x11Slots, slot) | |||
| 386 | + | } | |||
| 387 | + | ||||
| 388 | + | // show announces the window to core once its surface has content. | |||
| 389 | + | func (x *x11Window) show() { | |||
| 390 | + | surf := x.surface | |||
| 391 | + | if surf == nil || surf.x11win == nil || surf.destroyed || surf.current == nil { | |||
| 392 | + | return | |||
| 393 | + | } | |||
| 394 | + | w := surf.x11win | |||
| 395 | + | if w.mapped { | |||
| 396 | + | return | |||
| 397 | + | } | |||
| 398 | + | s := x.server | |||
| 399 | + | w.mapped = true | |||
| 400 | + | r := x.rect | |||
| 401 | + | s.emit(ports.UnmanagedMapped{ID: w.id, X: r.X, Y: r.Y, Width: r.W, Height: r.H, NoInput: x.noInput}) | |||
| 402 | + | surf.sendTreeScale() | |||
| 403 | + | if surf.has { | |||
| 404 | + | surf.resetInputEmission() | |||
| 405 | + | surf.redraw() | |||
| 406 | + | surf.emitInput() | |||
| 407 | + | } | |||
| 408 | + | } | |||
| 409 | + | ||||
| 410 | + | // hide withdraws a shown override window from core and the seat. | |||
| 411 | + | func (x *x11Window) hide() { | |||
| 412 | + | if x.surface == nil || x.surface.x11win == nil { | |||
| 413 | + | return | |||
| 414 | + | } | |||
| 415 | + | w := x.surface.x11win | |||
| 416 | + | s := x.server | |||
| 417 | + | if s.seat.pointerFocus == w.id { | |||
| 418 | + | s.changePointerFocus(0, 0, 0) | |||
| 419 | + | } | |||
| 420 | + | if !w.mapped { | |||
| 421 | + | return | |||
| 422 | + | } | |||
| 423 | + | w.mapped = false | |||
| 424 | + | x.surface.resetInputEmission() | |||
| 425 | + | s.emit(ports.UnmanagedUnmapped{ID: w.id}) | |||
| 426 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | // isOverride reports whether w is an override-redirect X11 window. | |||
| 430 | + | func (w *window) isOverride() bool { return w.x11 != nil && w.x11.override } | |||
| 431 | + | ||||
| 432 | + | // overrideOutput is the name of the output under the origin of an override | |||
| 433 | + | // window's rect, "" when none. | |||
| 434 | + | func (s *Server) overrideOutput(w *window) string { | |||
| 435 | + | if o, ok := s.outputPlaces.At(float64(w.x11.rect.X), float64(w.x11.rect.Y)); ok { | |||
| 436 | + | return o.Info.Name | |||
| 437 | + | } | |||
| 438 | + | return "" | |||
| 439 | + | } | |||
A internal/adapters/wayland/x11window_test.go +353 -0
| @@ -0,0 +1,353 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "net" | |||
| 7 | + | "os" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | |||
| 12 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 13 | + | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |||
| 14 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 15 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 16 | + | "github.com/bnema/wlturbo" | |||
| 17 | + | "golang.org/x/sys/unix" | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | var x11Serial = uint64(0x1234_0000_0042) | |||
| 21 | + | ||||
| 22 | + | // x11Rig is a server with an Xwayland client (a real wlturbo client on the | |||
| 23 | + | // adopted descriptor), a scripted X peer on the WM descriptor and the XWM | |||
| 24 | + | // between them. | |||
| 25 | + | type x11Rig struct { | |||
| 26 | + | t *testing.T | |||
| 27 | + | s *Server | |||
| 28 | + | events chan ports.ClientEvent | |||
| 29 | + | commands chan ports.ClientCommand | |||
| 30 | + | dir string | |||
| 31 | + | xc *wlturbo.Display | |||
| 32 | + | peer *x11Peer | |||
| 33 | + | stop func() | |||
| 34 | + | shell uint32 | |||
| 35 | + | comp uint32 | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | func newX11Rig(t *testing.T) *x11Rig { | |||
| 39 | + | t.Helper() | |||
| 40 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 41 | + | r := &x11Rig{t: t, s: s, events: events, commands: commands, dir: dir} | |||
| 42 | + | wl := xwaylandPair(t) | |||
| 43 | + | s.AttachXwayland(wl[0]) | |||
| 44 | + | f := os.NewFile(uintptr(wl[1]), "xwayland client") | |||
| 45 | + | wl[1] = -1 | |||
| 46 | + | conn, err := net.FileConn(f) | |||
| 47 | + | f.Close() | |||
| 48 | + | if err != nil { | |||
| 49 | + | t.Fatal(err) | |||
| 50 | + | } | |||
| 51 | + | if r.xc, err = wlturbo.ConnectFromConn(conn); err != nil { | |||
| 52 | + | t.Fatal(err) | |||
| 53 | + | } | |||
| 54 | + | t.Cleanup(func() { _ = r.xc.Close(); forgetWireClient(r.xc) }) | |||
| 55 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 56 | + | t.Fatal(err) | |||
| 57 | + | } | |||
| 58 | + | wm, peer := startX11Peer(t) | |||
| 59 | + | r.peer = peer | |||
| 60 | + | r.stop = s.AttachXWM(context.Background(), wm) | |||
| 61 | + | t.Cleanup(func() { r.stop() }) | |||
| 62 | + | r.shell = bindProtocol(t, r.xc, "xwayland_shell_v1") | |||
| 63 | + | r.comp = bindProtocol(t, r.xc, "wl_compositor") | |||
| 64 | + | return r | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // x11Window announces and maps X window win with the properties of an | |||
| 68 | + | // ordinary client, and waits for the XWM to be ready for it. | |||
| 69 | + | func (r *x11Rig) x11Window(win uint32) { | |||
| 70 | + | r.t.Helper() | |||
| 71 | + | p := r.peer | |||
| 72 | + | str := uint32(31) // XA_STRING | |||
| 73 | + | p.setProp(win, 67, str, 8, []byte("inst\x00Class\x00")) // WM_CLASS | |||
| 74 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("net title")) | |||
| 75 | + | p.setProp(win, p.atom("_NET_WM_PID"), 6, 32, x11le32(4242)) | |||
| 76 | + | p.createNotify(win) | |||
| 77 | + | skip := len(p.requests()) | |||
| 78 | + | p.mapRequest(win) | |||
| 79 | + | p.waitReq(skip, func(q x11Req) bool { return q.major == 8 && q.u32(4) == win }) // MapWindow | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // pair sends the serial from both sides and commits a buffer on the surface. | |||
| 83 | + | // It returns the wl_surface id of the Xwayland client. | |||
| 84 | + | func (r *x11Rig) pair(win uint32, serial uint64) uint32 { | |||
| 85 | + | r.t.Helper() | |||
| 86 | + | r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32)) | |||
| 87 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |||
| 88 | + | requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 89 | + | registerProtocol(r.t, r.xc, surf) | |||
| 90 | + | requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 91 | + | registerProtocol(r.t, r.xc, xs) | |||
| 92 | + | requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32)) | |||
| 93 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(r.t, r.xc), int32(0), int32(0)) | |||
| 94 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestCommit) | |||
| 95 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 96 | + | r.t.Fatal(err) | |||
| 97 | + | } | |||
| 98 | + | return surf | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | func awaitEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |||
| 102 | + | t.Helper() | |||
| 103 | + | for deadline := time.After(3 * time.Second); ; { | |||
| 104 | + | select { | |||
| 105 | + | case ev := <-events: | |||
| 106 | + | if v, ok := ev.(T); ok { | |||
| 107 | + | return v | |||
| 108 | + | } | |||
| 109 | + | case <-deadline: | |||
| 110 | + | var zero T | |||
| 111 | + | t.Fatalf("no %T event", zero) | |||
| 112 | + | } | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | func TestXwaylandGlobalFilter(t *testing.T) { | |||
| 117 | + | s, _, _, dir := lifecycleServer(t) | |||
| 118 | + | other := protocolClient(t, s, dir) | |||
| 119 | + | for _, iface := range []string{"xwayland_shell_v1", "zwp_xwayland_keyboard_grab_manager_v1"} { | |||
| 120 | + | if _, ok := other.Registry().FindGlobal(iface); ok { | |||
| 121 | + | t.Errorf("ordinary client sees %s", iface) | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | if _, ok := other.Registry().FindGlobal("xdg_wm_base"); !ok { | |||
| 125 | + | t.Error("ordinary globals filtered out") | |||
| 126 | + | } | |||
| 127 | + | r := newX11Rig(t) | |||
| 128 | + | if _, ok := r.xc.Registry().FindGlobal("xwayland_shell_v1"); !ok { | |||
| 129 | + | t.Error("Xwayland client misses xwayland_shell_v1") | |||
| 130 | + | } | |||
| 131 | + | // The ordinary client of the same server still does not see it. | |||
| 132 | + | late := protocolClient(t, r.s, r.dir) | |||
| 133 | + | if _, ok := late.Registry().FindGlobal("xwayland_shell_v1"); ok { | |||
| 134 | + | t.Error("ordinary client sees xwayland_shell_v1 while Xwayland is attached") | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | func TestXwaylandShellRoleErrors(t *testing.T) { | |||
| 139 | + | for _, scenario := range []string{"xdg role first", "serial twice", "zero serial"} { | |||
| 140 | + | t.Run(scenario, func(t *testing.T) { | |||
| 141 | + | r := newX11Rig(t) | |||
| 142 | + | c := r.xc | |||
| 143 | + | wmBase := bindProtocol(t, c, "xdg_wm_base") | |||
| 144 | + | surf := c.AllocateID() | |||
| 145 | + | requestProtocol(t, c, r.comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 146 | + | registerProtocol(t, c, surf) | |||
| 147 | + | xs := c.AllocateID() | |||
| 148 | + | switch scenario { | |||
| 149 | + | case "xdg role first": | |||
| 150 | + | xdg := xs | |||
| 151 | + | requestProtocol(t, c, wmBase, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 152 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, c.AllocateID(), surf) | |||
| 153 | + | expectProtocolError(t, c, r.shell, uint32(xwaylandshell.XwaylandShellV1ErrorRole)) | |||
| 154 | + | case "serial twice": | |||
| 155 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 156 | + | registerProtocol(t, c, xs) | |||
| 157 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(1), uint32(0)) | |||
| 158 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(2), uint32(0)) | |||
| 159 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated)) | |||
| 160 | + | case "zero serial": | |||
| 161 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 162 | + | registerProtocol(t, c, xs) | |||
| 163 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(0), uint32(0)) | |||
| 164 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial)) | |||
| 165 | + | } | |||
| 166 | + | }) | |||
| 167 | + | } | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | // A window announced by the X server and a surface carrying the same serial | |||
| 171 | + | // map as one neferwl window with the X11 info; core commands reach the X side. | |||
| 172 | + | func TestX11WindowLifecycle(t *testing.T) { | |||
| 173 | + | r := newX11Rig(t) | |||
| 174 | + | win := uint32(x11Win) | |||
| 175 | + | r.x11Window(win) | |||
| 176 | + | surf := r.pair(win, x11Serial) | |||
| 177 | + | ||||
| 178 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 179 | + | if m.AppID != "Class" || m.PID != 4242 || !m.Positioned || m.Floating || m.Slot != "" { | |||
| 180 | + | t.Fatalf("mapped %+v", m) | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | // Title and app id changes. | |||
| 184 | + | p := r.peer | |||
| 185 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("renamed")) | |||
| 186 | + | p.propertyNotify(win, p.atom("_NET_WM_NAME")) | |||
| 187 | + | p.setProp(win, 67, 31, 8, []byte("inst\x00Other\x00")) | |||
| 188 | + | p.propertyNotify(win, 67) | |||
| 189 | + | if a := awaitEvent[ports.WindowAppID](t, r.events); a.ID != m.ID || a.AppID != "Other" { | |||
| 190 | + | t.Fatalf("app id %+v", a) | |||
| 191 | + | } | |||
| 192 | + | var title string | |||
| 193 | + | waitTrue(t, "title", func() bool { | |||
| 194 | + | r.s.display.Do(func() { | |||
| 195 | + | if w := r.s.windows[m.ID]; w != nil { | |||
| 196 | + | title = w.title | |||
| 197 | + | } | |||
| 198 | + | }) | |||
| 199 | + | return title == "renamed" | |||
| 200 | + | }) | |||
| 201 | + | ||||
| 202 | + | // Fullscreen request from the client. | |||
| 203 | + | p.clientMessage(win, "_NET_WM_STATE", 1, p.atom("_NET_WM_STATE_FULLSCREEN")) | |||
| 204 | + | if f := awaitEvent[ports.WindowFullscreenRequest](t, r.events); f.ID != m.ID || !f.Fullscreen || f.External { | |||
| 205 | + | t.Fatalf("fullscreen %+v", f) | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // Configure and Place reach the X server as ConfigureWindow. | |||
| 209 | + | skip := len(p.requests()) | |||
| 210 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 640, Height: 360, Visible: true} | |||
| 211 | + | cfg, skip := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |||
| 212 | + | if w := binary.LittleEndian.Uint32(cfg.raw[20:]); w != 640 { | |||
| 213 | + | t.Fatalf("configured width %d, want 640 (%x)", w, cfg.raw) | |||
| 214 | + | } | |||
| 215 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 10, Y: 20} | |||
| 216 | + | cfg, _ = p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |||
| 217 | + | if x, y := int32(binary.LittleEndian.Uint32(cfg.raw[12:])), int32(binary.LittleEndian.Uint32(cfg.raw[16:])); x != 10 || y != 20 { | |||
| 218 | + | t.Fatalf("placed at %d,%d, want 10,20", x, y) | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // Focus. | |||
| 222 | + | skip = len(p.requests()) | |||
| 223 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 224 | + | if f, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) != win { | |||
| 225 | + | t.Fatalf("focus on %#x, want %#x", f.u32(4), win) | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | // Close: without WM_DELETE_WINDOW the client is killed. | |||
| 229 | + | skip = len(p.requests()) | |||
| 230 | + | r.commands <- ports.CloseWindow{ID: m.ID} | |||
| 231 | + | if k, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpKillClient }); k.u32(4) != win { | |||
| 232 | + | t.Fatalf("killed %#x, want %#x", k.u32(4), win) | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | // Destroying the surface unmaps the window. | |||
| 236 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |||
| 237 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 238 | + | t.Fatal(err) | |||
| 239 | + | } | |||
| 240 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |||
| 241 | + | t.Fatalf("unmapped %+v", u) | |||
| 242 | + | } | |||
| 243 | + | } | |||
| 244 | + | ||||
| 245 | + | // The serial may arrive on the Wayland side before the X side knows it, and | |||
| 246 | + | // an UnmapNotify unmaps; a remap pairs again. | |||
| 247 | + | func TestX11WindowSurfaceFirstAndRemap(t *testing.T) { | |||
| 248 | + | r := newX11Rig(t) | |||
| 249 | + | win := uint32(x11Win) | |||
| 250 | + | p := r.peer | |||
| 251 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |||
| 252 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 253 | + | registerProtocol(t, r.xc, surf) | |||
| 254 | + | requestProtocol(t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 255 | + | registerProtocol(t, r.xc, xs) | |||
| 256 | + | requestProtocol(t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(x11Serial), uint32(x11Serial>>32)) | |||
| 257 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(t, r.xc), int32(0), int32(0)) | |||
| 258 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestCommit) | |||
| 259 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 260 | + | t.Fatal(err) | |||
| 261 | + | } | |||
| 262 | + | r.x11Window(win) | |||
| 263 | + | p.clientMessage(win, "WL_SURFACE_SERIAL", uint32(x11Serial), uint32(x11Serial>>32)) | |||
| 264 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 265 | + | if m.Remap { | |||
| 266 | + | t.Fatalf("first map flagged as remap: %+v", m) | |||
| 267 | + | } | |||
| 268 | + | p.unmapNotify(win) | |||
| 269 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |||
| 270 | + | t.Fatalf("unmapped %+v", u) | |||
| 271 | + | } | |||
| 272 | + | p.destroyNotify(win) | |||
| 273 | + | waitTrue(t, "window forgotten", func() bool { | |||
| 274 | + | gone := false | |||
| 275 | + | r.s.display.Do(func() { gone = r.s.windows[m.ID] == nil }) | |||
| 276 | + | return gone | |||
| 277 | + | }) | |||
| 278 | + | } | |||
| 279 | + | ||||
| 280 | + | // stop closes the XWM: its windows unmap, and the server keeps serving. | |||
| 281 | + | func TestX11StopUnmapsWindows(t *testing.T) { | |||
| 282 | + | r := newX11Rig(t) | |||
| 283 | + | win := uint32(x11Win) | |||
| 284 | + | r.x11Window(win) | |||
| 285 | + | r.pair(win, x11Serial) | |||
| 286 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 287 | + | r.stop() | |||
| 288 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |||
| 289 | + | t.Fatalf("unmapped %+v", u) | |||
| 290 | + | } | |||
| 291 | + | r.s.display.Do(func() { | |||
| 292 | + | if r.s.xwm != nil || r.s.windows[m.ID] != nil || len(r.s.x11Slots) != 0 { | |||
| 293 | + | t.Error("XWM state left behind") | |||
| 294 | + | } | |||
| 295 | + | }) | |||
| 296 | + | r.stop() // idempotent | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | // Boot gives up as soon as its context ends, even against a silent server, | |||
| 300 | + | // and leaves the server serving. | |||
| 301 | + | func TestAttachXWMBootAbortsWithContext(t *testing.T) { | |||
| 302 | + | s, _, _, dir := lifecycleServer(t) | |||
| 303 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 304 | + | if err != nil { | |||
| 305 | + | t.Fatal(err) | |||
| 306 | + | } | |||
| 307 | + | defer unix.Close(fds[1]) // the silent server end | |||
| 308 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 309 | + | done := make(chan func(), 1) | |||
| 310 | + | go func() { done <- s.AttachXWM(ctx, fds[0]) }() | |||
| 311 | + | select { | |||
| 312 | + | case <-done: | |||
| 313 | + | t.Fatal("AttachXWM returned before the context ended") | |||
| 314 | + | case <-time.After(150 * time.Millisecond): | |||
| 315 | + | } | |||
| 316 | + | cancel() | |||
| 317 | + | select { | |||
| 318 | + | case stop := <-done: | |||
| 319 | + | stop() | |||
| 320 | + | case <-time.After(2 * time.Second): | |||
| 321 | + | t.Fatal("AttachXWM did not abort with its context") | |||
| 322 | + | } | |||
| 323 | + | assertFDClosed(t, fds[0]) | |||
| 324 | + | protocolClient(t, s, dir) // still serving | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | // Xwayland locks the pointer for games: the constraint reaches core for the | |||
| 328 | + | // X11 window like for an xdg toplevel. | |||
| 329 | + | func TestX11WindowPointerLock(t *testing.T) { | |||
| 330 | + | r := newX11Rig(t) | |||
| 331 | + | win := uint32(x11Win) | |||
| 332 | + | r.x11Window(win) | |||
| 333 | + | surf := r.pair(win, x11Serial) | |||
| 334 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 335 | + | ||||
| 336 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |||
| 337 | + | registerProtocol(t, r.xc, seat) | |||
| 338 | + | pointer := r.xc.AllocateID() | |||
| 339 | + | requestProtocol(t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 340 | + | registerProtocol(t, r.xc, pointer) | |||
| 341 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |||
| 342 | + | lock := r.xc.AllocateID() | |||
| 343 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 344 | + | registerProtocol(t, r.xc, lock) | |||
| 345 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 346 | + | t.Fatal(err) | |||
| 347 | + | } | |||
| 348 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |||
| 349 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 350 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock { | |||
| 351 | + | t.Fatalf("constrained %+v", got) | |||
| 352 | + | } | |||
| 353 | + | } | |||
M internal/adapters/wayland/xdg.go +32 -21
| @@ -128,10 +128,12 @@ func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | |||||
| 128 | 128 | ||||
| 129 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | 129 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | ||
| 130 | type window struct { | 130 | type window struct { | ||
| 131 | - | id ports.WindowID | 131 | + | id ports.WindowID |
| 132 | - | toplevel *xdgshell.Toplevel | 132 | + | toplevel *xdgshell.Toplevel |
| 133 | - | popup *popup | 133 | + | popup *popup |
| 134 | - | xdg *xdgSurface | 134 | + | xdg *xdgSurface |
| 135 | + | // x11 is set for the window of an X11 client (x11window.go). | |||
| 136 | + | x11 *x11Window | |||
| 135 | appID, title string | 137 | appID, title string | ||
| 136 | mapped bool | 138 | mapped bool | ||
| 137 | // gen counts the mappings: an unmap and a remap is a new toplevel to | 139 | // gen counts the mappings: an unmap and a remap is a new toplevel to | ||
| @@ -151,8 +153,8 @@ type window struct { | |||||
| 151 | hasLast bool | 153 | hasLast bool | ||
| 152 | } | 154 | } | ||
| 153 | 155 | ||||
| 154 | - | // sendConfigure sends w.last as xdg_toplevel + xdg_surface configure. | 156 | + | // sendXDGConfigure sends w.last as xdg_toplevel + xdg_surface configure. |
| 155 | - | func (w *window) sendConfigure() { | 157 | + | func (w *window) sendXDGConfigure() { |
| 156 | c := w.last | 158 | c := w.last | ||
| 157 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version())) | 159 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version())) | ||
| 158 | s := w.xdg.server | 160 | s := w.xdg.server | ||
| @@ -192,24 +194,29 @@ func toplevelStates(c ports.ConfigureWindow, version int32) []byte { | |||||
| 192 | } | 194 | } | ||
| 193 | 195 | ||||
| 194 | func (w *window) unmap() { | 196 | func (w *window) unmap() { | ||
| 195 | - | if w.xdg.server.seat.pointerFocus == w.id { | 197 | + | s := w.srv() |
| 196 | - | w.xdg.server.changePointerFocus(0, 0, 0) | 198 | + | if s.seat.pointerFocus == w.id { |
| 199 | + | s.changePointerFocus(0, 0, 0) | |||
| 197 | } | 200 | } | ||
| 198 | - | if w.xdg.server.seat.focused == w.id { | 201 | + | if s.seat.focused == w.id { |
| 199 | - | w.xdg.server.changeFocus(0) | 202 | + | s.changeFocus(0) |
| 200 | } | 203 | } | ||
| 201 | if !w.mapped { | 204 | if !w.mapped { | ||
| 202 | return | 205 | return | ||
| 203 | } | 206 | } | ||
| 204 | w.mapped = false | 207 | w.mapped = false | ||
| 205 | - | w.xdg.server.toplevelClosed(w.id) | 208 | + | s.toplevelClosed(w.id) |
| 206 | - | w.xdg.surface.resetInputEmission() | 209 | + | if surf := w.surf(); surf != nil { |
| 207 | - | w.xdg.configured = false | 210 | + | surf.resetInputEmission() |
| 208 | - | w.xdg.acked = false | 211 | + | } |
| 209 | - | w.xdg.serials = nil | 212 | + | if x := w.xdg; x != nil { |
| 210 | - | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | 213 | + | x.configured = false |
| 211 | - | w.xdg.server.syncInhibitors() | 214 | + | x.acked = false |
| 212 | - | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | 215 | + | x.serials = nil |
| 216 | + | } | |||
| 217 | + | s.emit(ports.WindowUnmapped{ID: w.id}) | |||
| 218 | + | s.syncInhibitors() | |||
| 219 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |||
| 213 | } | 220 | } | ||
| 214 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 221 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| 215 | if x.window != nil { | 222 | if x.window != nil { | ||
| @@ -337,7 +344,7 @@ func (w *window) setParent(p *window) { | |||||
| 337 | if p != nil && p.mapped { | 344 | if p != nil && p.mapped { | ||
| 338 | id = p.id | 345 | id = p.id | ||
| 339 | } | 346 | } | ||
| 340 | - | w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id}) | 347 | + | w.srv().emit(ports.WindowParent{ID: w.id, Parent: id}) |
| 341 | } | 348 | } | ||
| 342 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | 349 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | ||
| 343 | if title == t.w.title { | 350 | if title == t.w.title { | ||
| @@ -417,7 +424,7 @@ func (w *window) afterCommit() { | |||||
| 417 | } | 424 | } | ||
| 418 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | 425 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | ||
| 419 | w.floatW, w.floatH = fw, fh | 426 | w.floatW, w.floatH = fw, fh | ||
| 420 | - | w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) | 427 | + | w.srv().emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) |
| 421 | } | 428 | } | ||
| 422 | } | 429 | } | ||
| 423 | 430 | ||||
| @@ -431,6 +438,10 @@ func (w *window) floats() bool { | |||||
| 431 | 438 | ||||
| 432 | // size is the window geometry size, else the surface size, in logical pixels. | 439 | // size is the window geometry size, else the surface size, in logical pixels. | ||
| 433 | func (w *window) size() (int, int) { | 440 | func (w *window) size() (int, int) { | ||
| 441 | + | if w.xdg == nil { | |||
| 442 | + | c := w.surf().content | |||
| 443 | + | return c.LogicalW, c.LogicalH | |||
| 444 | + | } | |||
| 434 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | 445 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | ||
| 435 | return g.W, g.H | 446 | return g.W, g.H | ||
| 436 | } | 447 | } | ||
| @@ -441,7 +452,7 @@ func (w *window) size() (int, int) { | |||||
| 441 | // surfacePoint turns window coordinates (core's, from the geometry origin) | 452 | // surfacePoint turns window coordinates (core's, from the geometry origin) | ||
| 442 | // into surface coordinates. | 453 | // into surface coordinates. | ||
| 443 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | 454 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | ||
| 444 | - | g := w.xdg.geometry | 455 | + | g := w.geometry() |
| 445 | return x + float64(g.X), y + float64(g.Y) | 456 | return x + float64(g.X), y + float64(g.Y) | ||
| 446 | } | 457 | } | ||
| 447 | 458 | ||||
A internal/adapters/wayland/xdgoutputx_test.go +143 -0
| @@ -0,0 +1,143 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | "github.com/bnema/wlturbo" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // xdgPosProxy records the logical_position events of one zxdg_output_v1. | |||
| 13 | + | type xdgPosProxy struct { | |||
| 14 | + | wlturbo.BaseProxy | |||
| 15 | + | pos [][2]int32 | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func (p *xdgPosProxy) Dispatch(e *wlturbo.Event) { | |||
| 19 | + | if e.Opcode == 0 { | |||
| 20 | + | p.pos = append(p.pos, [2]int32{e.Int32(), e.Int32()}) | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // xdgOutputsOf binds the xdg-output manager and an xdg-output for every | |||
| 25 | + | // wl_output of c, and returns what each one reported. | |||
| 26 | + | func xdgOutputsOf(t *testing.T, c *wlturbo.Display) []*xdgPosProxy { | |||
| 27 | + | t.Helper() | |||
| 28 | + | if err := c.Roundtrip(); err != nil { | |||
| 29 | + | t.Fatal(err) | |||
| 30 | + | } | |||
| 31 | + | var mgrs int | |||
| 32 | + | var manager uint32 | |||
| 33 | + | for _, g := range c.Registry().GetGlobals() { | |||
| 34 | + | if g.Interface == "zxdg_output_manager_v1" { | |||
| 35 | + | mgrs++ | |||
| 36 | + | id, err := bindWireID(c, g.Name, g.Interface, 3) | |||
| 37 | + | if err != nil { | |||
| 38 | + | t.Fatal(err) | |||
| 39 | + | } | |||
| 40 | + | registerProtocol(t, c, id) | |||
| 41 | + | manager = id | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | if mgrs != 1 { | |||
| 45 | + | t.Fatalf("client sees %d zxdg_output_manager_v1 globals, want 1", mgrs) | |||
| 46 | + | } | |||
| 47 | + | var out []*xdgPosProxy | |||
| 48 | + | for _, g := range c.Registry().GetGlobals() { | |||
| 49 | + | if g.Interface != "wl_output" { | |||
| 50 | + | continue | |||
| 51 | + | } | |||
| 52 | + | wo, err := bindWireID(c, g.Name, g.Interface, 4) | |||
| 53 | + | if err != nil { | |||
| 54 | + | t.Fatal(err) | |||
| 55 | + | } | |||
| 56 | + | xp := &xdgPosProxy{} | |||
| 57 | + | xp.SetID(c.AllocateID()) | |||
| 58 | + | registerWireProxy(c, xp) | |||
| 59 | + | requestProtocol(t, c, manager, 1, xp.ID(), wo) | |||
| 60 | + | out = append(out, xp) | |||
| 61 | + | } | |||
| 62 | + | if err := c.Roundtrip(); err != nil { | |||
| 63 | + | t.Fatal(err) | |||
| 64 | + | } | |||
| 65 | + | return out | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func lastPositions(ps []*xdgPosProxy) [][2]int32 { | |||
| 69 | + | var r [][2]int32 | |||
| 70 | + | for _, p := range ps { | |||
| 71 | + | r = append(r, p.pos[len(p.pos)-1]) | |||
| 72 | + | } | |||
| 73 | + | slices.SortFunc(r, func(a, b [2]int32) int { return int(a[0] - b[0]) }) | |||
| 74 | + | return r | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func twoOutputs(ax int) ports.SetOutputs { | |||
| 78 | + | mk := func(name string, x int) ports.OutputPlacement { | |||
| 79 | + | return ports.OutputPlacement{Info: ports.OutputInfo{Name: name, Width: 1920, Height: 1080}, X: x, Y: 0, Width: 1920, Height: 1080, Scale: 1} | |||
| 80 | + | } | |||
| 81 | + | return ports.SetOutputs{Outputs: ports.Layout{mk("A", ax), mk("B", 0)}} | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // Xwayland gets X-space positions (layout top-left is 0,0) and re-receives | |||
| 85 | + | // them on a layout change; every other client keeps the layout coordinates. | |||
| 86 | + | func TestXwaylandPrivateXDGOutput(t *testing.T) { | |||
| 87 | + | r := newX11Rig(t) | |||
| 88 | + | applyOutputs(t, r.s, r.commands, twoOutputs(-1920)) | |||
| 89 | + | other := protocolClient(t, r.s, r.dir) | |||
| 90 | + | xo := xdgOutputsOf(t, r.xc) | |||
| 91 | + | oo := xdgOutputsOf(t, other) | |||
| 92 | + | if got, want := lastPositions(xo), [][2]int32{{0, 0}, {1920, 0}}; !slices.Equal(got, want) { | |||
| 93 | + | t.Errorf("Xwayland positions %v, want %v", got, want) | |||
| 94 | + | } | |||
| 95 | + | if got, want := lastPositions(oo), [][2]int32{{-1920, 0}, {0, 0}}; !slices.Equal(got, want) { | |||
| 96 | + | t.Errorf("ordinary positions %v, want %v", got, want) | |||
| 97 | + | } | |||
| 98 | + | applyOutputs(t, r.s, r.commands, twoOutputs(-3840)) | |||
| 99 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 100 | + | t.Fatal(err) | |||
| 101 | + | } | |||
| 102 | + | if err := other.Roundtrip(); err != nil { | |||
| 103 | + | t.Fatal(err) | |||
| 104 | + | } | |||
| 105 | + | if got, want := lastPositions(xo), [][2]int32{{0, 0}, {3840, 0}}; !slices.Equal(got, want) { | |||
| 106 | + | t.Errorf("Xwayland positions after change %v, want %v", got, want) | |||
| 107 | + | } | |||
| 108 | + | if got, want := lastPositions(oo), [][2]int32{{-3840, 0}, {0, 0}}; !slices.Equal(got, want) { | |||
| 109 | + | t.Errorf("ordinary positions after change %v, want %v", got, want) | |||
| 110 | + | } | |||
| 111 | + | // Hotplug: a third output on the right grows the extents. | |||
| 112 | + | third := twoOutputs(-3840) | |||
| 113 | + | third.Outputs = append(third.Outputs, ports.OutputPlacement{Info: ports.OutputInfo{Name: "C", Width: 1920, Height: 1080}, X: 1920, Width: 1920, Height: 1080, Scale: 1}) | |||
| 114 | + | applyOutputs(t, r.s, r.commands, third) | |||
| 115 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 116 | + | t.Fatal(err) | |||
| 117 | + | } | |||
| 118 | + | if got := lastPositions(xo); len(got) != 2 || got[1] != [2]int32{3840, 0} { | |||
| 119 | + | t.Errorf("Xwayland positions after hotplug %v", got) | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func TestGlobalFilterAllocations(t *testing.T) { | |||
| 124 | + | if raceEnabled { | |||
| 125 | + | t.Skip("the race detector allocates in the background") | |||
| 126 | + | } | |||
| 127 | + | s := &Server{xdgPublic: &server.Global{}, xdgPrivate: &server.Global{}} | |||
| 128 | + | if allocs := testing.AllocsPerRun(100, func() { | |||
| 129 | + | s.globalVisible(server.Client{}, s.xdgPrivate) | |||
| 130 | + | s.globalVisible(server.Client{}, s.xdgPublic) | |||
| 131 | + | }); allocs != 0 { | |||
| 132 | + | t.Errorf("globalVisible: %.1f allocs, want 0", allocs) | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | // The X origin is derived by one Space, kept without a window manager. | |||
| 137 | + | func TestXSpaceFollowsLayoutWithoutXWM(t *testing.T) { | |||
| 138 | + | r := newX11Rig(t) | |||
| 139 | + | applyOutputs(t, r.s, r.commands, twoOutputs(-1920)) | |||
| 140 | + | if ox, oy := r.s.xspace.Origin(); ox != -1920 || oy != 0 { | |||
| 141 | + | t.Fatalf("origin %d,%d, want -1920,0", ox, oy) | |||
| 142 | + | } | |||
| 143 | + | } | |||
A internal/adapters/wayland/xwayland.go +75 -0
| @@ -0,0 +1,75 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |||
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // xwaylandClient belongs to the display goroutine. | |||
| 13 | + | type xwaylandClient struct { | |||
| 14 | + | client server.Client | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // AttachXwayland transfers ownership of the Wayland descriptor to the display. | |||
| 18 | + | // It may be called from any goroutine. | |||
| 19 | + | func (s *Server) AttachXwayland(wlFD int) { | |||
| 20 | + | ran := s.display.Do(func() { | |||
| 21 | + | c, err := s.display.CreateClient(wlFD) | |||
| 22 | + | if err != nil { | |||
| 23 | + | s.log.Warn().Err(err).Msg("Xwayland connection not adopted") | |||
| 24 | + | _ = unix.Close(wlFD) // CreateClient only adopts on success. | |||
| 25 | + | return | |||
| 26 | + | } | |||
| 27 | + | s.forgetXwayland() | |||
| 28 | + | s.xwayland = xwaylandClient{client: c} | |||
| 29 | + | c.OnDestroy(func() { | |||
| 30 | + | if s.xwayland.client == c { | |||
| 31 | + | s.teardownXWM() | |||
| 32 | + | s.forgetXwayland() | |||
| 33 | + | } | |||
| 34 | + | }) | |||
| 35 | + | s.log.Info().Msg("Xwayland attached") | |||
| 36 | + | }) | |||
| 37 | + | if !ran { | |||
| 38 | + | _ = unix.Close(wlFD) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // forgetXwayland runs on the display owner, or after Run has joined it. | |||
| 43 | + | func (s *Server) forgetXwayland() { | |||
| 44 | + | if s.xwayland.client != (server.Client{}) { | |||
| 45 | + | s.xwayland = xwaylandClient{} | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // AttachXWM bootstraps the X11 window manager on the window manager | |||
| 50 | + | // descriptor, which it owns. It runs on the starter goroutine once Xwayland is | |||
| 51 | + | // ready and blocks until the XWM is installed, ctx ends or Boot fails. stop | |||
| 52 | + | // closes the XWM on the display goroutine and waits for it. | |||
| 53 | + | func (s *Server) AttachXWM(ctx context.Context, wmFD int) (stop func()) { | |||
| 54 | + | log := logging.For(ctx, "xwm") | |||
| 55 | + | x, err := xwm.Boot(ctx, wmFD, log) | |||
| 56 | + | if err != nil { | |||
| 57 | + | if ctx.Err() != nil { | |||
| 58 | + | log.Info().Err(err).Msg("X11 window management not started: Xwayland is going away") | |||
| 59 | + | } else { | |||
| 60 | + | log.Warn().Err(err).Msg("X11 window management disabled") | |||
| 61 | + | } | |||
| 62 | + | return func() {} | |||
| 63 | + | } | |||
| 64 | + | if !s.display.Do(func() { s.installXWM(x) }) { | |||
| 65 | + | x.CloseConn() | |||
| 66 | + | return func() {} | |||
| 67 | + | } | |||
| 68 | + | return func() { | |||
| 69 | + | s.display.Do(func() { | |||
| 70 | + | if s.xwm == x { | |||
| 71 | + | s.teardownXWM() | |||
| 72 | + | } | |||
| 73 | + | }) | |||
| 74 | + | } | |||
| 75 | + | } | |||
A internal/adapters/wayland/xwayland_test.go +84 -0
| @@ -0,0 +1,84 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/purego-libwayland/server" | |||
| 8 | + | "golang.org/x/sys/unix" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func xwaylandPair(t *testing.T) *[2]int { | |||
| 12 | + | t.Helper() | |||
| 13 | + | p, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 14 | + | if err != nil { | |||
| 15 | + | t.Fatal(err) | |||
| 16 | + | } | |||
| 17 | + | t.Cleanup(func() { | |||
| 18 | + | if p[1] >= 0 { | |||
| 19 | + | unix.Close(p[1]) | |||
| 20 | + | } | |||
| 21 | + | }) | |||
| 22 | + | return &p | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | func assertFDClosed(t *testing.T, fd int) { | |||
| 26 | + | t.Helper() | |||
| 27 | + | if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) { | |||
| 28 | + | t.Fatalf("fd %d still open: %v", fd, err) | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func TestAttachXwaylandLifecycle(t *testing.T) { | |||
| 33 | + | h := newAdmissionHarness(t, 1) | |||
| 34 | + | wl := xwaylandPair(t) | |||
| 35 | + | h.s.AttachXwayland(wl[0]) | |||
| 36 | + | var first server.Client | |||
| 37 | + | h.s.display.Do(func() { first = h.s.xwayland.client }) | |||
| 38 | + | if first == (server.Client{}) { | |||
| 39 | + | t.Fatal("client not recorded") | |||
| 40 | + | } | |||
| 41 | + | wl2 := xwaylandPair(t) | |||
| 42 | + | h.s.AttachXwayland(wl2[0]) | |||
| 43 | + | unix.Close(wl[1]) | |||
| 44 | + | wl[1] = -1 // Old client's destroy must not clear the new record. | |||
| 45 | + | waitTrue(t, "old client destroyed", func() bool { | |||
| 46 | + | _, err := unix.FcntlInt(uintptr(wl[0]), unix.F_GETFD, 0) | |||
| 47 | + | return errors.Is(err, unix.EBADF) | |||
| 48 | + | }) | |||
| 49 | + | h.s.display.Do(func() { | |||
| 50 | + | if h.s.xwayland.client == (server.Client{}) || h.s.xwayland.client == first { | |||
| 51 | + | t.Error("replacement lost") | |||
| 52 | + | } | |||
| 53 | + | }) | |||
| 54 | + | unix.Close(wl2[1]) | |||
| 55 | + | wl2[1] = -1 | |||
| 56 | + | waitTrue(t, "Xwayland disconnected", func() bool { | |||
| 57 | + | clear := false | |||
| 58 | + | h.s.display.Do(func() { clear = h.s.xwayland.client == (server.Client{}) }) | |||
| 59 | + | return clear | |||
| 60 | + | }) | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func TestAttachXwaylandFailureClosesFDs(t *testing.T) { | |||
| 64 | + | h := newAdmissionHarness(t, 1) | |||
| 65 | + | var p [2]int | |||
| 66 | + | if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil { | |||
| 67 | + | t.Fatal(err) | |||
| 68 | + | } | |||
| 69 | + | defer unix.Close(p[1]) | |||
| 70 | + | h.s.AttachXwayland(p[0]) | |||
| 71 | + | assertFDClosed(t, p[0]) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func TestAttachXwaylandStopped(t *testing.T) { | |||
| 75 | + | d, err := server.NewDisplay() | |||
| 76 | + | if err != nil { | |||
| 77 | + | t.Fatal(err) | |||
| 78 | + | } | |||
| 79 | + | d.Close() | |||
| 80 | + | s := &Server{display: d} | |||
| 81 | + | wl := xwaylandPair(t) | |||
| 82 | + | s.AttachXwayland(wl[0]) | |||
| 83 | + | assertFDClosed(t, wl[0]) | |||
| 84 | + | } | |||
A internal/adapters/wayland/xwm/atoms.go +96 -0
| @@ -0,0 +1,96 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | // Atoms interned at boot in one round trip, in this fixed order. The table | |||
| 4 | + | // includes the clipboard and selection atoms. | |||
| 5 | + | const ( | |||
| 6 | + | atomWMProtocols = iota | |||
| 7 | + | atomWMDeleteWindow | |||
| 8 | + | atomWMTakeFocus | |||
| 9 | + | atomWMState | |||
| 10 | + | atomWMChangeState | |||
| 11 | + | atomWMS0 | |||
| 12 | + | atomUTF8String | |||
| 13 | + | atomNetWMCMS0 | |||
| 14 | + | atomNetSupported | |||
| 15 | + | atomNetSupportingWMCheck | |||
| 16 | + | atomNetWMName | |||
| 17 | + | atomNetWMPID | |||
| 18 | + | atomNetWMWindowType | |||
| 19 | + | atomNetWMWindowTypeNormal | |||
| 20 | + | atomNetWMWindowTypeDialog | |||
| 21 | + | atomNetWMWindowTypeUtility | |||
| 22 | + | atomNetWMWindowTypeSplash | |||
| 23 | + | atomNetWMWindowTypeToolbar | |||
| 24 | + | atomNetWMWindowTypeMenu | |||
| 25 | + | atomNetWMWindowTypeDropdownMenu | |||
| 26 | + | atomNetWMWindowTypePopupMenu | |||
| 27 | + | atomNetWMWindowTypeTooltip | |||
| 28 | + | atomNetWMWindowTypeNotification | |||
| 29 | + | atomNetWMWindowTypeCombo | |||
| 30 | + | atomNetWMWindowTypeDND | |||
| 31 | + | atomNetWMState | |||
| 32 | + | atomNetWMStateModal | |||
| 33 | + | atomNetWMStateFullscreen | |||
| 34 | + | atomNetWMStateFocused | |||
| 35 | + | atomNetWMStateMaximizedVert | |||
| 36 | + | atomNetWMStateMaximizedHorz | |||
| 37 | + | atomNetActiveWindow | |||
| 38 | + | atomNetClientList | |||
| 39 | + | atomNetClientListStacking | |||
| 40 | + | atomNetWMMoveresize | |||
| 41 | + | atomNetWMBypassCompositor | |||
| 42 | + | atomWLSurfaceID | |||
| 43 | + | atomWLSurfaceSerial | |||
| 44 | + | atomNetWMIcon | |||
| 45 | + | atomMotifWMHints | |||
| 46 | + | atomClipboard | |||
| 47 | + | atomPrimary | |||
| 48 | + | atomTargets | |||
| 49 | + | atomCount | |||
| 50 | + | ) | |||
| 51 | + | ||||
| 52 | + | var atomNames = [atomCount]string{ | |||
| 53 | + | atomWMProtocols: "WM_PROTOCOLS", | |||
| 54 | + | atomWMDeleteWindow: "WM_DELETE_WINDOW", | |||
| 55 | + | atomWMTakeFocus: "WM_TAKE_FOCUS", | |||
| 56 | + | atomWMState: "WM_STATE", | |||
| 57 | + | atomWMChangeState: "WM_CHANGE_STATE", | |||
| 58 | + | atomWMS0: "WM_S0", | |||
| 59 | + | atomUTF8String: "UTF8_STRING", | |||
| 60 | + | atomNetWMCMS0: "_NET_WM_CM_S0", | |||
| 61 | + | atomNetSupported: "_NET_SUPPORTED", | |||
| 62 | + | atomNetSupportingWMCheck: "_NET_SUPPORTING_WM_CHECK", | |||
| 63 | + | atomNetWMName: "_NET_WM_NAME", | |||
| 64 | + | atomNetWMPID: "_NET_WM_PID", | |||
| 65 | + | atomNetWMWindowType: "_NET_WM_WINDOW_TYPE", | |||
| 66 | + | atomNetWMWindowTypeNormal: "_NET_WM_WINDOW_TYPE_NORMAL", | |||
| 67 | + | atomNetWMWindowTypeDialog: "_NET_WM_WINDOW_TYPE_DIALOG", | |||
| 68 | + | atomNetWMWindowTypeUtility: "_NET_WM_WINDOW_TYPE_UTILITY", | |||
| 69 | + | atomNetWMWindowTypeSplash: "_NET_WM_WINDOW_TYPE_SPLASH", | |||
| 70 | + | atomNetWMWindowTypeToolbar: "_NET_WM_WINDOW_TYPE_TOOLBAR", | |||
| 71 | + | atomNetWMWindowTypeMenu: "_NET_WM_WINDOW_TYPE_MENU", | |||
| 72 | + | atomNetWMWindowTypeDropdownMenu: "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", | |||
| 73 | + | atomNetWMWindowTypePopupMenu: "_NET_WM_WINDOW_TYPE_POPUP_MENU", | |||
| 74 | + | atomNetWMWindowTypeTooltip: "_NET_WM_WINDOW_TYPE_TOOLTIP", | |||
| 75 | + | atomNetWMWindowTypeNotification: "_NET_WM_WINDOW_TYPE_NOTIFICATION", | |||
| 76 | + | atomNetWMWindowTypeCombo: "_NET_WM_WINDOW_TYPE_COMBO", | |||
| 77 | + | atomNetWMWindowTypeDND: "_NET_WM_WINDOW_TYPE_DND", | |||
| 78 | + | atomNetWMState: "_NET_WM_STATE", | |||
| 79 | + | atomNetWMStateModal: "_NET_WM_STATE_MODAL", | |||
| 80 | + | atomNetWMStateFullscreen: "_NET_WM_STATE_FULLSCREEN", | |||
| 81 | + | atomNetWMStateFocused: "_NET_WM_STATE_FOCUSED", | |||
| 82 | + | atomNetWMStateMaximizedVert: "_NET_WM_STATE_MAXIMIZED_VERT", | |||
| 83 | + | atomNetWMStateMaximizedHorz: "_NET_WM_STATE_MAXIMIZED_HORZ", | |||
| 84 | + | atomNetActiveWindow: "_NET_ACTIVE_WINDOW", | |||
| 85 | + | atomNetClientList: "_NET_CLIENT_LIST", | |||
| 86 | + | atomNetClientListStacking: "_NET_CLIENT_LIST_STACKING", | |||
| 87 | + | atomNetWMMoveresize: "_NET_WM_MOVERESIZE", | |||
| 88 | + | atomNetWMBypassCompositor: "_NET_WM_BYPASS_COMPOSITOR", | |||
| 89 | + | atomWLSurfaceID: "WL_SURFACE_ID", | |||
| 90 | + | atomWLSurfaceSerial: "WL_SURFACE_SERIAL", | |||
| 91 | + | atomNetWMIcon: "_NET_WM_ICON", | |||
| 92 | + | atomMotifWMHints: "_MOTIF_WM_HINTS", | |||
| 93 | + | atomClipboard: "CLIPBOARD", | |||
| 94 | + | atomPrimary: "PRIMARY", | |||
| 95 | + | atomTargets: "TARGETS", | |||
| 96 | + | } | |||
A internal/adapters/wayland/xwm/configure.go +317 -0
| @@ -0,0 +1,317 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "math" | |||
| 5 | + | ||||
| 6 | + | "bnema.dev/xturbo" | |||
| 7 | + | "bnema.dev/xturbo/xproto" | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // maxClientSize bounds the size a floating window may ask for. | |||
| 12 | + | const maxClientSize = 16384 | |||
| 13 | + | ||||
| 14 | + | // _NET_WM_STATE bits tracked in record.stateSent. | |||
| 15 | + | const ( | |||
| 16 | + | stateFullscreen uint8 = 1 << iota | |||
| 17 | + | stateFocused | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | // size is the size to send: the requested one, else the last known. | |||
| 21 | + | func (r *record) size() (w, h uint32) { | |||
| 22 | + | w, h = r.pend.w, r.pend.h | |||
| 23 | + | if w == 0 { | |||
| 24 | + | w = r.w | |||
| 25 | + | } | |||
| 26 | + | if h == 0 { | |||
| 27 | + | h = r.h | |||
| 28 | + | } | |||
| 29 | + | return w, h | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | // differs reports whether the pending configure is not what the X server has. | |||
| 33 | + | func (r *record) differs() bool { | |||
| 34 | + | w, h := r.size() | |||
| 35 | + | p, s := &r.pend, &r.sent | |||
| 36 | + | return p.x != s.x || p.y != s.y || w != s.w || h != s.h || | |||
| 37 | + | p.fullscreen != s.fullscreen || p.activated != s.activated | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // markDirty queues s for the next flush when its pending configure differs. | |||
| 41 | + | func (x *XWM) markDirty(s Slot, r *record) { | |||
| 42 | + | if r.pend.dirty || !r.differs() { | |||
| 43 | + | return | |||
| 44 | + | } | |||
| 45 | + | if len(x.dirty) == cap(x.dirty) { | |||
| 46 | + | // Stale entries of freed and reused slots filled the queue: send | |||
| 47 | + | // what is queued rather than grow it. r stays valid, nothing here | |||
| 48 | + | // grows the table. | |||
| 49 | + | x.flushConfigures() | |||
| 50 | + | } | |||
| 51 | + | r.pend.dirty = true | |||
| 52 | + | x.dirty = append(x.dirty, s) | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // managedRec returns the record of a live managed window. | |||
| 56 | + | func (x *XWM) managedRec(s Slot) *record { | |||
| 57 | + | if x.closed { | |||
| 58 | + | return nil | |||
| 59 | + | } | |||
| 60 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged { | |||
| 61 | + | return r | |||
| 62 | + | } | |||
| 63 | + | return nil | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // Configure records the size and state core wants for s; sent at flush. A | |||
| 67 | + | // zero size keeps the current one. | |||
| 68 | + | func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) { | |||
| 69 | + | r := x.managedRec(s) | |||
| 70 | + | if r == nil { | |||
| 71 | + | return | |||
| 72 | + | } | |||
| 73 | + | if c.Width > 0 { | |||
| 74 | + | r.pend.w = uint32(min(c.Width, maxClientSize)) | |||
| 75 | + | } | |||
| 76 | + | if c.Height > 0 { | |||
| 77 | + | r.pend.h = uint32(min(c.Height, maxClientSize)) | |||
| 78 | + | } | |||
| 79 | + | r.pend.fullscreen, r.pend.activated = c.Fullscreen, c.Activated | |||
| 80 | + | x.markDirty(s, r) | |||
| 81 | + | x.wake() | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // Place records the layout position of the client rectangle of s. | |||
| 85 | + | func (x *XWM) Place(s Slot, px, py int) { | |||
| 86 | + | r := x.managedRec(s) | |||
| 87 | + | if r == nil { | |||
| 88 | + | return | |||
| 89 | + | } | |||
| 90 | + | r.lx, r.ly, r.placed = int32(px), int32(py), true | |||
| 91 | + | r.pend.x, r.pend.y = x.toX(r) | |||
| 92 | + | x.markDirty(s, r) | |||
| 93 | + | x.wake() | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // toX converts the layout position of r to X coordinates. | |||
| 97 | + | func (x *XWM) toX(r *record) (px, py int32) { | |||
| 98 | + | ix, iy := x.space.ToX(int(r.lx), int(r.ly)) | |||
| 99 | + | return int32(ix), int32(iy) | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // SetLayout installs the output layout that X coordinates derive from. When it | |||
| 103 | + | // changes, placed managed windows are repositioned and mapped override windows | |||
| 104 | + | // are re-reported in layout coordinates. Display goroutine only. | |||
| 105 | + | func (x *XWM) SetLayout(l ports.Layout) { | |||
| 106 | + | if x.closed { | |||
| 107 | + | return | |||
| 108 | + | } | |||
| 109 | + | x.LayoutChanged(x.space.Set(l)) | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // UseSpace makes sp the coordinate mapping, owned by the host, which calls | |||
| 113 | + | // Set on it and then LayoutChanged. Call before Attach. | |||
| 114 | + | func (x *XWM) UseSpace(sp *Space) { x.space = sp } | |||
| 115 | + | ||||
| 116 | + | // LayoutChanged repositions windows after the shared Space was set; changed | |||
| 117 | + | // is Set's result. Display goroutine only. | |||
| 118 | + | func (x *XWM) LayoutChanged(changed bool) { | |||
| 119 | + | if x.closed { | |||
| 120 | + | return | |||
| 121 | + | } | |||
| 122 | + | if n := x.space.Dropped(); n > 0 && !x.warnedSpace { | |||
| 123 | + | x.warnedSpace = true | |||
| 124 | + | x.log.Warn().Int("dropped", n).Int("max", maxRegions).Msg("more outputs than the X11 layout holds, extra outputs ignored") | |||
| 125 | + | } | |||
| 126 | + | if !changed { | |||
| 127 | + | return | |||
| 128 | + | } | |||
| 129 | + | for i := range x.tab.recs { | |||
| 130 | + | r, s := &x.tab.recs[i], Slot(i+1) | |||
| 131 | + | switch { | |||
| 132 | + | case r.kind == kindManaged && r.placed: | |||
| 133 | + | r.pend.x, r.pend.y = x.toX(r) | |||
| 134 | + | x.markDirty(s, r) | |||
| 135 | + | case r.kind == kindUnmanaged && r.ovPaired: | |||
| 136 | + | x.queueOverride(s, r) | |||
| 137 | + | } | |||
| 138 | + | } | |||
| 139 | + | x.wake() | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | // flushConfigures sends the pending configures, once per display iteration, | |||
| 143 | + | // then what focus left to do. | |||
| 144 | + | func (x *XWM) flushConfigures() { | |||
| 145 | + | for _, s := range x.dirty { | |||
| 146 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged && r.pend.dirty { | |||
| 147 | + | x.sendPending(s, r) | |||
| 148 | + | } | |||
| 149 | + | } | |||
| 150 | + | x.dirty = x.dirty[:0] | |||
| 151 | + | x.flushOverrides() | |||
| 152 | + | x.flushFocus() | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | // sendPending writes the geometry (with the synthetic ConfigureNotify ICCCM | |||
| 156 | + | // 4.1.5 asks for, since Xwayland may not move the window) and the | |||
| 157 | + | // _NET_WM_STATE bits that changed. | |||
| 158 | + | func (x *XWM) sendPending(s Slot, r *record) { | |||
| 159 | + | r.pend.dirty = false | |||
| 160 | + | p := &r.pend | |||
| 161 | + | w, h := r.size() | |||
| 162 | + | if w > 0 && h > 0 && (p.x != r.sent.x || p.y != r.sent.y || w != r.sent.w || h != r.sent.h) { | |||
| 163 | + | var v xproto.ConfigureWindowValues | |||
| 164 | + | v.SetX(p.x).SetY(p.y).SetWidth(w).SetHeight(h).SetBorderWidth(0) | |||
| 165 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0) | |||
| 166 | + | r.x, r.y, r.w, r.h = p.x, p.y, w, h | |||
| 167 | + | x.sendConfigureNotify(r) | |||
| 168 | + | r.sent.x, r.sent.y, r.sent.w, r.sent.h = p.x, p.y, w, h | |||
| 169 | + | } | |||
| 170 | + | var bits uint8 | |||
| 171 | + | if p.fullscreen { | |||
| 172 | + | bits |= stateFullscreen | |||
| 173 | + | } | |||
| 174 | + | if p.activated { | |||
| 175 | + | bits |= stateFocused | |||
| 176 | + | } | |||
| 177 | + | r.sent.fullscreen, r.sent.activated = p.fullscreen, p.activated | |||
| 178 | + | r.fullscreen = p.fullscreen // ours now: the property is not read back | |||
| 179 | + | if bits != r.stateSent { | |||
| 180 | + | r.stateSent = bits | |||
| 181 | + | x.writeState(r) | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // writeState rewrites _NET_WM_STATE of r from its bits. | |||
| 186 | + | func (x *XWM) writeState(r *record) { | |||
| 187 | + | var list [3]uint32 | |||
| 188 | + | n := 0 | |||
| 189 | + | if r.modal { | |||
| 190 | + | list[n] = x.atoms[atomNetWMStateModal] | |||
| 191 | + | n++ | |||
| 192 | + | } | |||
| 193 | + | if r.stateSent&stateFullscreen != 0 { | |||
| 194 | + | list[n] = x.atoms[atomNetWMStateFullscreen] | |||
| 195 | + | n++ | |||
| 196 | + | } | |||
| 197 | + | if r.stateSent&stateFocused != 0 { | |||
| 198 | + | list[n] = x.atoms[atomNetWMStateFocused] | |||
| 199 | + | n++ | |||
| 200 | + | } | |||
| 201 | + | x.setProp32(r.xid, atomNetWMState, xproto.AtomAtom, list[:n]) | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // sendConfigureNotify tells the client its geometry as the XWM sees it. | |||
| 205 | + | func (x *XWM) sendConfigureNotify(r *record) { | |||
| 206 | + | var raw [32]byte | |||
| 207 | + | d := xproto.ConfigureNotifyEventData{ | |||
| 208 | + | Event: xproto.Window(r.xid), Window: xproto.Window(r.xid), | |||
| 209 | + | X: int16(min(max(r.x, math.MinInt16), math.MaxInt16)), Y: int16(min(max(r.y, math.MinInt16), math.MaxInt16)), | |||
| 210 | + | Width: uint16(min(r.w, math.MaxUint16)), Height: uint16(min(r.h, math.MaxUint16)), | |||
| 211 | + | } | |||
| 212 | + | d.Put(&raw) | |||
| 213 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(r.xid), xproto.EventMaskStructureNotify, &raw) | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | // onConfigureRequest answers a client resizing itself. Tiled windows are told | |||
| 217 | + | // their current geometry; floating ones get the size they ask for; unmanaged | |||
| 218 | + | // and unknown windows are configured as requested. | |||
| 219 | + | func (x *XWM) onConfigureRequest(ev *xturbo.Event) { | |||
| 220 | + | e := xproto.ConfigureRequestOf(ev) | |||
| 221 | + | xid := uint32(e.Window()) | |||
| 222 | + | mask := e.ValueMask() | |||
| 223 | + | s := x.tab.lookup(xid) | |||
| 224 | + | r := x.managedRec(s) | |||
| 225 | + | if r == nil { | |||
| 226 | + | var v xproto.ConfigureWindowValues | |||
| 227 | + | if mask&xproto.ConfigWindowX != 0 { | |||
| 228 | + | v.SetX(int32(e.X())) | |||
| 229 | + | } | |||
| 230 | + | if mask&xproto.ConfigWindowY != 0 { | |||
| 231 | + | v.SetY(int32(e.Y())) | |||
| 232 | + | } | |||
| 233 | + | if mask&xproto.ConfigWindowWidth != 0 { | |||
| 234 | + | v.SetWidth(uint32(e.Width())) | |||
| 235 | + | } | |||
| 236 | + | if mask&xproto.ConfigWindowHeight != 0 { | |||
| 237 | + | v.SetHeight(uint32(e.Height())) | |||
| 238 | + | } | |||
| 239 | + | if mask&xproto.ConfigWindowBorderWidth != 0 { | |||
| 240 | + | v.SetBorderWidth(uint32(e.BorderWidth())) | |||
| 241 | + | } | |||
| 242 | + | if mask&xproto.ConfigWindowSibling != 0 { | |||
| 243 | + | v.SetSibling(e.Sibling()) | |||
| 244 | + | } | |||
| 245 | + | if mask&xproto.ConfigWindowStackMode != 0 { | |||
| 246 | + | v.SetStackMode(e.StackMode()) | |||
| 247 | + | } | |||
| 248 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v) | |||
| 249 | + | return | |||
| 250 | + | } | |||
| 251 | + | if !r.ready { | |||
| 252 | + | x.configureBeforeReady(s, r, e) | |||
| 253 | + | return | |||
| 254 | + | } | |||
| 255 | + | if r.floating && mask&(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 { | |||
| 256 | + | w, h := r.w, r.h | |||
| 257 | + | if mask&xproto.ConfigWindowWidth != 0 { | |||
| 258 | + | w = uint32(min(max(int(e.Width()), 1), maxClientSize)) | |||
| 259 | + | } | |||
| 260 | + | if mask&xproto.ConfigWindowHeight != 0 { | |||
| 261 | + | h = uint32(min(max(int(e.Height()), 1), maxClientSize)) | |||
| 262 | + | } | |||
| 263 | + | if w > 0 && h > 0 { | |||
| 264 | + | var v xproto.ConfigureWindowValues | |||
| 265 | + | v.SetWidth(w).SetHeight(h) | |||
| 266 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v), kindConfigure, x.tab.tag(s), 0) | |||
| 267 | + | r.w, r.h = w, h | |||
| 268 | + | // The size is the client's own: core learns it from the commit | |||
| 269 | + | // and Configure only resends it when core wants another. | |||
| 270 | + | r.pend.w, r.pend.h = w, h | |||
| 271 | + | r.sent.w, r.sent.h = w, h | |||
| 272 | + | } | |||
| 273 | + | } | |||
| 274 | + | x.sendConfigureNotify(r) | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | // configureBeforeReady honours the geometry a window asks for before it is | |||
| 278 | + | // ready (XMoveResize before XMapWindow), floating or not: core only sees the | |||
| 279 | + | // window at Ready and the client must find the geometry it set. | |||
| 280 | + | func (x *XWM) configureBeforeReady(s Slot, r *record, e xproto.ConfigureRequestEvent) { | |||
| 281 | + | mask := e.ValueMask() | |||
| 282 | + | var v xproto.ConfigureWindowValues | |||
| 283 | + | if mask&xproto.ConfigWindowX != 0 { | |||
| 284 | + | r.x = int32(e.X()) | |||
| 285 | + | v.SetX(r.x) | |||
| 286 | + | } | |||
| 287 | + | if mask&xproto.ConfigWindowY != 0 { | |||
| 288 | + | r.y = int32(e.Y()) | |||
| 289 | + | v.SetY(r.y) | |||
| 290 | + | } | |||
| 291 | + | if mask&xproto.ConfigWindowWidth != 0 { | |||
| 292 | + | r.w, r.sized = uint32(min(max(int(e.Width()), 1), maxClientSize)), true | |||
| 293 | + | v.SetWidth(r.w) | |||
| 294 | + | } | |||
| 295 | + | if mask&xproto.ConfigWindowHeight != 0 { | |||
| 296 | + | r.h, r.sized = uint32(min(max(int(e.Height()), 1), maxClientSize)), true | |||
| 297 | + | v.SetHeight(r.h) | |||
| 298 | + | } | |||
| 299 | + | if mask&(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 { | |||
| 300 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0) | |||
| 301 | + | } | |||
| 302 | + | x.sendConfigureNotify(r) | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | // onConfigureNotify keeps the known geometry of every top-level window. | |||
| 306 | + | func (x *XWM) onConfigureNotify(ev *xturbo.Event) { | |||
| 307 | + | e := xproto.ConfigureNotifyOf(ev) | |||
| 308 | + | if e.Event() != xproto.Window(x.root) { | |||
| 309 | + | return | |||
| 310 | + | } | |||
| 311 | + | if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree { | |||
| 312 | + | r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height()) | |||
| 313 | + | if r.kind == kindUnmanaged && r.ovPaired { | |||
| 314 | + | x.queueOverride(x.tab.lookup(r.xid), r) | |||
| 315 | + | } | |||
| 316 | + | } | |||
| 317 | + | } | |||
A internal/adapters/wayland/xwm/configure_test.go +565 -0
| @@ -0,0 +1,565 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "bnema.dev/xturbo/xproto" | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | "github.com/stretchr/testify/mock" | |||
| 10 | + | "github.com/stretchr/testify/require" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // newWin makes a ready managed window known to the XWM and the mocked host. | |||
| 14 | + | func newWin(t *testing.T, x *XWM, p *xpeer, h *mockhost, xid uint32) Slot { | |||
| 15 | + | t.Helper() | |||
| 16 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(xid)).Once() | |||
| 17 | + | p.createNotify(xid, false) | |||
| 18 | + | readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 }) | |||
| 19 | + | s := x.tab.lookup(xid) | |||
| 20 | + | r := x.tab.rec(s) | |||
| 21 | + | r.ready, r.w, r.h = true, 300, 200 | |||
| 22 | + | return s | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // settle flushes the XWM and waits until the peer has seen everything sent so | |||
| 26 | + | // far; it returns the number of requests received. | |||
| 27 | + | func settle(t *testing.T, x *XWM, p *xpeer) int { | |||
| 28 | + | t.Helper() | |||
| 29 | + | x.flush() | |||
| 30 | + | n := len(p.requests()) | |||
| 31 | + | xproto.GetInputFocus(x.conn.Buf()) | |||
| 32 | + | require.NoError(t, x.conn.Flush()) | |||
| 33 | + | _, n = p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |||
| 34 | + | return n | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // since returns the requests of the given major opcode received after skip. | |||
| 38 | + | func since(p *xpeer, skip int, major uint8) []xpeerReq { | |||
| 39 | + | var out []xpeerReq | |||
| 40 | + | for _, r := range p.requests()[skip:] { | |||
| 41 | + | if r.major == major { | |||
| 42 | + | out = append(out, r) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | return out | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // sentEvents returns the 32-byte events of the SendEvent requests to dest. | |||
| 49 | + | func sentEvents(p *xpeer, skip int, dest uint32) [][]byte { | |||
| 50 | + | var out [][]byte | |||
| 51 | + | for _, r := range since(p, skip, opSendEvent) { | |||
| 52 | + | if r.u32(4) == dest { | |||
| 53 | + | out = append(out, r.raw[12:44]) | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | return out | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | func propsTo(p *xpeer, skip int, win uint32, atom string) []xpeerReq { | |||
| 60 | + | var out []xpeerReq | |||
| 61 | + | a := p.atom(atom) | |||
| 62 | + | for _, r := range since(p, skip, opChangeProperty) { | |||
| 63 | + | if r.u32(4) == win && r.u32(8) == a { | |||
| 64 | + | out = append(out, r) | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | return out | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func (x *XWM) feed(t *testing.T, p *xpeer, send func()) { | |||
| 71 | + | t.Helper() | |||
| 72 | + | ev := x.stats.Events | |||
| 73 | + | send() | |||
| 74 | + | readUntil(t, x, func() bool { return x.stats.Events > ev }) | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func TestConfigureCoalescesIntoOneRequest(t *testing.T) { | |||
| 78 | + | x, p, h, _, runs := attached(t) | |||
| 79 | + | const win = 0x500001 | |||
| 80 | + | s := newWin(t, x, p, h, win) | |||
| 81 | + | n := settle(t, x, p) | |||
| 82 | + | ||||
| 83 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |||
| 84 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |||
| 85 | + | x.Place(s, 10, 20) | |||
| 86 | + | require.Len(t, *runs, 1, "one flush posted") | |||
| 87 | + | n2 := settle(t, x, p) | |||
| 88 | + | ||||
| 89 | + | cw := since(p, n, opConfigureWindow) | |||
| 90 | + | require.Len(t, cw, 1) | |||
| 91 | + | require.Equal(t, uint32(win), cw[0].u32(4)) | |||
| 92 | + | require.EqualValues(t, 0x1f, binary.LittleEndian.Uint16(cw[0].raw[8:]), "x y width height border") | |||
| 93 | + | require.Equal(t, []uint32{10, 20, 800, 600, 0}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24), cw[0].u32(28)}) | |||
| 94 | + | ||||
| 95 | + | evs := sentEvents(p, n, win) | |||
| 96 | + | require.Len(t, evs, 1, "one synthetic ConfigureNotify") | |||
| 97 | + | require.EqualValues(t, 22, evs[0][0]) | |||
| 98 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][4:])) | |||
| 99 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][8:])) | |||
| 100 | + | require.EqualValues(t, 10, binary.LittleEndian.Uint16(evs[0][16:])) | |||
| 101 | + | require.EqualValues(t, 20, binary.LittleEndian.Uint16(evs[0][18:])) | |||
| 102 | + | require.EqualValues(t, 800, binary.LittleEndian.Uint16(evs[0][20:])) | |||
| 103 | + | require.EqualValues(t, 600, binary.LittleEndian.Uint16(evs[0][22:])) | |||
| 104 | + | require.Equal(t, xproto.EventMaskStructureNotify, xproto.EventMask(since(p, n, opSendEvent)[0].u32(8))) | |||
| 105 | + | ||||
| 106 | + | // Unchanged: nothing is sent. | |||
| 107 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |||
| 108 | + | x.Place(s, 10, 20) | |||
| 109 | + | settle(t, x, p) | |||
| 110 | + | require.Len(t, since(p, n2, opConfigureWindow), 0) | |||
| 111 | + | require.Len(t, sentEvents(p, n2, win), 0) | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | func TestConfigureWritesNetWMState(t *testing.T) { | |||
| 115 | + | x, p, h, _, _ := attached(t) | |||
| 116 | + | const win = 0x500001 | |||
| 117 | + | s := newWin(t, x, p, h, win) | |||
| 118 | + | x.tab.rec(s).modal = true | |||
| 119 | + | n := settle(t, x, p) | |||
| 120 | + | ||||
| 121 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100}) | |||
| 122 | + | settle(t, x, p) | |||
| 123 | + | require.Empty(t, propsTo(p, n, win, "_NET_WM_STATE"), "no state bit changed") | |||
| 124 | + | ||||
| 125 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Fullscreen: true, Activated: true}) | |||
| 126 | + | settle(t, x, p) | |||
| 127 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |||
| 128 | + | require.True(t, ok) | |||
| 129 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FULLSCREEN"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |||
| 130 | + | ||||
| 131 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Activated: true}) | |||
| 132 | + | settle(t, x, p) | |||
| 133 | + | st, _ = p.prop(win, "_NET_WM_STATE") | |||
| 134 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | func TestConfigureBadWindowIsAttributedAndIgnored(t *testing.T) { | |||
| 138 | + | x, p, h, _, _ := attached(t) | |||
| 139 | + | const win = 0x500001 | |||
| 140 | + | s := newWin(t, x, p, h, win) | |||
| 141 | + | n := settle(t, x, p) | |||
| 142 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |||
| 143 | + | x.flush() | |||
| 144 | + | cw := []xpeerReq{} | |||
| 145 | + | r, _ := p.waitReq(n, func(r xpeerReq) bool { return r.major == opConfigureWindow }) | |||
| 146 | + | cw = append(cw, r) | |||
| 147 | + | ||||
| 148 | + | p.mu.Lock() | |||
| 149 | + | e := make([]byte, 32) | |||
| 150 | + | e[1] = xproto.BadWindow | |||
| 151 | + | binary.LittleEndian.PutUint16(e[2:], cw[0].seq) | |||
| 152 | + | binary.LittleEndian.PutUint32(e[4:], win) | |||
| 153 | + | e[10] = opConfigureWindow | |||
| 154 | + | p.write(e) | |||
| 155 | + | p.mu.Unlock() | |||
| 156 | + | readUntil(t, x, func() bool { return x.stats.Errors == 1 }) | |||
| 157 | + | require.Zero(t, x.stats.Unattributed) | |||
| 158 | + | require.False(t, x.closed) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | func sendConfigureRequest(p *xpeer, win uint32, mask uint16, x, y int16, w, h uint16) { | |||
| 162 | + | p.event(23, func(b *[32]byte) { | |||
| 163 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 164 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 165 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |||
| 166 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |||
| 167 | + | binary.LittleEndian.PutUint16(b[20:], w) | |||
| 168 | + | binary.LittleEndian.PutUint16(b[22:], h) | |||
| 169 | + | binary.LittleEndian.PutUint16(b[26:], mask) | |||
| 170 | + | }) | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | func TestConfigureRequestTiledOnlyGetsNotify(t *testing.T) { | |||
| 174 | + | x, p, h, _, _ := attached(t) | |||
| 175 | + | const win = 0x500001 | |||
| 176 | + | newWin(t, x, p, h, win) | |||
| 177 | + | n := settle(t, x, p) | |||
| 178 | + | ||||
| 179 | + | x.feed(t, p, func() { sendConfigureRequest(p, win, 0xf, 5, 6, 1000, 900) }) | |||
| 180 | + | settle(t, x, p) | |||
| 181 | + | require.Empty(t, since(p, n, opConfigureWindow), "a tiled window cannot resize itself") | |||
| 182 | + | evs := sentEvents(p, n, win) | |||
| 183 | + | require.Len(t, evs, 1) | |||
| 184 | + | require.EqualValues(t, 22, evs[0][0]) | |||
| 185 | + | require.EqualValues(t, 300, binary.LittleEndian.Uint16(evs[0][20:]), "current geometry") | |||
| 186 | + | require.EqualValues(t, 200, binary.LittleEndian.Uint16(evs[0][22:])) | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | func TestConfigureRequestFloatingHonoursSize(t *testing.T) { | |||
| 190 | + | x, p, h, _, _ := attached(t) | |||
| 191 | + | const win = 0x500001 | |||
| 192 | + | s := newWin(t, x, p, h, win) | |||
| 193 | + | x.tab.rec(s).floating = true | |||
| 194 | + | n := settle(t, x, p) | |||
| 195 | + | ||||
| 196 | + | x.feed(t, p, func() { | |||
| 197 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight), 0, 0, 70000&0xffff, 20000) | |||
| 198 | + | }) | |||
| 199 | + | settle(t, x, p) | |||
| 200 | + | cw := since(p, n, opConfigureWindow) | |||
| 201 | + | require.Len(t, cw, 1) | |||
| 202 | + | require.EqualValues(t, xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |||
| 203 | + | require.EqualValues(t, 4464, cw[0].u32(12)) | |||
| 204 | + | require.EqualValues(t, maxClientSize, cw[0].u32(16), "clamped") | |||
| 205 | + | require.Len(t, sentEvents(p, n, win), 1) | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | func TestConfigureRequestUnmanagedHonouredVerbatim(t *testing.T) { | |||
| 209 | + | x, p, _, _, _ := attached(t) | |||
| 210 | + | const win = 0x500009 | |||
| 211 | + | p.createNotify(win, true) | |||
| 212 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 }) | |||
| 213 | + | n := settle(t, x, p) | |||
| 214 | + | ||||
| 215 | + | x.feed(t, p, func() { | |||
| 216 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth), 30, 40, 50, 60) | |||
| 217 | + | }) | |||
| 218 | + | settle(t, x, p) | |||
| 219 | + | cw := since(p, n, opConfigureWindow) | |||
| 220 | + | require.Len(t, cw, 1) | |||
| 221 | + | require.EqualValues(t, 7, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |||
| 222 | + | require.Equal(t, []uint32{30, 40, 50}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20)}) | |||
| 223 | + | require.Empty(t, sentEvents(p, n, win)) | |||
| 224 | + | } | |||
| 225 | + | ||||
| 226 | + | func TestConfigureStaleAndDuplicateDirtySlots(t *testing.T) { | |||
| 227 | + | x, p, h, _, _ := attached(t) | |||
| 228 | + | const a, b = 0x500001, 0x500002 | |||
| 229 | + | s := newWin(t, x, p, h, a) | |||
| 230 | + | for i := 1; i <= 50; i++ { | |||
| 231 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i, Height: 100}) | |||
| 232 | + | } | |||
| 233 | + | require.Len(t, x.dirty, 1, "queued once per false→true transition") | |||
| 234 | + | ||||
| 235 | + | // The window dies with its entry queued; the slot is reused. | |||
| 236 | + | h.EXPECT().Dissociate(s, true).Once() | |||
| 237 | + | p.destroyNotify(a) | |||
| 238 | + | readUntil(t, x, func() bool { return x.tab.lookup(a) == 0 }) | |||
| 239 | + | s2 := newWin(t, x, p, h, b) | |||
| 240 | + | require.Equal(t, s, s2, "LIFO slot reuse") | |||
| 241 | + | n := len(p.requests()) | |||
| 242 | + | x.Configure(s2, ports.ConfigureWindow{Width: 640, Height: 480}) | |||
| 243 | + | require.Len(t, x.dirty, 1) | |||
| 244 | + | x.dirty = append(x.dirty, s, s) // stale and duplicate entries must be skipped | |||
| 245 | + | settle(t, x, p) | |||
| 246 | + | cw := since(p, n, opConfigureWindow) | |||
| 247 | + | require.Len(t, cw, 1, "sent once despite duplicates") | |||
| 248 | + | require.Equal(t, uint32(b), cw[0].u32(4)) | |||
| 249 | + | require.Empty(t, x.dirty) | |||
| 250 | + | require.LessOrEqual(t, cap(x.dirty), len(x.tab.recs)) | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | func TestDirtyQueueNeverGrows(t *testing.T) { | |||
| 254 | + | x, p, h, _, _ := attached(t) | |||
| 255 | + | c0 := cap(x.dirty) | |||
| 256 | + | const base = 0x600000 | |||
| 257 | + | for i := 0; i < 3*c0; i++ { // each round leaves a stale entry behind | |||
| 258 | + | xid := uint32(base + i) | |||
| 259 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(1)).Once() | |||
| 260 | + | p.createNotify(xid, false) | |||
| 261 | + | readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 }) | |||
| 262 | + | s := x.tab.lookup(xid) | |||
| 263 | + | x.tab.rec(s).ready = true | |||
| 264 | + | x.Configure(s, ports.ConfigureWindow{Width: 10, Height: 10}) | |||
| 265 | + | h.EXPECT().Dissociate(s, true).Once() | |||
| 266 | + | p.destroyNotify(xid) | |||
| 267 | + | readUntil(t, x, func() bool { return x.tab.lookup(xid) == 0 }) | |||
| 268 | + | } | |||
| 269 | + | require.Equal(t, c0, cap(x.dirty)) | |||
| 270 | + | require.LessOrEqual(t, len(x.dirty), c0) | |||
| 271 | + | } | |||
| 272 | + | ||||
| 273 | + | func TestPublicCommandsIgnoredWhenClosed(t *testing.T) { | |||
| 274 | + | x, p, h, src, _ := attached(t) | |||
| 275 | + | const win = 0x500001 | |||
| 276 | + | s := newWin(t, x, p, h, win) | |||
| 277 | + | h.EXPECT().Dissociate(s, true).Once() | |||
| 278 | + | src.EXPECT().Remove().Once() | |||
| 279 | + | x.Close() | |||
| 280 | + | require.NotPanics(t, func() { | |||
| 281 | + | x.Configure(s, ports.ConfigureWindow{Width: 1, Height: 1}) | |||
| 282 | + | x.Place(s, 1, 2) | |||
| 283 | + | x.Focus(s) | |||
| 284 | + | x.CloseWindow(s) | |||
| 285 | + | }) | |||
| 286 | + | require.Empty(t, x.dirty) | |||
| 287 | + | require.Zero(t, x.active) | |||
| 288 | + | } | |||
| 289 | + | ||||
| 290 | + | // Focus. | |||
| 291 | + | ||||
| 292 | + | type focusModel struct { | |||
| 293 | + | name string | |||
| 294 | + | input, take bool | |||
| 295 | + | setFocus bool // SetInputFocus on the window | |||
| 296 | + | takeFocus bool // WM_TAKE_FOCUS sent | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | func TestFocusInputModels(t *testing.T) { | |||
| 300 | + | for _, m := range []focusModel{ | |||
| 301 | + | {"passive", true, false, true, false}, | |||
| 302 | + | {"locally active", true, true, true, true}, | |||
| 303 | + | {"globally active", false, true, false, true}, | |||
| 304 | + | {"no input", false, false, false, false}, | |||
| 305 | + | } { | |||
| 306 | + | t.Run(m.name, func(t *testing.T) { | |||
| 307 | + | x, p, h, _, _ := attached(t) | |||
| 308 | + | const win = 0x500001 | |||
| 309 | + | s := newWin(t, x, p, h, win) | |||
| 310 | + | r := x.tab.rec(s) | |||
| 311 | + | r.input, r.takeFocus = m.input, m.take | |||
| 312 | + | n := settle(t, x, p) | |||
| 313 | + | ||||
| 314 | + | x.Focus(s) | |||
| 315 | + | settle(t, x, p) | |||
| 316 | + | var focuses []xpeerReq | |||
| 317 | + | for _, r := range since(p, n, opSetInputFocus) { | |||
| 318 | + | if r.u32(4) == win { | |||
| 319 | + | focuses = append(focuses, r) | |||
| 320 | + | } | |||
| 321 | + | } | |||
| 322 | + | if m.setFocus { | |||
| 323 | + | require.Len(t, focuses, 1) | |||
| 324 | + | require.EqualValues(t, xproto.InputFocusNone, focuses[0].raw[1], "never PointerRoot") | |||
| 325 | + | require.Zero(t, focuses[0].u32(8), "CurrentTime") | |||
| 326 | + | } else { | |||
| 327 | + | require.Empty(t, focuses) | |||
| 328 | + | } | |||
| 329 | + | var takes int | |||
| 330 | + | for _, ev := range sentEvents(p, n, win) { | |||
| 331 | + | if ev[0] == 33 && binary.LittleEndian.Uint32(ev[8:]) == p.atom("WM_PROTOCOLS") { | |||
| 332 | + | require.EqualValues(t, 32, ev[1]) | |||
| 333 | + | require.Equal(t, p.atom("WM_TAKE_FOCUS"), binary.LittleEndian.Uint32(ev[12:])) | |||
| 334 | + | require.Zero(t, binary.LittleEndian.Uint32(ev[16:]), "CurrentTime") | |||
| 335 | + | takes++ | |||
| 336 | + | } | |||
| 337 | + | } | |||
| 338 | + | require.Equal(t, map[bool]int{true: 1, false: 0}[m.takeFocus], takes) | |||
| 339 | + | ||||
| 340 | + | act, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 341 | + | require.True(t, ok) | |||
| 342 | + | require.Equal(t, []uint32{win}, atomsOf(act), "named even for No-input windows") | |||
| 343 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |||
| 344 | + | require.True(t, ok) | |||
| 345 | + | require.Contains(t, atomsOf(st), p.atom("_NET_WM_STATE_FOCUSED")) | |||
| 346 | + | }) | |||
| 347 | + | } | |||
| 348 | + | } | |||
| 349 | + | ||||
| 350 | + | func TestFocusActiveWindowWrittenOnlyOnChange(t *testing.T) { | |||
| 351 | + | x, p, h, _, _ := attached(t) | |||
| 352 | + | const a, b = 0x500001, 0x500002 | |||
| 353 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |||
| 354 | + | n := settle(t, x, p) | |||
| 355 | + | ||||
| 356 | + | x.Focus(sa) | |||
| 357 | + | x.Focus(sa) | |||
| 358 | + | settle(t, x, p) | |||
| 359 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_ACTIVE_WINDOW"), 1) | |||
| 360 | + | ||||
| 361 | + | x.Focus(sb) | |||
| 362 | + | settle(t, x, p) | |||
| 363 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 364 | + | require.Equal(t, []uint32{b}, atomsOf(act)) | |||
| 365 | + | sta, _ := p.prop(a, "_NET_WM_STATE") | |||
| 366 | + | require.NotContains(t, atomsOf(sta), p.atom("_NET_WM_STATE_FOCUSED"), "old window loses FOCUSED") | |||
| 367 | + | stb, _ := p.prop(b, "_NET_WM_STATE") | |||
| 368 | + | require.Contains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |||
| 369 | + | ||||
| 370 | + | n = settle(t, x, p) | |||
| 371 | + | x.Focus(0) | |||
| 372 | + | settle(t, x, p) | |||
| 373 | + | none := since(p, n, opSetInputFocus) | |||
| 374 | + | require.Len(t, none, 1) | |||
| 375 | + | require.Zero(t, none[0].u32(4), "focus None") | |||
| 376 | + | require.EqualValues(t, xproto.InputFocusNone, none[0].raw[1]) | |||
| 377 | + | act, _ = p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 378 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |||
| 379 | + | stb, _ = p.prop(b, "_NET_WM_STATE") | |||
| 380 | + | require.NotContains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |||
| 381 | + | require.Zero(t, x.active) | |||
| 382 | + | } | |||
| 383 | + | ||||
| 384 | + | func TestFocusStackingLists(t *testing.T) { | |||
| 385 | + | x, p, h, _, _ := attached(t) | |||
| 386 | + | const a, b, c = 0x500001, 0x500002, 0x500003 | |||
| 387 | + | sa := newWin(t, x, p, h, a) | |||
| 388 | + | newWin(t, x, p, h, b) | |||
| 389 | + | newWin(t, x, p, h, c) | |||
| 390 | + | for _, w := range []uint32{a, b, c} { | |||
| 391 | + | x.addClient(w) | |||
| 392 | + | } | |||
| 393 | + | settle(t, x, p) | |||
| 394 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 395 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list)) | |||
| 396 | + | n := settle(t, x, p) | |||
| 397 | + | ||||
| 398 | + | x.Focus(sa) | |||
| 399 | + | settle(t, x, p) | |||
| 400 | + | stack, _ := p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |||
| 401 | + | require.Equal(t, []uint32{b, c, a}, atomsOf(stack), "bottom to top") | |||
| 402 | + | list, _ = p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 403 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list), "map order") | |||
| 404 | + | require.Empty(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), "clients list untouched by focus") | |||
| 405 | + | var raised bool | |||
| 406 | + | for _, r := range since(p, n, opConfigureWindow) { | |||
| 407 | + | if r.u32(4) == a && binary.LittleEndian.Uint16(r.raw[8:]) == uint16(xproto.ConfigWindowStackMode) { | |||
| 408 | + | require.EqualValues(t, xproto.StackModeAbove, r.u32(12)) | |||
| 409 | + | raised = true | |||
| 410 | + | } | |||
| 411 | + | } | |||
| 412 | + | require.True(t, raised) | |||
| 413 | + | ||||
| 414 | + | // A burst costs one write per list. | |||
| 415 | + | n = settle(t, x, p) | |||
| 416 | + | x.removeClient(b) | |||
| 417 | + | x.removeClient(c) | |||
| 418 | + | settle(t, x, p) | |||
| 419 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), 1) | |||
| 420 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST_STACKING"), 1) | |||
| 421 | + | stack, _ = p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |||
| 422 | + | require.Equal(t, []uint32{a}, atomsOf(stack)) | |||
| 423 | + | } | |||
| 424 | + | ||||
| 425 | + | func TestDestroyedActiveWindowClearsActive(t *testing.T) { | |||
| 426 | + | x, p, h, _, _ := attached(t) | |||
| 427 | + | const win = 0x500001 | |||
| 428 | + | s := newWin(t, x, p, h, win) | |||
| 429 | + | x.addClient(win) | |||
| 430 | + | x.Focus(s) | |||
| 431 | + | settle(t, x, p) | |||
| 432 | + | h.EXPECT().Dissociate(s, true).Once() | |||
| 433 | + | p.destroyNotify(win) | |||
| 434 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 }) | |||
| 435 | + | settle(t, x, p) | |||
| 436 | + | require.Zero(t, x.active) | |||
| 437 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 438 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |||
| 439 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 440 | + | require.Empty(t, list.data) | |||
| 441 | + | } | |||
| 442 | + | ||||
| 443 | + | func TestFocusInDenied(t *testing.T) { | |||
| 444 | + | x, p, h, _, _ := attached(t) | |||
| 445 | + | const a, b = 0x500001, 0x500002 | |||
| 446 | + | sa := newWin(t, x, p, h, a) | |||
| 447 | + | newWin(t, x, p, h, b) | |||
| 448 | + | x.Focus(sa) | |||
| 449 | + | n := settle(t, x, p) | |||
| 450 | + | ||||
| 451 | + | focusIn := func(win uint32, detail byte) { | |||
| 452 | + | p.event(9, func(e *[32]byte) { | |||
| 453 | + | e[1] = detail | |||
| 454 | + | binary.LittleEndian.PutUint32(e[4:], win) | |||
| 455 | + | }) | |||
| 456 | + | } | |||
| 457 | + | focusCount := func(skip int) int { | |||
| 458 | + | c := 0 | |||
| 459 | + | for _, r := range since(p, skip, opSetInputFocus) { | |||
| 460 | + | if r.u32(4) == a { | |||
| 461 | + | c++ | |||
| 462 | + | } | |||
| 463 | + | } | |||
| 464 | + | return c | |||
| 465 | + | } | |||
| 466 | + | x.feed(t, p, func() { focusIn(a, 0) }) // the active window itself: fine | |||
| 467 | + | x.feed(t, p, func() { focusIn(b, byte(xproto.NotifyDetailPointer)) }) | |||
| 468 | + | settle(t, x, p) | |||
| 469 | + | require.Zero(t, focusCount(n)) | |||
| 470 | + | ||||
| 471 | + | x.feed(t, p, func() { focusIn(b, 0); focusIn(b, 0) }) | |||
| 472 | + | readUntil(t, x, func() bool { return x.stats.Events >= 5 }) | |||
| 473 | + | settle(t, x, p) | |||
| 474 | + | require.Equal(t, 1, focusCount(n), "denied once per iteration") | |||
| 475 | + | n = settle(t, x, p) | |||
| 476 | + | settle(t, x, p) | |||
| 477 | + | require.Zero(t, focusCount(n)) | |||
| 478 | + | } | |||
| 479 | + | ||||
| 480 | + | func TestCloseWindow(t *testing.T) { | |||
| 481 | + | x, p, h, _, _ := attached(t) | |||
| 482 | + | const a, b = 0x500001, 0x500002 | |||
| 483 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |||
| 484 | + | x.tab.rec(sa).deleteWindow = true | |||
| 485 | + | n := settle(t, x, p) | |||
| 486 | + | ||||
| 487 | + | x.CloseWindow(sa) | |||
| 488 | + | x.CloseWindow(sb) | |||
| 489 | + | settle(t, x, p) | |||
| 490 | + | evs := sentEvents(p, n, a) | |||
| 491 | + | require.Len(t, evs, 1) | |||
| 492 | + | require.EqualValues(t, 33, evs[0][0]) | |||
| 493 | + | require.EqualValues(t, 32, evs[0][1]) | |||
| 494 | + | require.Equal(t, p.atom("WM_PROTOCOLS"), binary.LittleEndian.Uint32(evs[0][8:])) | |||
| 495 | + | require.Equal(t, p.atom("WM_DELETE_WINDOW"), binary.LittleEndian.Uint32(evs[0][12:])) | |||
| 496 | + | require.Zero(t, binary.LittleEndian.Uint32(evs[0][16:])) | |||
| 497 | + | require.Empty(t, sentEvents(p, n, b)) | |||
| 498 | + | ||||
| 499 | + | kills := since(p, n, opKillClient) | |||
| 500 | + | require.Len(t, kills, 1) | |||
| 501 | + | require.Equal(t, uint32(b), kills[0].u32(4)) | |||
| 502 | + | } | |||
| 503 | + | ||||
| 504 | + | // Allocation guards. The peer sinks requests so the connection never blocks; | |||
| 505 | + | // its goroutine is idle in the sink loop before measuring. | |||
| 506 | + | ||||
| 507 | + | func sinkPeer(t *testing.T, x *XWM, p *xpeer) { | |||
| 508 | + | t.Helper() | |||
| 509 | + | p.sink.Store(true) | |||
| 510 | + | n := len(p.requests()) | |||
| 511 | + | xproto.GetInputFocus(x.conn.Buf()) // served by the old loop iteration | |||
| 512 | + | require.NoError(t, x.conn.Flush()) | |||
| 513 | + | p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |||
| 514 | + | // The peer records a request and answers it under mu: once the lock is | |||
| 515 | + | // free again it is back in its sink loop, which does not allocate. | |||
| 516 | + | p.mu.Lock() | |||
| 517 | + | _ = p.seq | |||
| 518 | + | p.mu.Unlock() | |||
| 519 | + | x.src = nil // no source: write backpressure would not allocate either | |||
| 520 | + | x.post = func(func()) {} | |||
| 521 | + | x.warnedRing = true // the sink never answers: Expect slots stay used | |||
| 522 | + | } | |||
| 523 | + | ||||
| 524 | + | func TestConfigureFlushAllocations(t *testing.T) { | |||
| 525 | + | x, p, h, _, _ := attached(t) | |||
| 526 | + | s := newWin(t, x, p, h, 0x500001) | |||
| 527 | + | sinkPeer(t, x, p) | |||
| 528 | + | i := 0 | |||
| 529 | + | allocs := testing.AllocsPerRun(300, func() { | |||
| 530 | + | i++ | |||
| 531 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i%2, Height: 100, Fullscreen: i%3 == 0, Activated: i%2 == 0}) | |||
| 532 | + | x.Place(s, i%5, i%7) | |||
| 533 | + | x.posted = false | |||
| 534 | + | x.flush() | |||
| 535 | + | }) | |||
| 536 | + | require.Zero(t, allocs) | |||
| 537 | + | require.False(t, x.closed) | |||
| 538 | + | } | |||
| 539 | + | ||||
| 540 | + | func TestFocusAllocations(t *testing.T) { | |||
| 541 | + | x, p, h, _, _ := attached(t) | |||
| 542 | + | a, b := newWin(t, x, p, h, 0x500001), newWin(t, x, p, h, 0x500002) | |||
| 543 | + | x.addClient(x.tab.rec(a).xid) | |||
| 544 | + | x.addClient(x.tab.rec(b).xid) | |||
| 545 | + | x.tab.rec(b).takeFocus = true | |||
| 546 | + | x.tab.rec(a).input, x.tab.rec(b).input = true, true | |||
| 547 | + | sinkPeer(t, x, p) | |||
| 548 | + | i := 0 | |||
| 549 | + | allocs := testing.AllocsPerRun(150, func() { | |||
| 550 | + | i++ | |||
| 551 | + | switch i % 3 { | |||
| 552 | + | case 0: | |||
| 553 | + | x.Focus(a) | |||
| 554 | + | case 1: | |||
| 555 | + | x.Focus(b) | |||
| 556 | + | default: | |||
| 557 | + | x.Focus(0) | |||
| 558 | + | } | |||
| 559 | + | x.refocus = i%5 == 0 | |||
| 560 | + | x.posted = false | |||
| 561 | + | x.flush() | |||
| 562 | + | }) | |||
| 563 | + | require.Zero(t, allocs) | |||
| 564 | + | require.False(t, x.closed) | |||
| 565 | + | } | |||
A internal/adapters/wayland/xwm/events.go +260 -0
| @@ -0,0 +1,260 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bnema.dev/xturbo" | |||
| 5 | + | "bnema.dev/xturbo/composite" | |||
| 6 | + | "bnema.dev/xturbo/res" | |||
| 7 | + | "bnema.dev/xturbo/xfixes" | |||
| 8 | + | "bnema.dev/xturbo/xproto" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Reply kinds registered with Expect. The first argument is the window tag | |||
| 12 | + | // (slot | generation<<16, see table.tag), the second a property index. | |||
| 13 | + | const ( | |||
| 14 | + | kindAttr uint16 = 16 + iota | |||
| 15 | + | kindProp | |||
| 16 | + | kindGeom | |||
| 17 | + | kindClientIds | |||
| 18 | + | kindConfigure | |||
| 19 | + | kindFocus | |||
| 20 | + | kindRootMask | |||
| 21 | + | kindVersion | |||
| 22 | + | kindTree | |||
| 23 | + | kindOverrideType | |||
| 24 | + | ) | |||
| 25 | + | ||||
| 26 | + | // onEvent dispatches an X event. Before Attach there is no host: only the | |||
| 27 | + | // table is maintained, and Attach replays the result. | |||
| 28 | + | func (x *XWM) onEvent(ev *xturbo.Event) { | |||
| 29 | + | x.stats.Events++ | |||
| 30 | + | if x.closed { | |||
| 31 | + | return | |||
| 32 | + | } | |||
| 33 | + | switch ev.Kind { | |||
| 34 | + | case xproto.KindCreateNotify: | |||
| 35 | + | x.onCreateNotify(ev) | |||
| 36 | + | case xproto.KindDestroyNotify: | |||
| 37 | + | x.onDestroyNotify(ev) | |||
| 38 | + | case xproto.KindReparentNotify: | |||
| 39 | + | x.onReparentNotify(ev) | |||
| 40 | + | } | |||
| 41 | + | if x.host == nil { | |||
| 42 | + | x.onEventDetached(ev) | |||
| 43 | + | return | |||
| 44 | + | } | |||
| 45 | + | switch ev.Kind { | |||
| 46 | + | case xproto.KindMapRequest: | |||
| 47 | + | x.onMapRequest(ev) | |||
| 48 | + | case xproto.KindMapNotify: | |||
| 49 | + | x.onMapNotify(ev) | |||
| 50 | + | case xproto.KindUnmapNotify: | |||
| 51 | + | x.onUnmapNotify(ev) | |||
| 52 | + | case xproto.KindConfigureRequest: | |||
| 53 | + | x.onConfigureRequest(ev) | |||
| 54 | + | case xproto.KindConfigureNotify: | |||
| 55 | + | x.onConfigureNotify(ev) | |||
| 56 | + | case xproto.KindPropertyNotify: | |||
| 57 | + | x.onPropertyNotify(ev) | |||
| 58 | + | case xproto.KindClientMessage: | |||
| 59 | + | x.onClientMessage(ev) | |||
| 60 | + | case xproto.KindFocusIn: | |||
| 61 | + | x.onFocusIn(ev) | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // onEventDetached keeps what Attach replays: windows asked to map, and those | |||
| 66 | + | // withdrawn again, so Attach skips them; geometry requests are answered. | |||
| 67 | + | func (x *XWM) onEventDetached(ev *xturbo.Event) { | |||
| 68 | + | switch ev.Kind { | |||
| 69 | + | case xproto.KindMapRequest: | |||
| 70 | + | if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 { | |||
| 71 | + | x.tab.rec(s).mapping = true | |||
| 72 | + | } | |||
| 73 | + | case xproto.KindUnmapNotify: | |||
| 74 | + | if ev.SendEvent { | |||
| 75 | + | return | |||
| 76 | + | } | |||
| 77 | + | if s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window())); s != 0 { | |||
| 78 | + | r := x.tab.rec(s) | |||
| 79 | + | r.mapped, r.mapping = false, false | |||
| 80 | + | } | |||
| 81 | + | case xproto.KindConfigureRequest: | |||
| 82 | + | x.onConfigureRequest(ev) | |||
| 83 | + | } | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | func (x *XWM) onReply(r *xturbo.Reply) { | |||
| 87 | + | x.stats.Replies++ | |||
| 88 | + | if x.closed { | |||
| 89 | + | return | |||
| 90 | + | } | |||
| 91 | + | switch r.Kind { | |||
| 92 | + | case kindVersion: | |||
| 93 | + | x.onVersionReply(r) | |||
| 94 | + | return | |||
| 95 | + | case kindTree: | |||
| 96 | + | x.onTreeReply(r) | |||
| 97 | + | return | |||
| 98 | + | } | |||
| 99 | + | s, rec := x.tab.live(r.Args[0]) | |||
| 100 | + | if rec == nil { | |||
| 101 | + | x.stats.Stale++ | |||
| 102 | + | return | |||
| 103 | + | } | |||
| 104 | + | switch r.Kind { | |||
| 105 | + | case kindAttr: | |||
| 106 | + | x.onAttrReply(rec, r) | |||
| 107 | + | case kindProp: | |||
| 108 | + | x.onPropReply(s, rec, r) | |||
| 109 | + | case kindGeom: | |||
| 110 | + | x.onGeomReply(s, rec, r) | |||
| 111 | + | case kindClientIds: | |||
| 112 | + | x.onClientIdsReply(s, rec, r) | |||
| 113 | + | case kindOverrideType: | |||
| 114 | + | x.onOverrideType(s, rec, r) | |||
| 115 | + | } | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // onError never tears the XWM down: an error names a request, not the | |||
| 119 | + | // connection. Sticky Buf errors surface through Flush. | |||
| 120 | + | func (x *XWM) onError(e *xturbo.XError) { | |||
| 121 | + | x.stats.Errors++ | |||
| 122 | + | if e.Kind == 0 { | |||
| 123 | + | x.stats.Unattributed++ | |||
| 124 | + | x.log.Debug().Uint8("code", e.Code).Uint8("major", e.Major).Uint16("minor", e.Minor). | |||
| 125 | + | Uint64("seq", uint64(e.Seq)).Msg("unattributed X error") | |||
| 126 | + | return | |||
| 127 | + | } | |||
| 128 | + | switch e.Kind { | |||
| 129 | + | case kindRootMask: | |||
| 130 | + | if e.Code == xproto.BadAccess { | |||
| 131 | + | x.bootErr = ErrWMExists | |||
| 132 | + | } | |||
| 133 | + | case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus, kindOverrideType: | |||
| 134 | + | // Usually BadWindow: the window died between request and reply and | |||
| 135 | + | // DestroyNotify follows. Whatever the code, the awaited reply will | |||
| 136 | + | // never come, so the map must not wait for it. | |||
| 137 | + | s, rec := x.tab.live(e.Args[0]) | |||
| 138 | + | if rec == nil { | |||
| 139 | + | x.stats.Stale++ | |||
| 140 | + | return | |||
| 141 | + | } | |||
| 142 | + | switch e.Kind { | |||
| 143 | + | case kindProp: | |||
| 144 | + | rec.pending &^= 1 << e.Args[1] | |||
| 145 | + | case kindGeom: | |||
| 146 | + | rec.pending &^= propGeom | |||
| 147 | + | case kindClientIds: | |||
| 148 | + | rec.pending &^= propClientIDs | |||
| 149 | + | case kindOverrideType: | |||
| 150 | + | if e.Code != xproto.BadWindow { // typed unknown: the window still shows | |||
| 151 | + | x.overrideReady(s) | |||
| 152 | + | } | |||
| 153 | + | return | |||
| 154 | + | default: | |||
| 155 | + | return | |||
| 156 | + | } | |||
| 157 | + | if e.Code != xproto.BadWindow && e.Code != xproto.BadDrawable { // a dead window is never announced | |||
| 158 | + | x.checkReady(s) | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | // onCreateNotify records a new top-level window. Override-redirect windows | |||
| 164 | + | // are kept (their geometry and serial matter later) but never reach the host. | |||
| 165 | + | func (x *XWM) onCreateNotify(ev *xturbo.Event) { | |||
| 166 | + | e := xproto.CreateNotifyOf(ev) | |||
| 167 | + | xid := uint32(e.Window()) | |||
| 168 | + | if xid == x.wmWin || x.tab.lookup(xid) != 0 { | |||
| 169 | + | return | |||
| 170 | + | } | |||
| 171 | + | s := x.insert(xid) | |||
| 172 | + | if s == 0 { | |||
| 173 | + | return | |||
| 174 | + | } | |||
| 175 | + | r := x.tab.rec(s) | |||
| 176 | + | override := e.OverrideRedirect() | |||
| 177 | + | r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height()) | |||
| 178 | + | if override { | |||
| 179 | + | r.kind = kindUnmanaged | |||
| 180 | + | x.stats.UnmanagedSeen++ | |||
| 181 | + | return | |||
| 182 | + | } | |||
| 183 | + | if x.host != nil { | |||
| 184 | + | x.adopt(s) | |||
| 185 | + | } | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | // adopt announces a managed window to the host and starts watching it. | |||
| 189 | + | func (x *XWM) adopt(s Slot) { | |||
| 190 | + | r := x.tab.rec(s) | |||
| 191 | + | r.id = x.host.Created(s, r.xid) | |||
| 192 | + | var v xproto.ChangeWindowAttributesValues | |||
| 193 | + | v.SetEventMask(xproto.EventMaskPropertyChange | xproto.EventMaskFocusChange) | |||
| 194 | + | x.expect(xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(r.xid), &v), kindAttr, x.tab.tag(s), 0) | |||
| 195 | + | x.log.Debug().Uint32("xid", r.xid).Uint32("slot", uint32(s)).Msg("x11 window created") | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | func (x *XWM) onDestroyNotify(ev *xturbo.Event) { | |||
| 199 | + | xid := uint32(xproto.DestroyNotifyOf(ev).Window()) | |||
| 200 | + | s := x.tab.lookup(xid) | |||
| 201 | + | if s == 0 { | |||
| 202 | + | return | |||
| 203 | + | } | |||
| 204 | + | x.forgetWindow(s) | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | // onTreeReply (boot): record every existing child and ask for its attributes. | |||
| 208 | + | func (x *XWM) onTreeReply(r *xturbo.Reply) { | |||
| 209 | + | q, ok := xproto.ParseQueryTreeReply(r.Raw) | |||
| 210 | + | if !ok { | |||
| 211 | + | return | |||
| 212 | + | } | |||
| 213 | + | children := q.Children() | |||
| 214 | + | for i := 0; i < children.Len(); i++ { | |||
| 215 | + | xid := uint32(children.At(i)) | |||
| 216 | + | if xid == x.wmWin || x.tab.lookup(xid) != 0 { | |||
| 217 | + | continue | |||
| 218 | + | } | |||
| 219 | + | s := x.insert(xid) | |||
| 220 | + | if s == 0 { | |||
| 221 | + | return | |||
| 222 | + | } | |||
| 223 | + | x.expect(xproto.GetWindowAttributes(x.conn.Buf(), xproto.Window(xid)), kindAttr, x.tab.tag(s), 0) | |||
| 224 | + | } | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | // onAttrReply (boot): classify an existing window; viewable ones go through | |||
| 228 | + | // the map flow once the host is attached. | |||
| 229 | + | func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) { | |||
| 230 | + | a, ok := xproto.ParseGetWindowAttributesReply(r.Raw) | |||
| 231 | + | if !ok || x.host != nil { | |||
| 232 | + | return | |||
| 233 | + | } | |||
| 234 | + | if a.OverrideRedirect() { | |||
| 235 | + | rec.kind = kindUnmanaged | |||
| 236 | + | rec.mapped = a.MapState() == xproto.MapStateViewable | |||
| 237 | + | x.stats.UnmanagedSeen++ | |||
| 238 | + | return | |||
| 239 | + | } | |||
| 240 | + | if a.MapState() == xproto.MapStateViewable { | |||
| 241 | + | rec.mapped, rec.mapping = true, true | |||
| 242 | + | } | |||
| 243 | + | } | |||
| 244 | + | ||||
| 245 | + | func (x *XWM) onVersionReply(r *xturbo.Reply) { | |||
| 246 | + | switch r.Args[0] { | |||
| 247 | + | case versionComposite: | |||
| 248 | + | if v, ok := composite.ParseQueryVersionReply(r.Raw); ok { | |||
| 249 | + | x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("Composite") | |||
| 250 | + | } | |||
| 251 | + | case versionXFixes: | |||
| 252 | + | if v, ok := xfixes.ParseQueryVersionReply(r.Raw); ok { | |||
| 253 | + | x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("XFixes") | |||
| 254 | + | } | |||
| 255 | + | case versionRes: | |||
| 256 | + | if v, ok := res.ParseQueryVersionReply(r.Raw); ok { | |||
| 257 | + | x.log.Info().Uint16("major", v.ServerMajor()).Uint16("minor", v.ServerMinor()).Msg("X-Resource") | |||
| 258 | + | } | |||
| 259 | + | } | |||
| 260 | + | } | |||
A internal/adapters/wayland/xwm/fdsource_mock_test.go +129 -0
| @@ -0,0 +1,129 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package xwm | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | mock "github.com/stretchr/testify/mock" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // newMockfdSource creates a new instance of mockfdSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 12 | + | // The first argument is typically a *testing.T value. | |||
| 13 | + | func newMockfdSource(t interface { | |||
| 14 | + | mock.TestingT | |||
| 15 | + | Cleanup(func()) | |||
| 16 | + | }) *mockfdSource { | |||
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 18 | + | helper.Helper() | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | mock := &mockfdSource{} | |||
| 22 | + | mock.Mock.Test(t) | |||
| 23 | + | ||||
| 24 | + | t.Cleanup(func() { | |||
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 26 | + | helper.Helper() | |||
| 27 | + | } | |||
| 28 | + | mock.AssertExpectations(t) | |||
| 29 | + | }) | |||
| 30 | + | ||||
| 31 | + | return mock | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // mockfdSource is an autogenerated mock type for the fdSource type | |||
| 35 | + | type mockfdSource struct { | |||
| 36 | + | mock.Mock | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | type mockfdSource_Expecter struct { | |||
| 40 | + | mock *mock.Mock | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (_m *mockfdSource) EXPECT() *mockfdSource_Expecter { | |||
| 44 | + | return &mockfdSource_Expecter{mock: &_m.Mock} | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // Remove provides a mock function for the type mockfdSource | |||
| 48 | + | func (_mock *mockfdSource) Remove() { | |||
| 49 | + | _mock.Called() | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // mockfdSource_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' | |||
| 54 | + | type mockfdSource_Remove_Call struct { | |||
| 55 | + | *mock.Call | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // Remove is a helper method to define mock.On call | |||
| 59 | + | func (_e *mockfdSource_Expecter) Remove() *mockfdSource_Remove_Call { | |||
| 60 | + | return &mockfdSource_Remove_Call{Call: _e.mock.On("Remove")} | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func (_c *mockfdSource_Remove_Call) Run(run func()) *mockfdSource_Remove_Call { | |||
| 64 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 65 | + | run() | |||
| 66 | + | }) | |||
| 67 | + | return _c | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func (_c *mockfdSource_Remove_Call) Return() *mockfdSource_Remove_Call { | |||
| 71 | + | _c.Call.Return() | |||
| 72 | + | return _c | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (_c *mockfdSource_Remove_Call) RunAndReturn(run func()) *mockfdSource_Remove_Call { | |||
| 76 | + | _c.Run(run) | |||
| 77 | + | return _c | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // SetWritable provides a mock function for the type mockfdSource | |||
| 81 | + | func (_mock *mockfdSource) SetWritable(on bool) error { | |||
| 82 | + | ret := _mock.Called(on) | |||
| 83 | + | ||||
| 84 | + | if len(ret) == 0 { | |||
| 85 | + | panic("no return value specified for SetWritable") | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | var r0 error | |||
| 89 | + | if returnFunc, ok := ret.Get(0).(func(bool) error); ok { | |||
| 90 | + | r0 = returnFunc(on) | |||
| 91 | + | } else { | |||
| 92 | + | r0 = ret.Error(0) | |||
| 93 | + | } | |||
| 94 | + | return r0 | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // mockfdSource_SetWritable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWritable' | |||
| 98 | + | type mockfdSource_SetWritable_Call struct { | |||
| 99 | + | *mock.Call | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // SetWritable is a helper method to define mock.On call | |||
| 103 | + | // - on bool | |||
| 104 | + | func (_e *mockfdSource_Expecter) SetWritable(on any) *mockfdSource_SetWritable_Call { | |||
| 105 | + | return &mockfdSource_SetWritable_Call{Call: _e.mock.On("SetWritable", on)} | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | func (_c *mockfdSource_SetWritable_Call) Run(run func(on bool)) *mockfdSource_SetWritable_Call { | |||
| 109 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 110 | + | var arg0 bool | |||
| 111 | + | if args[0] != nil { | |||
| 112 | + | arg0 = args[0].(bool) | |||
| 113 | + | } | |||
| 114 | + | run( | |||
| 115 | + | arg0, | |||
| 116 | + | ) | |||
| 117 | + | }) | |||
| 118 | + | return _c | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func (_c *mockfdSource_SetWritable_Call) Return(err error) *mockfdSource_SetWritable_Call { | |||
| 122 | + | _c.Call.Return(err) | |||
| 123 | + | return _c | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func (_c *mockfdSource_SetWritable_Call) RunAndReturn(run func(on bool) error) *mockfdSource_SetWritable_Call { | |||
| 127 | + | _c.Call.Return(run) | |||
| 128 | + | return _c | |||
| 129 | + | } | |||
A internal/adapters/wayland/xwm/fixes_test.go +230 -0
| @@ -0,0 +1,230 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "bnema.dev/xturbo" | |||
| 8 | + | "bnema.dev/xturbo/xproto" | |||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | + | "github.com/stretchr/testify/mock" | |||
| 11 | + | "github.com/stretchr/testify/require" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | func getProps(p *xpeer, skip int, win uint32) int { | |||
| 15 | + | n := 0 | |||
| 16 | + | for _, r := range since(p, skip, opGetProperty) { | |||
| 17 | + | if r.u32(4) == win { | |||
| 18 | + | n++ | |||
| 19 | + | } | |||
| 20 | + | } | |||
| 21 | + | return n | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // Our own _NET_WM_STATE writes must not be read back once the window is ready. | |||
| 25 | + | func TestOwnNetWMStateWriteIsNotReadBack(t *testing.T) { | |||
| 26 | + | hs := newHarness(t, nil) | |||
| 27 | + | const win = 0x500001 | |||
| 28 | + | hs.classic(win) | |||
| 29 | + | s := hs.mapAndWait(win) | |||
| 30 | + | ||||
| 31 | + | hs.x.Configure(s, ports.ConfigureWindow{Width: 300, Height: 200, Fullscreen: true}) | |||
| 32 | + | n := settle(t, hs.x, hs.p) | |||
| 33 | + | require.True(t, hs.x.tab.rec(s).fullscreen, "the written state is the known state") | |||
| 34 | + | st, _ := hs.p.prop(win, "_NET_WM_STATE") | |||
| 35 | + | require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FULLSCREEN")}, atomsOf(st)) | |||
| 36 | + | ||||
| 37 | + | ev := hs.x.Stats().Events | |||
| 38 | + | hs.p.propertyNotify(win, hs.p.atom("_NET_WM_STATE"), false) | |||
| 39 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > ev }) | |||
| 40 | + | settle(t, hs.x, hs.p) | |||
| 41 | + | require.Zero(t, getProps(hs.p, n, win), "no GetProperty") | |||
| 42 | + | ||||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // A pre-map XMoveResize is honoured, floating or not. | |||
| 46 | + | func TestConfigureRequestBeforeReadyIsHonoured(t *testing.T) { | |||
| 47 | + | hs := newHarness(t, nil) | |||
| 48 | + | const win = 0x500001 | |||
| 49 | + | hs.classic(win) | |||
| 50 | + | hs.create(win) | |||
| 51 | + | n := settle(t, hs.x, hs.p) | |||
| 52 | + | ||||
| 53 | + | mask := uint16(xproto.ConfigWindowX | xproto.ConfigWindowY | xproto.ConfigWindowWidth | xproto.ConfigWindowHeight) | |||
| 54 | + | hs.x.feed(t, hs.p, func() { sendConfigureRequest(hs.p, win, mask, 11, 22, 640, 70000&0xffff) }) | |||
| 55 | + | settle(t, hs.x, hs.p) | |||
| 56 | + | cw := since(hs.p, n, opConfigureWindow) | |||
| 57 | + | require.Len(t, cw, 1) | |||
| 58 | + | require.Equal(t, []uint32{11, 22, 640, 4464}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24)}) | |||
| 59 | + | require.Len(t, sentEvents(hs.p, n, win), 1) | |||
| 60 | + | ||||
| 61 | + | hs.expectReady(win) | |||
| 62 | + | hs.p.mapRequest(win) | |||
| 63 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |||
| 64 | + | require.Equal(t, 640, hs.ready[win].Width) | |||
| 65 | + | require.Equal(t, 4464, hs.ready[win].Height) | |||
| 66 | + | require.False(t, hs.ready[win].Floating) | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // After a remap the state is rewritten from scratch, FOCUSED included. | |||
| 70 | + | func TestRemapRewritesStateWithFocused(t *testing.T) { | |||
| 71 | + | hs := newHarness(t, nil) | |||
| 72 | + | const win = 0x500001 | |||
| 73 | + | hs.classic(win) | |||
| 74 | + | s := hs.mapAndWait(win) | |||
| 75 | + | hs.x.Focus(s) | |||
| 76 | + | settle(t, hs.x, hs.p) | |||
| 77 | + | st, _ := hs.p.prop(win, "_NET_WM_STATE") | |||
| 78 | + | require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |||
| 79 | + | ||||
| 80 | + | hs.h.EXPECT().Dissociate(s, false).Once() | |||
| 81 | + | hs.p.unmapNotify(win, false) | |||
| 82 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping }) | |||
| 83 | + | delete(hs.ready, win) | |||
| 84 | + | hs.expectReady(win) | |||
| 85 | + | hs.p.mapRequest(win) | |||
| 86 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |||
| 87 | + | ||||
| 88 | + | hs.x.Focus(s) | |||
| 89 | + | settle(t, hs.x, hs.p) | |||
| 90 | + | st, _ = hs.p.prop(win, "_NET_WM_STATE") | |||
| 91 | + | require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // Replies of a previous map never satisfy the next one (xturbo usually retires | |||
| 95 | + | // their slots first; the generation guards the rest). | |||
| 96 | + | func TestStaleRepliesOfPreviousMapAreDropped(t *testing.T) { | |||
| 97 | + | hs := newHarness(t, nil) | |||
| 98 | + | const win = 0x500001 | |||
| 99 | + | hs.classic(win) | |||
| 100 | + | s := hs.create(win) | |||
| 101 | + | hs.p.holdReplies() // the second map is never answered | |||
| 102 | + | hs.p.mapRequest(win) | |||
| 103 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping }) | |||
| 104 | + | oldTag := hs.x.tab.tag(s) | |||
| 105 | + | hs.h.EXPECT().Dissociate(s, false).Once() | |||
| 106 | + | hs.p.unmapNotify(win, false) | |||
| 107 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping }) | |||
| 108 | + | hs.p.mapRequest(win) | |||
| 109 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping }) | |||
| 110 | + | require.NotEqual(t, oldTag, hs.x.tab.tag(s)) | |||
| 111 | + | ||||
| 112 | + | rep := make([]byte, 32) | |||
| 113 | + | rep[0] = 1 | |||
| 114 | + | for idx := 0; idx < propCount; idx++ { | |||
| 115 | + | hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{oldTag, uint32(idx)}, Raw: rep}) | |||
| 116 | + | } | |||
| 117 | + | hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{oldTag}, Raw: rep}) | |||
| 118 | + | require.EqualValues(t, propCount+1, hs.x.Stats().Stale) | |||
| 119 | + | require.False(t, hs.x.tab.rec(s).ready) | |||
| 120 | + | require.Equal(t, propMaskAll&^propClientIDs, hs.x.tab.rec(s).pending, "the new map still awaits its own replies") | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func TestGetGeometryBadDrawableIsNotReady(t *testing.T) { | |||
| 124 | + | hs := newHarness(t, nil) | |||
| 125 | + | const win = 0x500001 | |||
| 126 | + | hs.classic(win) | |||
| 127 | + | s := hs.create(win) | |||
| 128 | + | hs.p.set(func(p *xpeer) { p.bad[win] = true }) | |||
| 129 | + | hs.p.mapRequest(win) | |||
| 130 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 }) | |||
| 131 | + | require.Zero(t, hs.x.Stats().Unattributed) | |||
| 132 | + | require.False(t, hs.x.tab.rec(s).ready) | |||
| 133 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |||
| 134 | + | hs.p.destroyNotify(win) | |||
| 135 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // A host that refocuses the window while dissociating it must not leave a | |||
| 139 | + | // dangling active slot. | |||
| 140 | + | func TestForgetWindowClearsActiveSetByHost(t *testing.T) { | |||
| 141 | + | x, p, h, _, _ := attached(t) | |||
| 142 | + | const win = 0x500001 | |||
| 143 | + | s := newWin(t, x, p, h, win) | |||
| 144 | + | h.EXPECT().Dissociate(s, true).Run(func(sl Slot, _ bool) { x.Focus(sl) }).Once() | |||
| 145 | + | p.destroyNotify(win) | |||
| 146 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 }) | |||
| 147 | + | require.Zero(t, x.active) | |||
| 148 | + | require.False(t, x.refocus) | |||
| 149 | + | settle(t, x, p) | |||
| 150 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 151 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | func TestTitleAndAppIDAreValidUTF8(t *testing.T) { | |||
| 155 | + | hs := newHarness(t, nil) | |||
| 156 | + | const win = 0x500001 | |||
| 157 | + | hs.classic(win) | |||
| 158 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) }) | |||
| 159 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("caf\xe9")) | |||
| 160 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00C\xe9\x00")) | |||
| 161 | + | s := hs.mapAndWait(win) | |||
| 162 | + | require.Equal(t, "caf\uFFFD", hs.ready[win].Title) | |||
| 163 | + | require.Equal(t, "C\uFFFD", hs.ready[win].AppID) | |||
| 164 | + | ||||
| 165 | + | // Unchanged after sanitising: no host call. | |||
| 166 | + | hs.notifyWMName(win) | |||
| 167 | + | // A changed truncated multibyte title reaches the host sanitised. | |||
| 168 | + | hs.h.EXPECT().Title(s, "\u00e9\uFFFD").Once() | |||
| 169 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("\xc3\xa9\xe2\x82")) | |||
| 170 | + | hs.notifyWMName(win) | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | func TestSyntheticConfigureNotifyClampsCoordinates(t *testing.T) { | |||
| 174 | + | x, p, h, _, _ := attached(t) | |||
| 175 | + | const win = 0x500001 | |||
| 176 | + | s := newWin(t, x, p, h, win) | |||
| 177 | + | n := settle(t, x, p) | |||
| 178 | + | r := x.tab.rec(s) | |||
| 179 | + | r.x, r.y, r.w, r.h = 70000, -70000, 70000, 5 | |||
| 180 | + | x.sendConfigureNotify(r) | |||
| 181 | + | settle(t, x, p) | |||
| 182 | + | ev := sentEvents(p, n, win) | |||
| 183 | + | require.Len(t, ev, 1) | |||
| 184 | + | require.EqualValues(t, 32767, int16(binary.LittleEndian.Uint16(ev[0][16:]))) | |||
| 185 | + | require.EqualValues(t, -32768, int16(binary.LittleEndian.Uint16(ev[0][18:]))) | |||
| 186 | + | require.EqualValues(t, 65535, binary.LittleEndian.Uint16(ev[0][20:])) | |||
| 187 | + | require.EqualValues(t, 5, binary.LittleEndian.Uint16(ev[0][22:])) | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // A window withdrawn before Attach is not mapped by Attach; a pre-attach | |||
| 191 | + | // ConfigureRequest is honoured. | |||
| 192 | + | func TestUnmapAndConfigureBeforeAttach(t *testing.T) { | |||
| 193 | + | x, p := bootPeer(t, nil) | |||
| 194 | + | const gone, sized = 0x500001, 0x500002 | |||
| 195 | + | for _, w := range []uint32{gone, sized} { | |||
| 196 | + | p.setProp(w, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00")) | |||
| 197 | + | p.geoms[w] = xpeerGeom{w: 300, h: 200} | |||
| 198 | + | p.createNotify(w, false) | |||
| 199 | + | p.mapRequest(w) | |||
| 200 | + | } | |||
| 201 | + | p.unmapNotify(gone, false) | |||
| 202 | + | mask := uint16(xproto.ConfigWindowWidth | xproto.ConfigWindowHeight) | |||
| 203 | + | sendConfigureRequest(p, sized, mask, 0, 0, 500, 400) | |||
| 204 | + | readUntil(t, x, func() bool { return x.Stats().Events >= 6 }) | |||
| 205 | + | n := len(p.requests()) | |||
| 206 | + | ||||
| 207 | + | h := newMockhost(t) | |||
| 208 | + | src := newMockfdSource(t) | |||
| 209 | + | src.EXPECT().Remove().Maybe() | |||
| 210 | + | post, _ := newPost() | |||
| 211 | + | var got WindowInfo | |||
| 212 | + | done := false | |||
| 213 | + | h.EXPECT().Created(mock.Anything, mock.Anything).Return(ports.WindowID(1)).Twice() | |||
| 214 | + | h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once() | |||
| 215 | + | x.attach(src, h, post) | |||
| 216 | + | readUntil(t, x, func() bool { return done }) | |||
| 217 | + | settle(t, x, p) | |||
| 218 | + | for _, r := range since(p, n, opMapWindow) { | |||
| 219 | + | require.NotEqual(t, uint32(gone), r.u32(4), "withdrawn window must not be mapped") | |||
| 220 | + | } | |||
| 221 | + | require.Equal(t, 500, got.Width) | |||
| 222 | + | require.Equal(t, 400, got.Height) | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | func (hs *harness) notifyWMName(win uint32) { | |||
| 226 | + | hs.t.Helper() | |||
| 227 | + | before := hs.x.Stats().Replies | |||
| 228 | + | hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false) | |||
| 229 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |||
| 230 | + | } | |||
A internal/adapters/wayland/xwm/focus.go +218 -0
| @@ -0,0 +1,218 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "slices" | |||
| 6 | + | ||||
| 7 | + | "bnema.dev/xturbo" | |||
| 8 | + | "bnema.dev/xturbo/xproto" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Bits of XWM.listsDirty: the client lists to rewrite at flush. | |||
| 12 | + | const ( | |||
| 13 | + | listClients uint8 = 1 << iota | |||
| 14 | + | listStacking | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // onFocusIn denies a focus change the XWM did not make: Wayland is the truth. | |||
| 18 | + | // The active window's focus is re-issued once at the next flush. | |||
| 19 | + | func (x *XWM) onFocusIn(ev *xturbo.Event) { | |||
| 20 | + | e := xproto.FocusInOf(ev) | |||
| 21 | + | if e.Detail() == xproto.NotifyDetailPointer { | |||
| 22 | + | return | |||
| 23 | + | } | |||
| 24 | + | // Keyboard grabs are how menus steal focus legitimately (wlroots does | |||
| 25 | + | // the same): fighting them would close every Steam menu. | |||
| 26 | + | if e.Mode() == xproto.NotifyModeGrab || e.Mode() == xproto.NotifyModeUngrab { | |||
| 27 | + | return | |||
| 28 | + | } | |||
| 29 | + | s := x.tab.lookup(uint32(e.Event())) | |||
| 30 | + | if s == 0 || s == x.active { | |||
| 31 | + | return | |||
| 32 | + | } | |||
| 33 | + | if r := x.tab.rec(s); r.kind == kindUnmanaged { | |||
| 34 | + | return | |||
| 35 | + | } | |||
| 36 | + | x.refocus = true | |||
| 37 | + | x.wake() | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // Focus gives keyboard focus to s; 0 means a native Wayland window or none. | |||
| 41 | + | // Focus never uses PointerRoot: without an X window to focus it is None. | |||
| 42 | + | func (x *XWM) Focus(s Slot) { | |||
| 43 | + | if x.closed { | |||
| 44 | + | return | |||
| 45 | + | } | |||
| 46 | + | r := x.managedRec(s) | |||
| 47 | + | if r == nil { | |||
| 48 | + | s = 0 | |||
| 49 | + | } | |||
| 50 | + | if s == x.active { | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | if old := x.managedRec(x.active); old != nil { | |||
| 54 | + | old.pend.activated = false | |||
| 55 | + | x.markDirty(x.active, old) | |||
| 56 | + | } | |||
| 57 | + | x.active = s | |||
| 58 | + | if r == nil { | |||
| 59 | + | x.issueFocus(0, nil) | |||
| 60 | + | x.setActive(0) | |||
| 61 | + | x.wake() | |||
| 62 | + | return | |||
| 63 | + | } | |||
| 64 | + | r.pend.activated = true | |||
| 65 | + | x.markDirty(s, r) | |||
| 66 | + | x.issueFocus(s, r) | |||
| 67 | + | x.setActive(r.xid) | |||
| 68 | + | x.raise(r) | |||
| 69 | + | x.wake() | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // issueFocus applies the ICCCM input model of r: Passive and Locally active | |||
| 73 | + | // windows get SetInputFocus, Locally and Globally active ones WM_TAKE_FOCUS, | |||
| 74 | + | // No-input windows nothing. A nil r clears the X focus. | |||
| 75 | + | func (x *XWM) issueFocus(s Slot, r *record) { | |||
| 76 | + | b := x.conn.Buf() | |||
| 77 | + | if r == nil { | |||
| 78 | + | xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0) | |||
| 79 | + | return | |||
| 80 | + | } | |||
| 81 | + | if r.input { | |||
| 82 | + | x.expect(xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.Window(r.xid), 0), kindFocus, x.tab.tag(s), 0) | |||
| 83 | + | } | |||
| 84 | + | if r.takeFocus { | |||
| 85 | + | x.sendProtocol(r.xid, atomWMTakeFocus) | |||
| 86 | + | } | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // sendProtocol sends a WM_PROTOCOLS client message [proto, CurrentTime]. | |||
| 90 | + | func (x *XWM) sendProtocol(xid uint32, proto int) { | |||
| 91 | + | var raw [32]byte | |||
| 92 | + | raw[0], raw[1] = 33, 32 // ClientMessage, format 32 | |||
| 93 | + | binary.LittleEndian.PutUint32(raw[4:], xid) | |||
| 94 | + | binary.LittleEndian.PutUint32(raw[8:], x.atoms[atomWMProtocols]) | |||
| 95 | + | binary.LittleEndian.PutUint32(raw[12:], x.atoms[proto]) | |||
| 96 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(xid), xproto.EventMaskNoEvent, &raw) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // setActive writes _NET_ACTIVE_WINDOW on the root. | |||
| 100 | + | func (x *XWM) setActive(xid uint32) { | |||
| 101 | + | v := [1]uint32{xid} | |||
| 102 | + | x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, v[:]) | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // raise restacks r above its siblings and at the top of the stacking list. | |||
| 106 | + | func (x *XWM) raise(r *record) { | |||
| 107 | + | var v xproto.ConfigureWindowValues | |||
| 108 | + | v.SetStackMode(xproto.StackModeAbove) | |||
| 109 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v) | |||
| 110 | + | if i := slices.Index(x.stacking, r.xid); i >= 0 { | |||
| 111 | + | x.stacking = append(x.stacking[:i], x.stacking[i+1:]...) | |||
| 112 | + | x.stacking = append(x.stacking, r.xid) | |||
| 113 | + | x.listsDirty |= listStacking | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client. | |||
| 118 | + | func (x *XWM) CloseWindow(s Slot) { | |||
| 119 | + | r := x.managedRec(s) | |||
| 120 | + | if r == nil { | |||
| 121 | + | return | |||
| 122 | + | } | |||
| 123 | + | if r.deleteWindow { | |||
| 124 | + | x.sendProtocol(r.xid, atomWMDeleteWindow) | |||
| 125 | + | } else { | |||
| 126 | + | xproto.KillClient(x.conn.Buf(), r.xid) | |||
| 127 | + | } | |||
| 128 | + | x.wake() | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // addClient puts a ready window at the end of both client lists. | |||
| 132 | + | func (x *XWM) addClient(xid uint32) { | |||
| 133 | + | if !slices.Contains(x.clients, xid) { | |||
| 134 | + | x.clients = append(x.clients, xid) | |||
| 135 | + | x.listsDirty |= listClients | |||
| 136 | + | } | |||
| 137 | + | if !slices.Contains(x.stacking, xid) { | |||
| 138 | + | x.stacking = append(x.stacking, xid) | |||
| 139 | + | x.listsDirty |= listStacking | |||
| 140 | + | } | |||
| 141 | + | x.wake() | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | // removeClient drops an unmapped or destroyed window from both client lists. | |||
| 145 | + | func (x *XWM) removeClient(xid uint32) { | |||
| 146 | + | if i := slices.Index(x.clients, xid); i >= 0 { | |||
| 147 | + | x.clients = slices.Delete(x.clients, i, i+1) | |||
| 148 | + | x.listsDirty |= listClients | |||
| 149 | + | } | |||
| 150 | + | if i := slices.Index(x.stacking, xid); i >= 0 { | |||
| 151 | + | x.stacking = slices.Delete(x.stacking, i, i+1) | |||
| 152 | + | x.listsDirty |= listStacking | |||
| 153 | + | } | |||
| 154 | + | x.wake() | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | // dropWindow takes s out of the client lists, the focus and the configure | |||
| 158 | + | // queue when it stops being mapped (unmap, destroy). It is idempotent. | |||
| 159 | + | func (x *XWM) dropWindow(s Slot) { | |||
| 160 | + | r := x.tab.rec(s) | |||
| 161 | + | if r != nil { | |||
| 162 | + | r.ovQueued = false // a reused slot must not inherit a queued report | |||
| 163 | + | } | |||
| 164 | + | if r == nil || r.kind != kindManaged { | |||
| 165 | + | return | |||
| 166 | + | } | |||
| 167 | + | x.removeClient(r.xid) | |||
| 168 | + | r.pend.dirty = false // the queued slot is skipped at flush | |||
| 169 | + | // A remap starts from a clean _NET_WM_STATE, so Focus writes FOCUSED again. | |||
| 170 | + | r.pend.activated, r.sent.activated = false, false | |||
| 171 | + | r.sent.fullscreen, r.stateSent = false, 0 | |||
| 172 | + | if x.active == s { | |||
| 173 | + | x.active = 0 | |||
| 174 | + | x.refocus = false | |||
| 175 | + | x.setActive(0) | |||
| 176 | + | } | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // forgetWindow ends the life of s (destroy, reparent away): it is dropped, | |||
| 180 | + | // the host is told if it knew the window, and the record is freed. | |||
| 181 | + | func (x *XWM) forgetWindow(s Slot) { | |||
| 182 | + | r := x.tab.rec(s) | |||
| 183 | + | if r == nil || r.kind == kindFree { | |||
| 184 | + | return | |||
| 185 | + | } | |||
| 186 | + | x.dropWindow(s) | |||
| 187 | + | if r.kind == kindUnmanaged && r.ovPaired && x.host != nil { | |||
| 188 | + | r.ovPaired = false | |||
| 189 | + | x.host.OverrideGone(s) | |||
| 190 | + | } | |||
| 191 | + | if r.kind == kindManaged && r.id != 0 && x.host != nil { | |||
| 192 | + | r.id = 0 | |||
| 193 | + | x.host.Dissociate(s, true) | |||
| 194 | + | if x.active == s { // the host re-focused the window while dissociating it | |||
| 195 | + | x.active, x.refocus = 0, false | |||
| 196 | + | x.issueFocus(0, nil) | |||
| 197 | + | x.setActive(0) | |||
| 198 | + | x.wake() | |||
| 199 | + | } | |||
| 200 | + | } | |||
| 201 | + | x.tab.remove(s) | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // flushFocus rewrites the client lists that changed and re-issues a denied | |||
| 205 | + | // focus, once per display iteration. | |||
| 206 | + | func (x *XWM) flushFocus() { | |||
| 207 | + | if x.listsDirty&listClients != 0 { | |||
| 208 | + | x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, x.clients) | |||
| 209 | + | } | |||
| 210 | + | if x.listsDirty&listStacking != 0 { | |||
| 211 | + | x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, x.stacking) | |||
| 212 | + | } | |||
| 213 | + | x.listsDirty = 0 | |||
| 214 | + | if x.refocus { | |||
| 215 | + | x.refocus = false | |||
| 216 | + | x.issueFocus(x.active, x.managedRec(x.active)) | |||
| 217 | + | } | |||
| 218 | + | } | |||
A internal/adapters/wayland/xwm/host.go +57 -0
| @@ -0,0 +1,57 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/neferwl/internal/ports" | |||
| 4 | + | ||||
| 5 | + | // Slot is the XWM's window slot: table index + 1, stable until the window is | |||
| 6 | + | // destroyed. 0 means none. | |||
| 7 | + | type Slot uint32 | |||
| 8 | + | ||||
| 9 | + | // WindowInfo is what the XWM learned about a managed window by the time it is | |||
| 10 | + | // ready to be mapped. The host copies it. | |||
| 11 | + | type WindowInfo struct { | |||
| 12 | + | PID int | |||
| 13 | + | Floating bool // dialog/utility/splash/fixed size | |||
| 14 | + | Width, Height int // requested size, logical (scale 1) | |||
| 15 | + | Parent Slot | |||
| 16 | + | Modal bool // _NET_WM_STATE_MODAL | |||
| 17 | + | Fullscreen bool // _NET_WM_STATE_FULLSCREEN at map | |||
| 18 | + | Title, AppID string | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | // host is the wayland server as the XWM sees it. Display goroutine only. | |||
| 22 | + | // All strings passed are owned by the XWM record and already deduplicated | |||
| 23 | + | // (only passed when changed). | |||
| 24 | + | type host interface { | |||
| 25 | + | // Created: a managed (non-override-redirect) X window is known; the host | |||
| 26 | + | // allocates a ports.WindowID and returns it. Not yet mapped. | |||
| 27 | + | Created(slot Slot, xid uint32) ports.WindowID | |||
| 28 | + | // Ready: properties read and MapWindow issued; the surface may still be | |||
| 29 | + | // missing. Info is copied by the host. | |||
| 30 | + | Ready(slot Slot, info WindowInfo) | |||
| 31 | + | // Associate links the X window to the wl_surface with identity surfaceID. | |||
| 32 | + | // The host maps the window on the surface's first buffer commit. | |||
| 33 | + | Associate(slot Slot, surfaceID uint64) | |||
| 34 | + | // Dissociate: UnmapNotify/DestroyNotify; the host emits WindowUnmapped if | |||
| 35 | + | // mapped and drops the surface link. destroyed also frees the WindowID. | |||
| 36 | + | Dissociate(slot Slot, destroyed bool) | |||
| 37 | + | Title(slot Slot, title string) // changed only | |||
| 38 | + | AppID(slot Slot, appID string) // changed only (WM_CLASS class) | |||
| 39 | + | Parent(slot Slot, parent Slot) // 0 = none; after a WM_TRANSIENT_FOR change | |||
| 40 | + | // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message | |||
| 41 | + | // or initial state); the host emits WindowFullscreenRequest. | |||
| 42 | + | Fullscreen(slot Slot, on bool) | |||
| 43 | + | ||||
| 44 | + | // OverrideMapped reports an override-redirect window that is mapped, | |||
| 45 | + | // typed and paired with its wl_surface. Rect is in layout coordinates. | |||
| 46 | + | OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x, y, w, h int, noInput bool) | |||
| 47 | + | // OverrideMoved reports a changed layout rect of a mapped override window. | |||
| 48 | + | OverrideMoved(slot Slot, x, y, w, h int) | |||
| 49 | + | // OverrideGone ends a mapped override window: unmapped or destroyed. | |||
| 50 | + | OverrideGone(slot Slot) | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // fdSource is the display's event source for the X socket (*server.Source). | |||
| 54 | + | type fdSource interface { | |||
| 55 | + | SetWritable(on bool) error | |||
| 56 | + | Remove() | |||
| 57 | + | } | |||
A internal/adapters/wayland/xwm/host_mock_test.go +611 -0
| @@ -0,0 +1,611 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package xwm | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | mock "github.com/stretchr/testify/mock" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // newMockhost creates a new instance of mockhost. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 13 | + | // The first argument is typically a *testing.T value. | |||
| 14 | + | func newMockhost(t interface { | |||
| 15 | + | mock.TestingT | |||
| 16 | + | Cleanup(func()) | |||
| 17 | + | }) *mockhost { | |||
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 19 | + | helper.Helper() | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | mock := &mockhost{} | |||
| 23 | + | mock.Mock.Test(t) | |||
| 24 | + | ||||
| 25 | + | t.Cleanup(func() { | |||
| 26 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 27 | + | helper.Helper() | |||
| 28 | + | } | |||
| 29 | + | mock.AssertExpectations(t) | |||
| 30 | + | }) | |||
| 31 | + | ||||
| 32 | + | return mock | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // mockhost is an autogenerated mock type for the host type | |||
| 36 | + | type mockhost struct { | |||
| 37 | + | mock.Mock | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | type mockhost_Expecter struct { | |||
| 41 | + | mock *mock.Mock | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func (_m *mockhost) EXPECT() *mockhost_Expecter { | |||
| 45 | + | return &mockhost_Expecter{mock: &_m.Mock} | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // AppID provides a mock function for the type mockhost | |||
| 49 | + | func (_mock *mockhost) AppID(slot Slot, appID string) { | |||
| 50 | + | _mock.Called(slot, appID) | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // mockhost_AppID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppID' | |||
| 55 | + | type mockhost_AppID_Call struct { | |||
| 56 | + | *mock.Call | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // AppID is a helper method to define mock.On call | |||
| 60 | + | // - slot Slot | |||
| 61 | + | // - appID string | |||
| 62 | + | func (_e *mockhost_Expecter) AppID(slot any, appID any) *mockhost_AppID_Call { | |||
| 63 | + | return &mockhost_AppID_Call{Call: _e.mock.On("AppID", slot, appID)} | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func (_c *mockhost_AppID_Call) Run(run func(slot Slot, appID string)) *mockhost_AppID_Call { | |||
| 67 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 68 | + | var arg0 Slot | |||
| 69 | + | if args[0] != nil { | |||
| 70 | + | arg0 = args[0].(Slot) | |||
| 71 | + | } | |||
| 72 | + | var arg1 string | |||
| 73 | + | if args[1] != nil { | |||
| 74 | + | arg1 = args[1].(string) | |||
| 75 | + | } | |||
| 76 | + | run( | |||
| 77 | + | arg0, | |||
| 78 | + | arg1, | |||
| 79 | + | ) | |||
| 80 | + | }) | |||
| 81 | + | return _c | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | func (_c *mockhost_AppID_Call) Return() *mockhost_AppID_Call { | |||
| 85 | + | _c.Call.Return() | |||
| 86 | + | return _c | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func (_c *mockhost_AppID_Call) RunAndReturn(run func(slot Slot, appID string)) *mockhost_AppID_Call { | |||
| 90 | + | _c.Run(run) | |||
| 91 | + | return _c | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // Associate provides a mock function for the type mockhost | |||
| 95 | + | func (_mock *mockhost) Associate(slot Slot, surfaceID uint64) { | |||
| 96 | + | _mock.Called(slot, surfaceID) | |||
| 97 | + | return | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // mockhost_Associate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Associate' | |||
| 101 | + | type mockhost_Associate_Call struct { | |||
| 102 | + | *mock.Call | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // Associate is a helper method to define mock.On call | |||
| 106 | + | // - slot Slot | |||
| 107 | + | // - surfaceID uint64 | |||
| 108 | + | func (_e *mockhost_Expecter) Associate(slot any, surfaceID any) *mockhost_Associate_Call { | |||
| 109 | + | return &mockhost_Associate_Call{Call: _e.mock.On("Associate", slot, surfaceID)} | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | func (_c *mockhost_Associate_Call) Run(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call { | |||
| 113 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 114 | + | var arg0 Slot | |||
| 115 | + | if args[0] != nil { | |||
| 116 | + | arg0 = args[0].(Slot) | |||
| 117 | + | } | |||
| 118 | + | var arg1 uint64 | |||
| 119 | + | if args[1] != nil { | |||
| 120 | + | arg1 = args[1].(uint64) | |||
| 121 | + | } | |||
| 122 | + | run( | |||
| 123 | + | arg0, | |||
| 124 | + | arg1, | |||
| 125 | + | ) | |||
| 126 | + | }) | |||
| 127 | + | return _c | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | func (_c *mockhost_Associate_Call) Return() *mockhost_Associate_Call { | |||
| 131 | + | _c.Call.Return() | |||
| 132 | + | return _c | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | func (_c *mockhost_Associate_Call) RunAndReturn(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call { | |||
| 136 | + | _c.Run(run) | |||
| 137 | + | return _c | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | // Created provides a mock function for the type mockhost | |||
| 141 | + | func (_mock *mockhost) Created(slot Slot, xid uint32) ports.WindowID { | |||
| 142 | + | ret := _mock.Called(slot, xid) | |||
| 143 | + | ||||
| 144 | + | if len(ret) == 0 { | |||
| 145 | + | panic("no return value specified for Created") | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | var r0 ports.WindowID | |||
| 149 | + | if returnFunc, ok := ret.Get(0).(func(Slot, uint32) ports.WindowID); ok { | |||
| 150 | + | r0 = returnFunc(slot, xid) | |||
| 151 | + | } else { | |||
| 152 | + | r0 = ret.Get(0).(ports.WindowID) | |||
| 153 | + | } | |||
| 154 | + | return r0 | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | // mockhost_Created_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Created' | |||
| 158 | + | type mockhost_Created_Call struct { | |||
| 159 | + | *mock.Call | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // Created is a helper method to define mock.On call | |||
| 163 | + | // - slot Slot | |||
| 164 | + | // - xid uint32 | |||
| 165 | + | func (_e *mockhost_Expecter) Created(slot any, xid any) *mockhost_Created_Call { | |||
| 166 | + | return &mockhost_Created_Call{Call: _e.mock.On("Created", slot, xid)} | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | func (_c *mockhost_Created_Call) Run(run func(slot Slot, xid uint32)) *mockhost_Created_Call { | |||
| 170 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 171 | + | var arg0 Slot | |||
| 172 | + | if args[0] != nil { | |||
| 173 | + | arg0 = args[0].(Slot) | |||
| 174 | + | } | |||
| 175 | + | var arg1 uint32 | |||
| 176 | + | if args[1] != nil { | |||
| 177 | + | arg1 = args[1].(uint32) | |||
| 178 | + | } | |||
| 179 | + | run( | |||
| 180 | + | arg0, | |||
| 181 | + | arg1, | |||
| 182 | + | ) | |||
| 183 | + | }) | |||
| 184 | + | return _c | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | func (_c *mockhost_Created_Call) Return(windowID ports.WindowID) *mockhost_Created_Call { | |||
| 188 | + | _c.Call.Return(windowID) | |||
| 189 | + | return _c | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | func (_c *mockhost_Created_Call) RunAndReturn(run func(slot Slot, xid uint32) ports.WindowID) *mockhost_Created_Call { | |||
| 193 | + | _c.Call.Return(run) | |||
| 194 | + | return _c | |||
| 195 | + | } | |||
| 196 | + | ||||
| 197 | + | // Dissociate provides a mock function for the type mockhost | |||
| 198 | + | func (_mock *mockhost) Dissociate(slot Slot, destroyed bool) { | |||
| 199 | + | _mock.Called(slot, destroyed) | |||
| 200 | + | return | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // mockhost_Dissociate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dissociate' | |||
| 204 | + | type mockhost_Dissociate_Call struct { | |||
| 205 | + | *mock.Call | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // Dissociate is a helper method to define mock.On call | |||
| 209 | + | // - slot Slot | |||
| 210 | + | // - destroyed bool | |||
| 211 | + | func (_e *mockhost_Expecter) Dissociate(slot any, destroyed any) *mockhost_Dissociate_Call { | |||
| 212 | + | return &mockhost_Dissociate_Call{Call: _e.mock.On("Dissociate", slot, destroyed)} | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | func (_c *mockhost_Dissociate_Call) Run(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call { | |||
| 216 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 217 | + | var arg0 Slot | |||
| 218 | + | if args[0] != nil { | |||
| 219 | + | arg0 = args[0].(Slot) | |||
| 220 | + | } | |||
| 221 | + | var arg1 bool | |||
| 222 | + | if args[1] != nil { | |||
| 223 | + | arg1 = args[1].(bool) | |||
| 224 | + | } | |||
| 225 | + | run( | |||
| 226 | + | arg0, | |||
| 227 | + | arg1, | |||
| 228 | + | ) | |||
| 229 | + | }) | |||
| 230 | + | return _c | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | func (_c *mockhost_Dissociate_Call) Return() *mockhost_Dissociate_Call { | |||
| 234 | + | _c.Call.Return() | |||
| 235 | + | return _c | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | func (_c *mockhost_Dissociate_Call) RunAndReturn(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call { | |||
| 239 | + | _c.Run(run) | |||
| 240 | + | return _c | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | // Fullscreen provides a mock function for the type mockhost | |||
| 244 | + | func (_mock *mockhost) Fullscreen(slot Slot, on bool) { | |||
| 245 | + | _mock.Called(slot, on) | |||
| 246 | + | return | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | // mockhost_Fullscreen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fullscreen' | |||
| 250 | + | type mockhost_Fullscreen_Call struct { | |||
| 251 | + | *mock.Call | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | // Fullscreen is a helper method to define mock.On call | |||
| 255 | + | // - slot Slot | |||
| 256 | + | // - on bool | |||
| 257 | + | func (_e *mockhost_Expecter) Fullscreen(slot any, on any) *mockhost_Fullscreen_Call { | |||
| 258 | + | return &mockhost_Fullscreen_Call{Call: _e.mock.On("Fullscreen", slot, on)} | |||
| 259 | + | } | |||
| 260 | + | ||||
| 261 | + | func (_c *mockhost_Fullscreen_Call) Run(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call { | |||
| 262 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 263 | + | var arg0 Slot | |||
| 264 | + | if args[0] != nil { | |||
| 265 | + | arg0 = args[0].(Slot) | |||
| 266 | + | } | |||
| 267 | + | var arg1 bool | |||
| 268 | + | if args[1] != nil { | |||
| 269 | + | arg1 = args[1].(bool) | |||
| 270 | + | } | |||
| 271 | + | run( | |||
| 272 | + | arg0, | |||
| 273 | + | arg1, | |||
| 274 | + | ) | |||
| 275 | + | }) | |||
| 276 | + | return _c | |||
| 277 | + | } | |||
| 278 | + | ||||
| 279 | + | func (_c *mockhost_Fullscreen_Call) Return() *mockhost_Fullscreen_Call { | |||
| 280 | + | _c.Call.Return() | |||
| 281 | + | return _c | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call { | |||
| 285 | + | _c.Run(run) | |||
| 286 | + | return _c | |||
| 287 | + | } | |||
| 288 | + | ||||
| 289 | + | // OverrideGone provides a mock function for the type mockhost | |||
| 290 | + | func (_mock *mockhost) OverrideGone(slot Slot) { | |||
| 291 | + | _mock.Called(slot) | |||
| 292 | + | return | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | // mockhost_OverrideGone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideGone' | |||
| 296 | + | type mockhost_OverrideGone_Call struct { | |||
| 297 | + | *mock.Call | |||
| 298 | + | } | |||
| 299 | + | ||||
| 300 | + | // OverrideGone is a helper method to define mock.On call | |||
| 301 | + | // - slot Slot | |||
| 302 | + | func (_e *mockhost_Expecter) OverrideGone(slot any) *mockhost_OverrideGone_Call { | |||
| 303 | + | return &mockhost_OverrideGone_Call{Call: _e.mock.On("OverrideGone", slot)} | |||
| 304 | + | } | |||
| 305 | + | ||||
| 306 | + | func (_c *mockhost_OverrideGone_Call) Run(run func(slot Slot)) *mockhost_OverrideGone_Call { | |||
| 307 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 308 | + | var arg0 Slot | |||
| 309 | + | if args[0] != nil { | |||
| 310 | + | arg0 = args[0].(Slot) | |||
| 311 | + | } | |||
| 312 | + | run( | |||
| 313 | + | arg0, | |||
| 314 | + | ) | |||
| 315 | + | }) | |||
| 316 | + | return _c | |||
| 317 | + | } | |||
| 318 | + | ||||
| 319 | + | func (_c *mockhost_OverrideGone_Call) Return() *mockhost_OverrideGone_Call { | |||
| 320 | + | _c.Call.Return() | |||
| 321 | + | return _c | |||
| 322 | + | } | |||
| 323 | + | ||||
| 324 | + | func (_c *mockhost_OverrideGone_Call) RunAndReturn(run func(slot Slot)) *mockhost_OverrideGone_Call { | |||
| 325 | + | _c.Run(run) | |||
| 326 | + | return _c | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | // OverrideMapped provides a mock function for the type mockhost | |||
| 330 | + | func (_mock *mockhost) OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool) { | |||
| 331 | + | _mock.Called(slot, xid, surfaceID, x, y, w, h, noInput) | |||
| 332 | + | return | |||
| 333 | + | } | |||
| 334 | + | ||||
| 335 | + | // mockhost_OverrideMapped_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMapped' | |||
| 336 | + | type mockhost_OverrideMapped_Call struct { | |||
| 337 | + | *mock.Call | |||
| 338 | + | } | |||
| 339 | + | ||||
| 340 | + | // OverrideMapped is a helper method to define mock.On call | |||
| 341 | + | // - slot Slot | |||
| 342 | + | // - xid uint32 | |||
| 343 | + | // - surfaceID uint64 | |||
| 344 | + | // - x int | |||
| 345 | + | // - y int | |||
| 346 | + | // - w int | |||
| 347 | + | // - h int | |||
| 348 | + | // - noInput bool | |||
| 349 | + | func (_e *mockhost_Expecter) OverrideMapped(slot any, xid any, surfaceID any, x any, y any, w any, h any, noInput any) *mockhost_OverrideMapped_Call { | |||
| 350 | + | return &mockhost_OverrideMapped_Call{Call: _e.mock.On("OverrideMapped", slot, xid, surfaceID, x, y, w, h, noInput)} | |||
| 351 | + | } | |||
| 352 | + | ||||
| 353 | + | func (_c *mockhost_OverrideMapped_Call) Run(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call { | |||
| 354 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 355 | + | var arg0 Slot | |||
| 356 | + | if args[0] != nil { | |||
| 357 | + | arg0 = args[0].(Slot) | |||
| 358 | + | } | |||
| 359 | + | var arg1 uint32 | |||
| 360 | + | if args[1] != nil { | |||
| 361 | + | arg1 = args[1].(uint32) | |||
| 362 | + | } | |||
| 363 | + | var arg2 uint64 | |||
| 364 | + | if args[2] != nil { | |||
| 365 | + | arg2 = args[2].(uint64) | |||
| 366 | + | } | |||
| 367 | + | var arg3 int | |||
| 368 | + | if args[3] != nil { | |||
| 369 | + | arg3 = args[3].(int) | |||
| 370 | + | } | |||
| 371 | + | var arg4 int | |||
| 372 | + | if args[4] != nil { | |||
| 373 | + | arg4 = args[4].(int) | |||
| 374 | + | } | |||
| 375 | + | var arg5 int | |||
| 376 | + | if args[5] != nil { | |||
| 377 | + | arg5 = args[5].(int) | |||
| 378 | + | } | |||
| 379 | + | var arg6 int | |||
| 380 | + | if args[6] != nil { | |||
| 381 | + | arg6 = args[6].(int) | |||
| 382 | + | } | |||
| 383 | + | var arg7 bool | |||
| 384 | + | if args[7] != nil { | |||
| 385 | + | arg7 = args[7].(bool) | |||
| 386 | + | } | |||
| 387 | + | run( | |||
| 388 | + | arg0, | |||
| 389 | + | arg1, | |||
| 390 | + | arg2, | |||
| 391 | + | arg3, | |||
| 392 | + | arg4, | |||
| 393 | + | arg5, | |||
| 394 | + | arg6, | |||
| 395 | + | arg7, | |||
| 396 | + | ) | |||
| 397 | + | }) | |||
| 398 | + | return _c | |||
| 399 | + | } | |||
| 400 | + | ||||
| 401 | + | func (_c *mockhost_OverrideMapped_Call) Return() *mockhost_OverrideMapped_Call { | |||
| 402 | + | _c.Call.Return() | |||
| 403 | + | return _c | |||
| 404 | + | } | |||
| 405 | + | ||||
| 406 | + | func (_c *mockhost_OverrideMapped_Call) RunAndReturn(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call { | |||
| 407 | + | _c.Run(run) | |||
| 408 | + | return _c | |||
| 409 | + | } | |||
| 410 | + | ||||
| 411 | + | // OverrideMoved provides a mock function for the type mockhost | |||
| 412 | + | func (_mock *mockhost) OverrideMoved(slot Slot, x int, y int, w int, h int) { | |||
| 413 | + | _mock.Called(slot, x, y, w, h) | |||
| 414 | + | return | |||
| 415 | + | } | |||
| 416 | + | ||||
| 417 | + | // mockhost_OverrideMoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMoved' | |||
| 418 | + | type mockhost_OverrideMoved_Call struct { | |||
| 419 | + | *mock.Call | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | // OverrideMoved is a helper method to define mock.On call | |||
| 423 | + | // - slot Slot | |||
| 424 | + | // - x int | |||
| 425 | + | // - y int | |||
| 426 | + | // - w int | |||
| 427 | + | // - h int | |||
| 428 | + | func (_e *mockhost_Expecter) OverrideMoved(slot any, x any, y any, w any, h any) *mockhost_OverrideMoved_Call { | |||
| 429 | + | return &mockhost_OverrideMoved_Call{Call: _e.mock.On("OverrideMoved", slot, x, y, w, h)} | |||
| 430 | + | } | |||
| 431 | + | ||||
| 432 | + | func (_c *mockhost_OverrideMoved_Call) Run(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call { | |||
| 433 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 434 | + | var arg0 Slot | |||
| 435 | + | if args[0] != nil { | |||
| 436 | + | arg0 = args[0].(Slot) | |||
| 437 | + | } | |||
| 438 | + | var arg1 int | |||
| 439 | + | if args[1] != nil { | |||
| 440 | + | arg1 = args[1].(int) | |||
| 441 | + | } | |||
| 442 | + | var arg2 int | |||
| 443 | + | if args[2] != nil { | |||
| 444 | + | arg2 = args[2].(int) | |||
| 445 | + | } | |||
| 446 | + | var arg3 int | |||
| 447 | + | if args[3] != nil { | |||
| 448 | + | arg3 = args[3].(int) | |||
| 449 | + | } | |||
| 450 | + | var arg4 int | |||
| 451 | + | if args[4] != nil { | |||
| 452 | + | arg4 = args[4].(int) | |||
| 453 | + | } | |||
| 454 | + | run( | |||
| 455 | + | arg0, | |||
| 456 | + | arg1, | |||
| 457 | + | arg2, | |||
| 458 | + | arg3, | |||
| 459 | + | arg4, | |||
| 460 | + | ) | |||
| 461 | + | }) | |||
| 462 | + | return _c | |||
| 463 | + | } | |||
| 464 | + | ||||
| 465 | + | func (_c *mockhost_OverrideMoved_Call) Return() *mockhost_OverrideMoved_Call { | |||
| 466 | + | _c.Call.Return() | |||
| 467 | + | return _c | |||
| 468 | + | } | |||
| 469 | + | ||||
| 470 | + | func (_c *mockhost_OverrideMoved_Call) RunAndReturn(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call { | |||
| 471 | + | _c.Run(run) | |||
| 472 | + | return _c | |||
| 473 | + | } | |||
| 474 | + | ||||
| 475 | + | // Parent provides a mock function for the type mockhost | |||
| 476 | + | func (_mock *mockhost) Parent(slot Slot, parent Slot) { | |||
| 477 | + | _mock.Called(slot, parent) | |||
| 478 | + | return | |||
| 479 | + | } | |||
| 480 | + | ||||
| 481 | + | // mockhost_Parent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parent' | |||
| 482 | + | type mockhost_Parent_Call struct { | |||
| 483 | + | *mock.Call | |||
| 484 | + | } | |||
| 485 | + | ||||
| 486 | + | // Parent is a helper method to define mock.On call | |||
| 487 | + | // - slot Slot | |||
| 488 | + | // - parent Slot | |||
| 489 | + | func (_e *mockhost_Expecter) Parent(slot any, parent any) *mockhost_Parent_Call { | |||
| 490 | + | return &mockhost_Parent_Call{Call: _e.mock.On("Parent", slot, parent)} | |||
| 491 | + | } | |||
| 492 | + | ||||
| 493 | + | func (_c *mockhost_Parent_Call) Run(run func(slot Slot, parent Slot)) *mockhost_Parent_Call { | |||
| 494 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 495 | + | var arg0 Slot | |||
| 496 | + | if args[0] != nil { | |||
| 497 | + | arg0 = args[0].(Slot) | |||
| 498 | + | } | |||
| 499 | + | var arg1 Slot | |||
| 500 | + | if args[1] != nil { | |||
| 501 | + | arg1 = args[1].(Slot) | |||
| 502 | + | } | |||
| 503 | + | run( | |||
| 504 | + | arg0, | |||
| 505 | + | arg1, | |||
| 506 | + | ) | |||
| 507 | + | }) | |||
| 508 | + | return _c | |||
| 509 | + | } | |||
| 510 | + | ||||
| 511 | + | func (_c *mockhost_Parent_Call) Return() *mockhost_Parent_Call { | |||
| 512 | + | _c.Call.Return() | |||
| 513 | + | return _c | |||
| 514 | + | } | |||
| 515 | + | ||||
| 516 | + | func (_c *mockhost_Parent_Call) RunAndReturn(run func(slot Slot, parent Slot)) *mockhost_Parent_Call { | |||
| 517 | + | _c.Run(run) | |||
| 518 | + | return _c | |||
| 519 | + | } | |||
| 520 | + | ||||
| 521 | + | // Ready provides a mock function for the type mockhost | |||
| 522 | + | func (_mock *mockhost) Ready(slot Slot, info WindowInfo) { | |||
| 523 | + | _mock.Called(slot, info) | |||
| 524 | + | return | |||
| 525 | + | } | |||
| 526 | + | ||||
| 527 | + | // mockhost_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready' | |||
| 528 | + | type mockhost_Ready_Call struct { | |||
| 529 | + | *mock.Call | |||
| 530 | + | } | |||
| 531 | + | ||||
| 532 | + | // Ready is a helper method to define mock.On call | |||
| 533 | + | // - slot Slot | |||
| 534 | + | // - info WindowInfo | |||
| 535 | + | func (_e *mockhost_Expecter) Ready(slot any, info any) *mockhost_Ready_Call { | |||
| 536 | + | return &mockhost_Ready_Call{Call: _e.mock.On("Ready", slot, info)} | |||
| 537 | + | } | |||
| 538 | + | ||||
| 539 | + | func (_c *mockhost_Ready_Call) Run(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call { | |||
| 540 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 541 | + | var arg0 Slot | |||
| 542 | + | if args[0] != nil { | |||
| 543 | + | arg0 = args[0].(Slot) | |||
| 544 | + | } | |||
| 545 | + | var arg1 WindowInfo | |||
| 546 | + | if args[1] != nil { | |||
| 547 | + | arg1 = args[1].(WindowInfo) | |||
| 548 | + | } | |||
| 549 | + | run( | |||
| 550 | + | arg0, | |||
| 551 | + | arg1, | |||
| 552 | + | ) | |||
| 553 | + | }) | |||
| 554 | + | return _c | |||
| 555 | + | } | |||
| 556 | + | ||||
| 557 | + | func (_c *mockhost_Ready_Call) Return() *mockhost_Ready_Call { | |||
| 558 | + | _c.Call.Return() | |||
| 559 | + | return _c | |||
| 560 | + | } | |||
| 561 | + | ||||
| 562 | + | func (_c *mockhost_Ready_Call) RunAndReturn(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call { | |||
| 563 | + | _c.Run(run) | |||
| 564 | + | return _c | |||
| 565 | + | } | |||
| 566 | + | ||||
| 567 | + | // Title provides a mock function for the type mockhost | |||
| 568 | + | func (_mock *mockhost) Title(slot Slot, title string) { | |||
| 569 | + | _mock.Called(slot, title) | |||
| 570 | + | return | |||
| 571 | + | } | |||
| 572 | + | ||||
| 573 | + | // mockhost_Title_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Title' | |||
| 574 | + | type mockhost_Title_Call struct { | |||
| 575 | + | *mock.Call | |||
| 576 | + | } | |||
| 577 | + | ||||
| 578 | + | // Title is a helper method to define mock.On call | |||
| 579 | + | // - slot Slot | |||
| 580 | + | // - title string | |||
| 581 | + | func (_e *mockhost_Expecter) Title(slot any, title any) *mockhost_Title_Call { | |||
| 582 | + | return &mockhost_Title_Call{Call: _e.mock.On("Title", slot, title)} | |||
| 583 | + | } | |||
| 584 | + | ||||
| 585 | + | func (_c *mockhost_Title_Call) Run(run func(slot Slot, title string)) *mockhost_Title_Call { | |||
| 586 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 587 | + | var arg0 Slot | |||
| 588 | + | if args[0] != nil { | |||
| 589 | + | arg0 = args[0].(Slot) | |||
| 590 | + | } | |||
| 591 | + | var arg1 string | |||
| 592 | + | if args[1] != nil { | |||
| 593 | + | arg1 = args[1].(string) | |||
| 594 | + | } | |||
| 595 | + | run( | |||
| 596 | + | arg0, | |||
| 597 | + | arg1, | |||
| 598 | + | ) | |||
| 599 | + | }) | |||
| 600 | + | return _c | |||
| 601 | + | } | |||
| 602 | + | ||||
| 603 | + | func (_c *mockhost_Title_Call) Return() *mockhost_Title_Call { | |||
| 604 | + | _c.Call.Return() | |||
| 605 | + | return _c | |||
| 606 | + | } | |||
| 607 | + | ||||
| 608 | + | func (_c *mockhost_Title_Call) RunAndReturn(run func(slot Slot, title string)) *mockhost_Title_Call { | |||
| 609 | + | _c.Run(run) | |||
| 610 | + | return _c | |||
| 611 | + | } | |||
A internal/adapters/wayland/xwm/mapflow.go +355 -0
| @@ -0,0 +1,355 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bnema.dev/xturbo" | |||
| 5 | + | "bnema.dev/xturbo/xproto" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // ICCCM WM_STATE values. | |||
| 9 | + | const ( | |||
| 10 | + | wmStateWithdrawn uint32 = 0 | |||
| 11 | + | wmStateNormal uint32 = 1 | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // _NET_WM_STATE client message actions. | |||
| 15 | + | const ( | |||
| 16 | + | netStateRemove = 0 | |||
| 17 | + | netStateAdd = 1 | |||
| 18 | + | netStateToggle = 2 | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | // beginMap starts the map flow of a managed window: every property, the | |||
| 22 | + | // geometry and MapWindow go out in one batch (wlroots order). Also called by | |||
| 23 | + | // Attach for windows mapped before it. | |||
| 24 | + | func (x *XWM) beginMap(s Slot) { | |||
| 25 | + | rec := x.tab.rec(s) | |||
| 26 | + | if rec.kind != kindManaged || rec.mapping { | |||
| 27 | + | return | |||
| 28 | + | } | |||
| 29 | + | rec.mapping, rec.ready = true, false | |||
| 30 | + | rec.hasNetName = false | |||
| 31 | + | rec.pending = propGeom | |||
| 32 | + | xid := rec.xid | |||
| 33 | + | for idx := 0; idx < propCount; idx++ { | |||
| 34 | + | x.requestProp(s, idx) | |||
| 35 | + | } | |||
| 36 | + | b := x.conn.Buf() | |||
| 37 | + | x.expect(xproto.GetGeometry(b, xproto.Drawable(xid)), kindGeom, x.tab.tag(s), 0) | |||
| 38 | + | xproto.MapWindow(b, xproto.Window(xid)) | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func (x *XWM) onMapRequest(ev *xturbo.Event) { | |||
| 42 | + | if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 { | |||
| 43 | + | x.beginMap(s) | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func (x *XWM) onMapNotify(ev *xturbo.Event) { | |||
| 48 | + | s := x.tab.lookup(uint32(xproto.MapNotifyOf(ev).Window())) | |||
| 49 | + | if s == 0 { | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | rec := x.tab.rec(s) | |||
| 53 | + | again := rec.mapped | |||
| 54 | + | rec.mapped = true | |||
| 55 | + | if rec.kind == kindUnmanaged && !again { | |||
| 56 | + | x.requestOverrideType(s) | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | // onUnmapNotify ends the mapped life of a managed window; a later MapRequest | |||
| 61 | + | // starts over and re-reads the properties. Synthetic events (ICCCM unmap | |||
| 62 | + | // requests) are not an unmap. | |||
| 63 | + | func (x *XWM) onUnmapNotify(ev *xturbo.Event) { | |||
| 64 | + | if ev.SendEvent { | |||
| 65 | + | return | |||
| 66 | + | } | |||
| 67 | + | s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window())) | |||
| 68 | + | if s == 0 { | |||
| 69 | + | return | |||
| 70 | + | } | |||
| 71 | + | rec := x.tab.rec(s) | |||
| 72 | + | rec.mapped = false | |||
| 73 | + | if rec.kind == kindUnmanaged { | |||
| 74 | + | x.overrideUnmapped(s, rec) | |||
| 75 | + | return | |||
| 76 | + | } | |||
| 77 | + | if rec.kind != kindManaged { | |||
| 78 | + | return | |||
| 79 | + | } | |||
| 80 | + | wasMapping := rec.mapping | |||
| 81 | + | rec.ready, rec.mapping, rec.pending, rec.sized = false, false, 0, false | |||
| 82 | + | rec.gen++ // replies still in flight for this map name a dead tag | |||
| 83 | + | if rec.gen == 0 { | |||
| 84 | + | rec.gen = 1 | |||
| 85 | + | } | |||
| 86 | + | rec.pend.dirty = false | |||
| 87 | + | rec.serial, rec.surface = 0, 0 | |||
| 88 | + | if wasMapping { | |||
| 89 | + | xid := rec.xid | |||
| 90 | + | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateWithdrawn, 0}) | |||
| 91 | + | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(xid), xproto.Atom(x.atoms[atomNetWMState])) | |||
| 92 | + | } | |||
| 93 | + | x.dropWindow(s) | |||
| 94 | + | if rec.id != 0 { | |||
| 95 | + | x.host.Dissociate(s, false) | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // setProp32Typed is setProp32 with an explicit property type atom. | |||
| 100 | + | func (x *XWM) setProp32Typed(win uint32, prop int, typ uint32, vals []uint32) { | |||
| 101 | + | x.setProp32(win, prop, xproto.Atom(typ), vals) | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // checkReady fires Ready once every awaited reply is in. | |||
| 105 | + | func (x *XWM) checkReady(s Slot) { | |||
| 106 | + | rec := x.tab.rec(s) | |||
| 107 | + | if rec == nil || rec.pending != 0 || rec.ready || !rec.mapping || rec.kind != kindManaged || x.host == nil { | |||
| 108 | + | return | |||
| 109 | + | } | |||
| 110 | + | rec.ready = true | |||
| 111 | + | x.classify(rec) | |||
| 112 | + | info := x.info(s) | |||
| 113 | + | xid := rec.xid | |||
| 114 | + | ||||
| 115 | + | var state [2]uint32 | |||
| 116 | + | n := 0 | |||
| 117 | + | if rec.modal { | |||
| 118 | + | state[n] = x.atoms[atomNetWMStateModal] | |||
| 119 | + | n++ | |||
| 120 | + | } | |||
| 121 | + | if rec.fullscreen { | |||
| 122 | + | state[n] = x.atoms[atomNetWMStateFullscreen] | |||
| 123 | + | n++ | |||
| 124 | + | } | |||
| 125 | + | x.setProp32(xid, atomNetWMState, xproto.AtomAtom, state[:n]) | |||
| 126 | + | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateNormal, 0}) | |||
| 127 | + | x.addClient(xid) | |||
| 128 | + | ||||
| 129 | + | x.host.Ready(s, info) | |||
| 130 | + | x.tryPair(s) | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | // info is what the host learns when a window is ready. | |||
| 134 | + | func (x *XWM) info(s Slot) WindowInfo { | |||
| 135 | + | rec := x.tab.rec(s) | |||
| 136 | + | parent := x.managedSlot(rec.transient) | |||
| 137 | + | info := WindowInfo{ | |||
| 138 | + | PID: int(rec.pid), | |||
| 139 | + | Floating: rec.floating, | |||
| 140 | + | Parent: parent, | |||
| 141 | + | Modal: rec.modal, | |||
| 142 | + | Fullscreen: rec.fullscreen, | |||
| 143 | + | Title: rec.title, | |||
| 144 | + | AppID: rec.class, | |||
| 145 | + | } | |||
| 146 | + | if info.Floating || rec.sized { | |||
| 147 | + | info.Width, info.Height = int(rec.w), int(rec.h) | |||
| 148 | + | } | |||
| 149 | + | return info | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | func (x *XWM) onGeomReply(s Slot, rec *record, r *xturbo.Reply) { | |||
| 153 | + | if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok { | |||
| 154 | + | rec.x, rec.y, rec.w, rec.h = int32(g.X()), int32(g.Y()), uint32(g.Width()), uint32(g.Height()) | |||
| 155 | + | } | |||
| 156 | + | rec.pending &^= propGeom | |||
| 157 | + | x.checkReady(s) | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | // onClientMessage handles WL_SURFACE_SERIAL (pairing), the legacy | |||
| 161 | + | // WL_SURFACE_ID (counted, ignored) and _NET_WM_STATE requests. | |||
| 162 | + | func (x *XWM) onClientMessage(ev *xturbo.Event) { | |||
| 163 | + | e := xproto.ClientMessageOf(ev) | |||
| 164 | + | if e.Format() != 32 { | |||
| 165 | + | return | |||
| 166 | + | } | |||
| 167 | + | typ := uint32(e.Type()) | |||
| 168 | + | switch typ { | |||
| 169 | + | case x.atoms[atomWLSurfaceSerial]: | |||
| 170 | + | s := x.tab.lookup(uint32(e.Window())) | |||
| 171 | + | if s == 0 { | |||
| 172 | + | return | |||
| 173 | + | } | |||
| 174 | + | rec := x.tab.rec(s) | |||
| 175 | + | if rec.serial != 0 { | |||
| 176 | + | x.log.Debug().Uint32("xid", rec.xid).Msg("duplicate WL_SURFACE_SERIAL ignored") | |||
| 177 | + | return | |||
| 178 | + | } | |||
| 179 | + | d := e.Data().Data32() | |||
| 180 | + | rec.serial = uint64(d[1])<<32 | uint64(d[0]) | |||
| 181 | + | x.tryPair(s) | |||
| 182 | + | case x.atoms[atomWLSurfaceID]: | |||
| 183 | + | x.stats.LegacyID++ | |||
| 184 | + | if x.stats.LegacyID == 1 { | |||
| 185 | + | x.log.Warn().Msg("WL_SURFACE_ID received: Xwayland did not bind xwayland_shell_v1, window ignored") | |||
| 186 | + | } | |||
| 187 | + | case x.atoms[atomNetWMState]: | |||
| 188 | + | x.onNetWMState(e) | |||
| 189 | + | case x.atoms[atomWMChangeState]: | |||
| 190 | + | // Iconify requests are ignored: windows stay mapped. | |||
| 191 | + | } | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | // onNetWMState forwards fullscreen requests to the host and records them; the | |||
| 195 | + | // property itself is written when core answers with a configure. | |||
| 196 | + | func (x *XWM) onNetWMState(e xproto.ClientMessageEvent) { | |||
| 197 | + | s := x.tab.lookup(uint32(e.Window())) | |||
| 198 | + | if s == 0 { | |||
| 199 | + | return | |||
| 200 | + | } | |||
| 201 | + | rec := x.tab.rec(s) | |||
| 202 | + | if rec.kind != kindManaged { | |||
| 203 | + | return | |||
| 204 | + | } | |||
| 205 | + | d := e.Data().Data32() | |||
| 206 | + | apply := func(cur bool) bool { | |||
| 207 | + | switch d[0] { | |||
| 208 | + | case netStateRemove: | |||
| 209 | + | return false | |||
| 210 | + | case netStateAdd: | |||
| 211 | + | return true | |||
| 212 | + | case netStateToggle: | |||
| 213 | + | return !cur | |||
| 214 | + | } | |||
| 215 | + | return cur | |||
| 216 | + | } | |||
| 217 | + | for _, a := range d[1:3] { | |||
| 218 | + | switch { | |||
| 219 | + | case a == 0: | |||
| 220 | + | case a == x.atoms[atomNetWMStateModal]: | |||
| 221 | + | rec.modal = apply(rec.modal) | |||
| 222 | + | case a == x.atoms[atomNetWMStateFullscreen]: | |||
| 223 | + | on := apply(rec.fullscreen) | |||
| 224 | + | if on == rec.fullscreen { | |||
| 225 | + | continue | |||
| 226 | + | } | |||
| 227 | + | rec.fullscreen = on | |||
| 228 | + | if rec.ready { | |||
| 229 | + | x.host.Fullscreen(s, on) | |||
| 230 | + | rec = x.tab.rec(s) | |||
| 231 | + | } | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | // onReparentNotify keeps the table to root's direct children: a window moved | |||
| 237 | + | // away is forgotten, one moved into root is treated as newly created. | |||
| 238 | + | func (x *XWM) onReparentNotify(ev *xturbo.Event) { | |||
| 239 | + | e := xproto.ReparentNotifyOf(ev) | |||
| 240 | + | xid := uint32(e.Window()) | |||
| 241 | + | if xid == x.wmWin { | |||
| 242 | + | return | |||
| 243 | + | } | |||
| 244 | + | s := x.tab.lookup(xid) | |||
| 245 | + | if uint32(e.Parent()) != x.root { | |||
| 246 | + | if s != 0 { | |||
| 247 | + | x.forgetWindow(s) | |||
| 248 | + | } | |||
| 249 | + | return | |||
| 250 | + | } | |||
| 251 | + | if s != 0 { | |||
| 252 | + | return | |||
| 253 | + | } | |||
| 254 | + | s = x.insert(xid) | |||
| 255 | + | if s == 0 { | |||
| 256 | + | return | |||
| 257 | + | } | |||
| 258 | + | rec := x.tab.rec(s) | |||
| 259 | + | rec.x, rec.y = int32(e.X()), int32(e.Y()) | |||
| 260 | + | if e.OverrideRedirect() { | |||
| 261 | + | rec.kind = kindUnmanaged | |||
| 262 | + | x.stats.UnmanagedSeen++ | |||
| 263 | + | return | |||
| 264 | + | } | |||
| 265 | + | if x.host != nil { | |||
| 266 | + | x.adopt(s) | |||
| 267 | + | } | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | // tryPair links the window to the wl_surface carrying its serial, once both | |||
| 271 | + | // are known and the window is ready (the host maps on the surface's first | |||
| 272 | + | // buffer, so it must know the window's info by then). Unmanaged windows keep | |||
| 273 | + | // the surface but are never associated. | |||
| 274 | + | func (x *XWM) tryPair(s Slot) { | |||
| 275 | + | rec := x.tab.rec(s) | |||
| 276 | + | if rec.serial == 0 || rec.surface != 0 { | |||
| 277 | + | return | |||
| 278 | + | } | |||
| 279 | + | if rec.kind == kindManaged && !rec.ready { | |||
| 280 | + | return | |||
| 281 | + | } | |||
| 282 | + | for i := 0; i < x.unpairedN; i++ { | |||
| 283 | + | if x.unpaired[i].serial != rec.serial { | |||
| 284 | + | continue | |||
| 285 | + | } | |||
| 286 | + | rec.surface = x.unpaired[i].surface | |||
| 287 | + | x.unpairedN-- | |||
| 288 | + | x.unpaired[i] = x.unpaired[x.unpairedN] | |||
| 289 | + | if rec.kind == kindManaged { | |||
| 290 | + | x.log.Debug().Uint32("xid", rec.xid).Uint64("surface", rec.surface).Msg("x11 window paired") | |||
| 291 | + | x.host.Associate(s, rec.surface) | |||
| 292 | + | } else { | |||
| 293 | + | x.overrideMapped(s, rec) | |||
| 294 | + | } | |||
| 295 | + | return | |||
| 296 | + | } | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | // SetSerial records the serial of a wl_surface (xwayland_surface_v1.set_serial) | |||
| 300 | + | // and pairs it with the window announcing the same serial. | |||
| 301 | + | func (x *XWM) SetSerial(surfaceID uint64, serial uint64) { | |||
| 302 | + | if serial == 0 || x.closed { | |||
| 303 | + | return | |||
| 304 | + | } | |||
| 305 | + | for i := 0; i < x.unpairedN; i++ { | |||
| 306 | + | if x.unpaired[i].serial == serial { | |||
| 307 | + | x.unpaired[i].surface = surfaceID | |||
| 308 | + | x.pairSerial(serial) | |||
| 309 | + | return | |||
| 310 | + | } | |||
| 311 | + | } | |||
| 312 | + | if x.unpairedN == unpairedCap { | |||
| 313 | + | copy(x.unpaired[:], x.unpaired[1:]) | |||
| 314 | + | x.unpairedN-- | |||
| 315 | + | if !x.warnedUnpaired { | |||
| 316 | + | x.warnedUnpaired = true | |||
| 317 | + | x.log.Warn().Msg("too many unpaired surfaces, dropping the oldest") | |||
| 318 | + | } | |||
| 319 | + | } | |||
| 320 | + | x.unpaired[x.unpairedN] = struct{ serial, surface uint64 }{serial, surfaceID} | |||
| 321 | + | x.unpairedN++ | |||
| 322 | + | x.pairSerial(serial) | |||
| 323 | + | } | |||
| 324 | + | ||||
| 325 | + | // pairSerial offers a newly known surface serial to the windows. | |||
| 326 | + | func (x *XWM) pairSerial(serial uint64) { | |||
| 327 | + | for i := range x.tab.recs { | |||
| 328 | + | if r := &x.tab.recs[i]; r.kind != kindFree && r.serial == serial && r.surface == 0 { | |||
| 329 | + | x.tryPair(Slot(i + 1)) | |||
| 330 | + | break | |||
| 331 | + | } | |||
| 332 | + | } | |||
| 333 | + | x.wake() | |||
| 334 | + | } | |||
| 335 | + | ||||
| 336 | + | // SurfaceGone forgets a destroyed wl_surface. | |||
| 337 | + | func (x *XWM) SurfaceGone(surfaceID uint64) { | |||
| 338 | + | for i := 0; i < x.unpairedN; i++ { | |||
| 339 | + | if x.unpaired[i].surface == surfaceID { | |||
| 340 | + | x.unpairedN-- | |||
| 341 | + | x.unpaired[i] = x.unpaired[x.unpairedN] | |||
| 342 | + | break | |||
| 343 | + | } | |||
| 344 | + | } | |||
| 345 | + | for i := range x.tab.recs { | |||
| 346 | + | if r := &x.tab.recs[i]; r.kind != kindFree && r.surface == surfaceID { | |||
| 347 | + | r.surface = 0 | |||
| 348 | + | if r.kind == kindUnmanaged && r.ovPaired { | |||
| 349 | + | r.ovPaired = false | |||
| 350 | + | x.host.OverrideGone(Slot(i + 1)) | |||
| 351 | + | } | |||
| 352 | + | } | |||
| 353 | + | } | |||
| 354 | + | x.wake() | |||
| 355 | + | } | |||
A internal/adapters/wayland/xwm/mapflow_test.go +779 -0
| @@ -0,0 +1,779 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "bnema.dev/xturbo" | |||
| 9 | + | "bnema.dev/xturbo/xproto" | |||
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 11 | + | "github.com/stretchr/testify/mock" | |||
| 12 | + | "github.com/stretchr/testify/require" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // harness bundles an attached XWM, its peer and the host mock. | |||
| 16 | + | type harness struct { | |||
| 17 | + | t *testing.T | |||
| 18 | + | x *XWM | |||
| 19 | + | p *xpeer | |||
| 20 | + | h *mockhost | |||
| 21 | + | ||||
| 22 | + | ready map[uint32]WindowInfo // by xid, filled by Ready | |||
| 23 | + | next ports.WindowID | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func newHarness(t *testing.T, mutate func(p *xpeer)) *harness { | |||
| 27 | + | t.Helper() | |||
| 28 | + | x, p := bootPeer(t, mutate) | |||
| 29 | + | hs := &harness{t: t, x: x, p: p, h: newMockhost(t), ready: map[uint32]WindowInfo{}} | |||
| 30 | + | src := newMockfdSource(t) | |||
| 31 | + | src.EXPECT().Remove().Maybe() | |||
| 32 | + | src.EXPECT().SetWritable(mock.Anything).Return(nil).Maybe() | |||
| 33 | + | post, _ := newPost() | |||
| 34 | + | x.attach(src, hs.h, post) | |||
| 35 | + | return hs | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | func (hs *harness) expectCreated(win uint32) { | |||
| 39 | + | hs.next++ | |||
| 40 | + | hs.h.EXPECT().Created(mock.Anything, win).Return(hs.next).Once() | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (hs *harness) expectReady(win uint32) { | |||
| 44 | + | hs.h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, info WindowInfo) { | |||
| 45 | + | hs.ready[win] = info | |||
| 46 | + | }).Once() | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | func (hs *harness) create(win uint32) Slot { | |||
| 50 | + | hs.t.Helper() | |||
| 51 | + | hs.expectCreated(win) | |||
| 52 | + | hs.p.createNotify(win, false) | |||
| 53 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 }) | |||
| 54 | + | return hs.x.tab.lookup(win) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // mapAndWait creates and maps win and waits for Ready. | |||
| 58 | + | func (hs *harness) mapAndWait(win uint32) Slot { | |||
| 59 | + | hs.t.Helper() | |||
| 60 | + | s := hs.create(win) | |||
| 61 | + | hs.expectReady(win) | |||
| 62 | + | hs.p.mapRequest(win) | |||
| 63 | + | readUntil(hs.t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |||
| 64 | + | return s | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func (hs *harness) str(win uint32, atom string, typ uint32, s string) { | |||
| 68 | + | hs.p.setProp(win, hs.p.atom(atom), typ, 8, []byte(s)) | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | func (hs *harness) words(win uint32, atom string, typ uint32, w ...uint32) { | |||
| 72 | + | hs.p.setProp(win, hs.p.atom(atom), typ, 32, le32(w...)) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (hs *harness) atoms(win uint32, atom string, names ...string) { | |||
| 76 | + | var w []uint32 | |||
| 77 | + | for _, n := range names { | |||
| 78 | + | w = append(w, hs.p.atom(n)) | |||
| 79 | + | } | |||
| 80 | + | hs.words(win, atom, uint32(xproto.AtomAtom), w...) | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func (hs *harness) classic(win uint32) { | |||
| 84 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("inst\x00Class\x00")) | |||
| 85 | + | hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "net title") | |||
| 86 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("old title")) | |||
| 87 | + | hs.words(win, "_NET_WM_PID", uint32(xproto.AtomCardinal), 4242) | |||
| 88 | + | hs.p.set(func(p *xpeer) { p.geoms[win] = xpeerGeom{w: 300, h: 200} }) | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | func (hs *harness) count(match func(r xpeerReq) bool) int { | |||
| 92 | + | n := 0 | |||
| 93 | + | for _, r := range hs.p.requests() { | |||
| 94 | + | if match(r) { | |||
| 95 | + | n++ | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | return n | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | func (hs *harness) waitMap(win uint32) { | |||
| 102 | + | hs.t.Helper() | |||
| 103 | + | readUntil(hs.t, hs.x, func() bool { | |||
| 104 | + | return hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win }) > 0 | |||
| 105 | + | }) | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | func TestMapFlowBatchesRequestsAndReadyFiresOnce(t *testing.T) { | |||
| 109 | + | hs := newHarness(t, nil) | |||
| 110 | + | const win = 0x500001 | |||
| 111 | + | hs.classic(win) | |||
| 112 | + | hs.create(win) | |||
| 113 | + | base := len(hs.p.requests()) | |||
| 114 | + | ||||
| 115 | + | hs.p.holdReplies() | |||
| 116 | + | hs.p.mapRequest(win) | |||
| 117 | + | hs.expectReady(win) | |||
| 118 | + | hs.waitMap(win) | |||
| 119 | + | ||||
| 120 | + | reqs := hs.p.requests()[base:] | |||
| 121 | + | want := []uint32{ | |||
| 122 | + | uint32(xproto.AtomWmClass), uint32(xproto.AtomWmName), hs.p.atom("_NET_WM_NAME"), uint32(xproto.AtomWmHints), | |||
| 123 | + | uint32(xproto.AtomWmNormalHints), hs.p.atom("WM_PROTOCOLS"), uint32(xproto.AtomWmTransientFor), | |||
| 124 | + | hs.p.atom("_NET_WM_WINDOW_TYPE"), hs.p.atom("_NET_WM_STATE"), hs.p.atom("_NET_WM_PID"), | |||
| 125 | + | } | |||
| 126 | + | require.GreaterOrEqual(t, len(reqs), len(want)+2) | |||
| 127 | + | for i, a := range want { | |||
| 128 | + | require.EqualValues(t, opGetProperty, reqs[i].major, i) | |||
| 129 | + | require.Equal(t, uint32(win), reqs[i].u32(4)) | |||
| 130 | + | require.Equal(t, a, reqs[i].u32(8), i) | |||
| 131 | + | require.EqualValues(t, 1024, reqs[i].u32(20), "long length: one 4 KiB read") | |||
| 132 | + | } | |||
| 133 | + | require.EqualValues(t, opGetGeometry, reqs[len(want)].major) | |||
| 134 | + | require.EqualValues(t, opMapWindow, reqs[len(want)+1].major, "MapWindow in the same batch") | |||
| 135 | + | ||||
| 136 | + | hs.p.release() // every reply arrives after MapWindow went out | |||
| 137 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |||
| 138 | + | info := hs.ready[win] | |||
| 139 | + | require.Equal(t, WindowInfo{PID: 4242, Title: "net title", AppID: "Class"}, info, "_NET_WM_NAME wins, class is the second string") | |||
| 140 | + | ||||
| 141 | + | state, ok := hs.p.prop(win, "WM_STATE") | |||
| 142 | + | require.True(t, ok) | |||
| 143 | + | require.Equal(t, []uint32{1, 0}, atomsOf(state)) | |||
| 144 | + | list, ok := hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 145 | + | require.True(t, ok) | |||
| 146 | + | require.Equal(t, []uint32{win}, atomsOf(list)) | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | func TestMapFlowFloatingAndModal(t *testing.T) { | |||
| 150 | + | hint := func(minw, minh, maxw, maxh uint32) []byte { | |||
| 151 | + | return le32(hintMinSize|hintMaxSize, 0, 0, 0, 0, minw, minh, maxw, maxh, 0, 0, 0, 0, 0, 0, 0, 0, 0) | |||
| 152 | + | } | |||
| 153 | + | cases := []struct { | |||
| 154 | + | name string | |||
| 155 | + | setup func(hs *harness, win uint32) | |||
| 156 | + | want func(info WindowInfo) | |||
| 157 | + | }{ | |||
| 158 | + | {"dialog type", func(hs *harness, w uint32) { | |||
| 159 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_DIALOG") | |||
| 160 | + | }, func(i WindowInfo) { | |||
| 161 | + | require.True(t, i.Floating) | |||
| 162 | + | require.Equal(t, 300, i.Width) | |||
| 163 | + | require.Equal(t, 200, i.Height) | |||
| 164 | + | }}, | |||
| 165 | + | {"utility type", func(hs *harness, w uint32) { | |||
| 166 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_UTILITY") | |||
| 167 | + | }, func(i WindowInfo) { require.True(t, i.Floating) }}, | |||
| 168 | + | {"splash type", func(hs *harness, w uint32) { | |||
| 169 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_SPLASH") | |||
| 170 | + | }, func(i WindowInfo) { require.True(t, i.Floating) }}, | |||
| 171 | + | {"normal type", func(hs *harness, w uint32) { | |||
| 172 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_NORMAL") | |||
| 173 | + | }, func(i WindowInfo) { require.False(t, i.Floating); require.Zero(t, i.Width) }}, | |||
| 174 | + | {"fixed size", func(hs *harness, w uint32) { | |||
| 175 | + | hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(200, 100, 200, 100)) | |||
| 176 | + | }, func(i WindowInfo) { require.True(t, i.Floating) }}, | |||
| 177 | + | {"resizable", func(hs *harness, w uint32) { | |||
| 178 | + | hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(100, 100, 800, 600)) | |||
| 179 | + | }, func(i WindowInfo) { require.False(t, i.Floating) }}, | |||
| 180 | + | {"modal", func(hs *harness, w uint32) { | |||
| 181 | + | hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_MODAL") | |||
| 182 | + | }, func(i WindowInfo) { require.True(t, i.Modal); require.False(t, i.Fullscreen) }}, | |||
| 183 | + | {"fullscreen at map", func(hs *harness, w uint32) { | |||
| 184 | + | hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_FULLSCREEN") | |||
| 185 | + | }, func(i WindowInfo) { require.True(t, i.Fullscreen) }}, | |||
| 186 | + | } | |||
| 187 | + | for _, c := range cases { | |||
| 188 | + | t.Run(c.name, func(t *testing.T) { | |||
| 189 | + | hs := newHarness(t, nil) | |||
| 190 | + | const win = 0x500001 | |||
| 191 | + | hs.classic(win) | |||
| 192 | + | c.setup(hs, win) | |||
| 193 | + | hs.mapAndWait(win) | |||
| 194 | + | c.want(hs.ready[win]) | |||
| 195 | + | }) | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | func TestMapFlowTransientForManagedWindowFloats(t *testing.T) { | |||
| 200 | + | hs := newHarness(t, nil) | |||
| 201 | + | const parent, child = 0x500001, 0x500002 | |||
| 202 | + | hs.classic(parent) | |||
| 203 | + | ps := hs.mapAndWait(parent) | |||
| 204 | + | hs.classic(child) | |||
| 205 | + | hs.p.setProp(child, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(parent)) | |||
| 206 | + | hs.mapAndWait(child) | |||
| 207 | + | require.True(t, hs.ready[child].Floating) | |||
| 208 | + | require.Equal(t, ps, hs.ready[child].Parent) | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | func TestMapFlowTransientForUnknownWindowIsNotAParent(t *testing.T) { | |||
| 212 | + | hs := newHarness(t, nil) | |||
| 213 | + | const win = 0x500001 | |||
| 214 | + | hs.classic(win) | |||
| 215 | + | hs.p.setProp(win, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(0x777777)) | |||
| 216 | + | hs.mapAndWait(win) | |||
| 217 | + | require.Zero(t, hs.ready[win].Parent) | |||
| 218 | + | require.False(t, hs.ready[win].Floating) | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | func TestMapFlowClassFallsBackToInstance(t *testing.T) { | |||
| 222 | + | hs := newHarness(t, nil) | |||
| 223 | + | const win = 0x500001 | |||
| 224 | + | hs.classic(win) | |||
| 225 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("onlyinstance\x00")) | |||
| 226 | + | hs.mapAndWait(win) | |||
| 227 | + | require.Equal(t, "onlyinstance", hs.ready[win].AppID) | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | func TestMapFlowTitleFallsBackToWMName(t *testing.T) { | |||
| 231 | + | hs := newHarness(t, nil) | |||
| 232 | + | const win = 0x500001 | |||
| 233 | + | hs.classic(win) | |||
| 234 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) }) | |||
| 235 | + | hs.mapAndWait(win) | |||
| 236 | + | require.Equal(t, "old title", hs.ready[win].Title) | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | func TestMapFlowPIDFromXRes(t *testing.T) { | |||
| 240 | + | hs := newHarness(t, func(p *xpeer) { p.pids = map[uint32]uint32{0x500001: 777} }) | |||
| 241 | + | const win = 0x500001 | |||
| 242 | + | hs.classic(win) | |||
| 243 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) }) | |||
| 244 | + | hs.create(win) | |||
| 245 | + | hs.expectReady(win) | |||
| 246 | + | hs.p.mapRequest(win) | |||
| 247 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |||
| 248 | + | require.Equal(t, 777, hs.ready[win].PID, "Ready waited for the QueryClientIds reply") | |||
| 249 | + | require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == 144 && r.minor == 4 })) | |||
| 250 | + | require.Zero(t, hs.x.tab.rec(hs.x.tab.lookup(win)).pending) | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | func TestMapFlowPIDUnknownStillReady(t *testing.T) { | |||
| 254 | + | hs := newHarness(t, nil) | |||
| 255 | + | const win = 0x500001 | |||
| 256 | + | hs.classic(win) | |||
| 257 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) }) | |||
| 258 | + | hs.mapAndWait(win) | |||
| 259 | + | require.Zero(t, hs.ready[win].PID) | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | func TestMapFlowNoXResAsksNothing(t *testing.T) { | |||
| 263 | + | exts := fullPeerExts() | |||
| 264 | + | delete(exts, "X-Resource") | |||
| 265 | + | fd, p := startXPeer(t, exts) | |||
| 266 | + | x, err := Boot(context.Background(), fd, testLog()) | |||
| 267 | + | require.NoError(t, err) | |||
| 268 | + | t.Cleanup(x.CloseConn) | |||
| 269 | + | h := newMockhost(t) | |||
| 270 | + | src := newMockfdSource(t) | |||
| 271 | + | src.EXPECT().Remove().Maybe() | |||
| 272 | + | post, _ := newPost() | |||
| 273 | + | x.attach(src, h, post) | |||
| 274 | + | hs := &harness{t: t, x: x, p: p, h: h, ready: map[uint32]WindowInfo{}} | |||
| 275 | + | const win = 0x500001 | |||
| 276 | + | hs.classic(win) | |||
| 277 | + | p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) }) | |||
| 278 | + | hs.mapAndWait(win) | |||
| 279 | + | require.Zero(t, hs.count(func(r xpeerReq) bool { return r.major == 144 })) | |||
| 280 | + | } | |||
| 281 | + | ||||
| 282 | + | func TestMapFlowBadWindowOnPropertiesNeverReady(t *testing.T) { | |||
| 283 | + | hs := newHarness(t, nil) | |||
| 284 | + | const win = 0x500001 | |||
| 285 | + | hs.classic(win) | |||
| 286 | + | hs.p.set(func(p *xpeer) { p.bad[win] = true }) | |||
| 287 | + | s := hs.create(win) | |||
| 288 | + | hs.p.mapRequest(win) | |||
| 289 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 }) | |||
| 290 | + | require.Zero(t, hs.x.tab.rec(s).pending, "failed replies are no longer awaited") | |||
| 291 | + | require.False(t, hs.x.tab.rec(s).ready) | |||
| 292 | + | require.Zero(t, hs.x.Stats().Unattributed) | |||
| 293 | + | ||||
| 294 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |||
| 295 | + | hs.p.destroyNotify(win) | |||
| 296 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | func TestMapRequestTwiceBeforeReadyAsksOnce(t *testing.T) { | |||
| 300 | + | hs := newHarness(t, nil) | |||
| 301 | + | const win = 0x500001 | |||
| 302 | + | hs.classic(win) | |||
| 303 | + | hs.create(win) | |||
| 304 | + | hs.p.holdReplies() | |||
| 305 | + | hs.p.mapRequest(win) | |||
| 306 | + | hs.p.mapRequest(win) | |||
| 307 | + | hs.expectReady(win) | |||
| 308 | + | hs.waitMap(win) | |||
| 309 | + | hs.p.release() | |||
| 310 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |||
| 311 | + | require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win })) | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | func TestMapNotifyMarksMapped(t *testing.T) { | |||
| 315 | + | hs := newHarness(t, nil) | |||
| 316 | + | const win = 0x500001 | |||
| 317 | + | hs.classic(win) | |||
| 318 | + | s := hs.mapAndWait(win) | |||
| 319 | + | hs.p.mapNotify(win) | |||
| 320 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapped }) | |||
| 321 | + | } | |||
| 322 | + | ||||
| 323 | + | // Property changes after Ready. | |||
| 324 | + | ||||
| 325 | + | func (hs *harness) notify(win uint32, atom string, deleted bool) { | |||
| 326 | + | hs.t.Helper() | |||
| 327 | + | before := hs.x.Stats().Replies | |||
| 328 | + | hs.p.propertyNotify(win, hs.p.atom(atom), deleted) | |||
| 329 | + | if deleted { | |||
| 330 | + | hs.p.propertyNotify(win, hs.p.atom("WM_PROTOCOLS"), false) // sentinel: one reply follows | |||
| 331 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |||
| 332 | + | return | |||
| 333 | + | } | |||
| 334 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |||
| 335 | + | } | |||
| 336 | + | ||||
| 337 | + | func TestPropertyChangedTitleReachesHostOnce(t *testing.T) { | |||
| 338 | + | hs := newHarness(t, nil) | |||
| 339 | + | const win = 0x500001 | |||
| 340 | + | hs.classic(win) | |||
| 341 | + | s := hs.mapAndWait(win) | |||
| 342 | + | ||||
| 343 | + | hs.h.EXPECT().Title(s, "renamed").Once() | |||
| 344 | + | hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "renamed") | |||
| 345 | + | hs.notify(win, "_NET_WM_NAME", false) | |||
| 346 | + | require.Equal(t, "renamed", hs.x.tab.rec(s).title) | |||
| 347 | + | ||||
| 348 | + | // Same value again: no host call (the mock fails on an unexpected one). | |||
| 349 | + | hs.notify(win, "_NET_WM_NAME", false) | |||
| 350 | + | ||||
| 351 | + | // WM_NAME is ignored while _NET_WM_NAME is set. | |||
| 352 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("legacy")) | |||
| 353 | + | before := hs.x.Stats().Events | |||
| 354 | + | hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false) | |||
| 355 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > before }) | |||
| 356 | + | require.Equal(t, "renamed", hs.x.tab.rec(s).title) | |||
| 357 | + | } | |||
| 358 | + | ||||
| 359 | + | func TestPropertyNetNameDeletedFallsBackToWMName(t *testing.T) { | |||
| 360 | + | hs := newHarness(t, nil) | |||
| 361 | + | const win = 0x500001 | |||
| 362 | + | hs.classic(win) | |||
| 363 | + | s := hs.mapAndWait(win) | |||
| 364 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) }) | |||
| 365 | + | hs.h.EXPECT().Title(s, "old title").Once() | |||
| 366 | + | hs.p.propertyNotify(win, hs.p.atom("_NET_WM_NAME"), true) | |||
| 367 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).title == "old title" }) | |||
| 368 | + | } | |||
| 369 | + | ||||
| 370 | + | func TestPropertyClassChangeReachesHost(t *testing.T) { | |||
| 371 | + | hs := newHarness(t, nil) | |||
| 372 | + | const win = 0x500001 | |||
| 373 | + | hs.classic(win) | |||
| 374 | + | s := hs.mapAndWait(win) | |||
| 375 | + | hs.h.EXPECT().AppID(s, "Other").Once() | |||
| 376 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00Other\x00")) | |||
| 377 | + | hs.p.propertyNotify(win, uint32(xproto.AtomWmClass), false) | |||
| 378 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).class == "Other" }) | |||
| 379 | + | } | |||
| 380 | + | ||||
| 381 | + | func TestPropertyTransientChangeReachesHost(t *testing.T) { | |||
| 382 | + | hs := newHarness(t, nil) | |||
| 383 | + | const a, b = 0x500001, 0x500002 | |||
| 384 | + | hs.classic(a) | |||
| 385 | + | as := hs.mapAndWait(a) | |||
| 386 | + | hs.classic(b) | |||
| 387 | + | bs := hs.mapAndWait(b) | |||
| 388 | + | hs.h.EXPECT().Parent(bs, as).Once() | |||
| 389 | + | hs.p.setProp(b, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(a)) | |||
| 390 | + | hs.p.propertyNotify(b, uint32(xproto.AtomWmTransientFor), false) | |||
| 391 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(bs).transient == a }) | |||
| 392 | + | } | |||
| 393 | + | ||||
| 394 | + | func TestPropertyInputProtocolsAndHints(t *testing.T) { | |||
| 395 | + | hs := newHarness(t, nil) | |||
| 396 | + | const win = 0x500001 | |||
| 397 | + | hs.classic(win) | |||
| 398 | + | hs.atoms(win, "WM_PROTOCOLS", "WM_DELETE_WINDOW", "WM_TAKE_FOCUS") | |||
| 399 | + | hs.p.setProp(win, uint32(xproto.AtomWmHints), uint32(xproto.AtomWmHints), 32, le32(hintInput, 0, 0, 0, 0, 0, 0, 0, 0)) | |||
| 400 | + | s := hs.mapAndWait(win) | |||
| 401 | + | r := hs.x.tab.rec(s) | |||
| 402 | + | require.True(t, r.deleteWindow) | |||
| 403 | + | require.True(t, r.takeFocus) | |||
| 404 | + | require.False(t, r.input, "WM_HINTS input = false") | |||
| 405 | + | ||||
| 406 | + | // No WM_HINTS: input defaults to true. | |||
| 407 | + | const other = 0x500002 | |||
| 408 | + | hs.classic(other) | |||
| 409 | + | o := hs.mapAndWait(other) | |||
| 410 | + | require.True(t, hs.x.tab.rec(o).input) | |||
| 411 | + | require.False(t, hs.x.tab.rec(o).deleteWindow) | |||
| 412 | + | } | |||
| 413 | + | ||||
| 414 | + | func TestPropertyUnwatchedAtomAsksNothing(t *testing.T) { | |||
| 415 | + | hs := newHarness(t, nil) | |||
| 416 | + | const win = 0x500001 | |||
| 417 | + | hs.classic(win) | |||
| 418 | + | hs.mapAndWait(win) | |||
| 419 | + | n := len(hs.p.requests()) | |||
| 420 | + | hs.p.propertyNotify(win, hs.p.atom("_MOTIF_WM_HINTS"), false) | |||
| 421 | + | hs.p.propertyNotify(0x999999, hs.p.atom("_NET_WM_NAME"), false) // unknown window | |||
| 422 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 }) | |||
| 423 | + | require.Len(t, hs.p.requests(), n) | |||
| 424 | + | } | |||
| 425 | + | ||||
| 426 | + | // Pairing. | |||
| 427 | + | ||||
| 428 | + | func (hs *harness) serialMsg(win uint32, serial uint64) { | |||
| 429 | + | hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_SERIAL"), uint32(serial), uint32(serial>>32)) | |||
| 430 | + | hs.t.Helper() | |||
| 431 | + | before := hs.x.Stats().Events | |||
| 432 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events > before }) | |||
| 433 | + | } | |||
| 434 | + | ||||
| 435 | + | func TestPairingSerialBeforeSetSerial(t *testing.T) { | |||
| 436 | + | hs := newHarness(t, nil) | |||
| 437 | + | const win = 0x500001 | |||
| 438 | + | hs.classic(win) | |||
| 439 | + | s := hs.mapAndWait(win) | |||
| 440 | + | hs.serialMsg(win, 0x1_0000_0002) | |||
| 441 | + | require.EqualValues(t, 0x1_0000_0002, hs.x.tab.rec(s).serial, "high word in data32[1]") | |||
| 442 | + | hs.h.EXPECT().Associate(s, uint64(99)).Once() | |||
| 443 | + | hs.x.SetSerial(99, 0x1_0000_0002) | |||
| 444 | + | require.EqualValues(t, 99, hs.x.tab.rec(s).surface) | |||
| 445 | + | require.Zero(t, hs.x.unpairedN) | |||
| 446 | + | } | |||
| 447 | + | ||||
| 448 | + | func TestPairingSetSerialBeforeSerial(t *testing.T) { | |||
| 449 | + | hs := newHarness(t, nil) | |||
| 450 | + | const win = 0x500001 | |||
| 451 | + | hs.classic(win) | |||
| 452 | + | s := hs.mapAndWait(win) | |||
| 453 | + | hs.x.SetSerial(99, 7) | |||
| 454 | + | require.Equal(t, 1, hs.x.unpairedN) | |||
| 455 | + | hs.h.EXPECT().Associate(s, uint64(99)).Once() | |||
| 456 | + | hs.serialMsg(win, 7) | |||
| 457 | + | require.Zero(t, hs.x.unpairedN) | |||
| 458 | + | } | |||
| 459 | + | ||||
| 460 | + | func TestPairingWaitsForReady(t *testing.T) { | |||
| 461 | + | hs := newHarness(t, nil) | |||
| 462 | + | const win = 0x500001 | |||
| 463 | + | hs.classic(win) | |||
| 464 | + | s := hs.create(win) | |||
| 465 | + | hs.x.SetSerial(5, 3) | |||
| 466 | + | hs.serialMsg(win, 3) // not mapped yet: no Associate (the mock fails on one) | |||
| 467 | + | require.Zero(t, hs.x.tab.rec(s).surface) | |||
| 468 | + | ||||
| 469 | + | hs.h.EXPECT().Associate(s, uint64(5)).Once() | |||
| 470 | + | hs.expectReady(win) | |||
| 471 | + | hs.p.mapRequest(win) | |||
| 472 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).surface != 0 }) | |||
| 473 | + | } | |||
| 474 | + | ||||
| 475 | + | func TestPairingSerialForUnknownWindowIgnored(t *testing.T) { | |||
| 476 | + | hs := newHarness(t, nil) | |||
| 477 | + | hs.x.SetSerial(5, 3) | |||
| 478 | + | hs.serialMsg(0x888888, 3) | |||
| 479 | + | require.Equal(t, 1, hs.x.unpairedN, "stays unpaired") | |||
| 480 | + | } | |||
| 481 | + | ||||
| 482 | + | func TestPairingSurfaceGoneBeforePair(t *testing.T) { | |||
| 483 | + | hs := newHarness(t, nil) | |||
| 484 | + | const win = 0x500001 | |||
| 485 | + | hs.classic(win) | |||
| 486 | + | s := hs.mapAndWait(win) | |||
| 487 | + | hs.x.SetSerial(5, 3) | |||
| 488 | + | hs.x.SurfaceGone(5) | |||
| 489 | + | require.Zero(t, hs.x.unpairedN) | |||
| 490 | + | hs.serialMsg(win, 3) // the surface is gone: nothing to pair with | |||
| 491 | + | require.Zero(t, hs.x.tab.rec(s).surface) | |||
| 492 | + | } | |||
| 493 | + | ||||
| 494 | + | func TestPairingSurfaceGoneAfterPairClearsLink(t *testing.T) { | |||
| 495 | + | hs := newHarness(t, nil) | |||
| 496 | + | const win = 0x500001 | |||
| 497 | + | hs.classic(win) | |||
| 498 | + | s := hs.mapAndWait(win) | |||
| 499 | + | hs.serialMsg(win, 3) | |||
| 500 | + | hs.h.EXPECT().Associate(s, uint64(5)).Once() | |||
| 501 | + | hs.x.SetSerial(5, 3) | |||
| 502 | + | hs.x.SurfaceGone(5) | |||
| 503 | + | require.Zero(t, hs.x.tab.rec(s).surface) | |||
| 504 | + | } | |||
| 505 | + | ||||
| 506 | + | func TestPairingDuplicateSerialIgnored(t *testing.T) { | |||
| 507 | + | hs := newHarness(t, nil) | |||
| 508 | + | const win = 0x500001 | |||
| 509 | + | hs.classic(win) | |||
| 510 | + | s := hs.mapAndWait(win) | |||
| 511 | + | hs.serialMsg(win, 3) | |||
| 512 | + | hs.serialMsg(win, 4) | |||
| 513 | + | require.EqualValues(t, 3, hs.x.tab.rec(s).serial) | |||
| 514 | + | hs.x.SetSerial(8, 4) // nobody announces 4: stays unpaired | |||
| 515 | + | require.Equal(t, 1, hs.x.unpairedN) | |||
| 516 | + | } | |||
| 517 | + | ||||
| 518 | + | func TestPairingUnmanagedKeepsSurfaceWithoutAssociate(t *testing.T) { | |||
| 519 | + | hs := newHarness(t, nil) | |||
| 520 | + | hs.p.createNotify(0x500009, true) | |||
| 521 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 }) | |||
| 522 | + | hs.serialMsg(0x500009, 3) | |||
| 523 | + | hs.x.SetSerial(5, 3) // the mock fails on an Associate | |||
| 524 | + | require.EqualValues(t, 5, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).surface) | |||
| 525 | + | } | |||
| 526 | + | ||||
| 527 | + | func TestPairingTableOverflowDropsOldest(t *testing.T) { | |||
| 528 | + | hs := newHarness(t, nil) | |||
| 529 | + | for i := uint64(1); i <= unpairedCap+1; i++ { | |||
| 530 | + | hs.x.SetSerial(100+i, i) | |||
| 531 | + | } | |||
| 532 | + | require.Equal(t, unpairedCap, hs.x.unpairedN) | |||
| 533 | + | for i := 0; i < hs.x.unpairedN; i++ { | |||
| 534 | + | require.NotEqual(t, uint64(1), hs.x.unpaired[i].serial, "oldest dropped") | |||
| 535 | + | } | |||
| 536 | + | } | |||
| 537 | + | ||||
| 538 | + | func TestPairingZeroSerialIgnored(t *testing.T) { | |||
| 539 | + | hs := newHarness(t, nil) | |||
| 540 | + | hs.x.SetSerial(5, 0) | |||
| 541 | + | require.Zero(t, hs.x.unpairedN) | |||
| 542 | + | } | |||
| 543 | + | ||||
| 544 | + | func TestLegacySurfaceIDCountedAndIgnored(t *testing.T) { | |||
| 545 | + | hs := newHarness(t, nil) | |||
| 546 | + | const win = 0x500001 | |||
| 547 | + | hs.classic(win) | |||
| 548 | + | s := hs.mapAndWait(win) | |||
| 549 | + | hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_ID"), 12) | |||
| 550 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().LegacyID == 1 }) | |||
| 551 | + | require.Zero(t, hs.x.tab.rec(s).serial) | |||
| 552 | + | } | |||
| 553 | + | ||||
| 554 | + | func TestNetWMStateClientMessageForwardsFullscreen(t *testing.T) { | |||
| 555 | + | hs := newHarness(t, nil) | |||
| 556 | + | const win = 0x500001 | |||
| 557 | + | hs.classic(win) | |||
| 558 | + | s := hs.mapAndWait(win) | |||
| 559 | + | hs.h.EXPECT().Fullscreen(s, true).Once() | |||
| 560 | + | hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 1, hs.p.atom("_NET_WM_STATE_FULLSCREEN")) | |||
| 561 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).fullscreen }) | |||
| 562 | + | hs.h.EXPECT().Fullscreen(s, false).Once() // toggle | |||
| 563 | + | hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 2, hs.p.atom("_NET_WM_STATE_FULLSCREEN")) | |||
| 564 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).fullscreen }) | |||
| 565 | + | // Not written back by the message itself. | |||
| 566 | + | st, ok := hs.p.prop(win, "_NET_WM_STATE") | |||
| 567 | + | require.True(t, ok) | |||
| 568 | + | require.Empty(t, st.data) | |||
| 569 | + | } | |||
| 570 | + | ||||
| 571 | + | // Unmap, destroy, remap. | |||
| 572 | + | ||||
| 573 | + | func TestUnmapDissociatesAndRemapRereadsProperties(t *testing.T) { | |||
| 574 | + | hs := newHarness(t, nil) | |||
| 575 | + | const win = 0x500001 | |||
| 576 | + | hs.classic(win) | |||
| 577 | + | s := hs.mapAndWait(win) | |||
| 578 | + | ||||
| 579 | + | // A synthetic UnmapNotify (ICCCM withdraw request) is not an unmap. | |||
| 580 | + | hs.p.unmapNotify(win, true) | |||
| 581 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 }) | |||
| 582 | + | require.True(t, hs.x.tab.rec(s).ready) | |||
| 583 | + | ||||
| 584 | + | hs.h.EXPECT().Dissociate(s, false).Once() | |||
| 585 | + | hs.p.unmapNotify(win, false) | |||
| 586 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).ready }) | |||
| 587 | + | r := hs.x.tab.rec(s) | |||
| 588 | + | require.False(t, r.mapping) | |||
| 589 | + | require.Zero(t, r.serial) | |||
| 590 | + | hs.p.waitReq(0, func(q xpeerReq) bool { | |||
| 591 | + | return q.major == opSetInputFocus || q.major == opChangeProperty && q.u32(4) == win | |||
| 592 | + | }) | |||
| 593 | + | readUntil(t, hs.x, func() bool { | |||
| 594 | + | v, ok := hs.p.prop(win, "WM_STATE") | |||
| 595 | + | return ok && len(v.data) == 8 && binary.LittleEndian.Uint32(v.data) == 0 | |||
| 596 | + | }) | |||
| 597 | + | _, ok := hs.p.prop(win, "_NET_WM_STATE") | |||
| 598 | + | require.False(t, ok, "_NET_WM_STATE deleted") | |||
| 599 | + | list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 600 | + | require.Empty(t, list.data) | |||
| 601 | + | ||||
| 602 | + | // Remap starts over. | |||
| 603 | + | delete(hs.ready, win) | |||
| 604 | + | hs.expectReady(win) | |||
| 605 | + | n := hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win }) | |||
| 606 | + | hs.p.mapRequest(win) | |||
| 607 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |||
| 608 | + | require.Equal(t, n+propCount, hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win })) | |||
| 609 | + | list, _ = hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 610 | + | require.Equal(t, []uint32{win}, atomsOf(list)) | |||
| 611 | + | } | |||
| 612 | + | ||||
| 613 | + | func TestDestroyWhileMappingNeverReady(t *testing.T) { | |||
| 614 | + | hs := newHarness(t, nil) | |||
| 615 | + | const win = 0x500001 | |||
| 616 | + | hs.classic(win) | |||
| 617 | + | s := hs.create(win) | |||
| 618 | + | hs.p.holdReplies() | |||
| 619 | + | hs.p.mapRequest(win) | |||
| 620 | + | hs.waitMap(win) | |||
| 621 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |||
| 622 | + | hs.p.destroyNotify(win) | |||
| 623 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |||
| 624 | + | replies := hs.x.Stats().Replies | |||
| 625 | + | hs.p.release() // replies for a window that is gone: xturbo retired their slots | |||
| 626 | + | readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) }) | |||
| 627 | + | require.Equal(t, replies, hs.x.Stats().Replies, "none reaches the XWM") | |||
| 628 | + | } | |||
| 629 | + | ||||
| 630 | + | func TestStaleRepliesAfterSlotReuseAreIgnored(t *testing.T) { | |||
| 631 | + | hs := newHarness(t, nil) | |||
| 632 | + | const old, fresh = 0x500001, 0x500002 | |||
| 633 | + | hs.classic(old) | |||
| 634 | + | hs.classic(fresh) | |||
| 635 | + | s := hs.create(old) | |||
| 636 | + | hs.p.holdReplies() | |||
| 637 | + | hs.p.mapRequest(old) | |||
| 638 | + | hs.waitMap(old) | |||
| 639 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |||
| 640 | + | hs.p.destroyNotify(old) | |||
| 641 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(old) == 0 }) | |||
| 642 | + | ||||
| 643 | + | hs.expectCreated(fresh) | |||
| 644 | + | hs.p.createNotify(fresh, false) | |||
| 645 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(fresh) != 0 }) | |||
| 646 | + | require.Equal(t, s, hs.x.tab.lookup(fresh), "the freed slot is reused") | |||
| 647 | + | ||||
| 648 | + | hs.p.release() | |||
| 649 | + | readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) }) | |||
| 650 | + | ||||
| 651 | + | // A reply that does get through with the old tag (slot reused, generation | |||
| 652 | + | // bumped) is ignored. | |||
| 653 | + | oldTag := s | Slot(hs.x.tab.rec(s).gen-1)<<16 | |||
| 654 | + | rep := make([]byte, 32) | |||
| 655 | + | rep[0] = 1 | |||
| 656 | + | hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{uint32(oldTag), propClass}, Raw: rep}) | |||
| 657 | + | hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{uint32(oldTag)}, Raw: rep}) | |||
| 658 | + | require.EqualValues(t, 2, hs.x.Stats().Stale) | |||
| 659 | + | require.False(t, hs.x.tab.rec(s).ready, "stale replies must not complete the new window") | |||
| 660 | + | require.Zero(t, hs.x.tab.rec(s).pending) | |||
| 661 | + | } | |||
| 662 | + | ||||
| 663 | + | // Reparenting. | |||
| 664 | + | ||||
| 665 | + | func TestReparentAwayFromRootRemovesManagedWindow(t *testing.T) { | |||
| 666 | + | hs := newHarness(t, nil) | |||
| 667 | + | const win = 0x500001 | |||
| 668 | + | hs.classic(win) | |||
| 669 | + | s := hs.mapAndWait(win) | |||
| 670 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |||
| 671 | + | hs.p.reparentNotify(win, 0x600000) | |||
| 672 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |||
| 673 | + | list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 674 | + | require.Empty(t, list.data) | |||
| 675 | + | require.Zero(t, hs.x.tab.n) | |||
| 676 | + | } | |||
| 677 | + | ||||
| 678 | + | func TestReparentAwayOfUnmanagedWindowRemovesIt(t *testing.T) { | |||
| 679 | + | hs := newHarness(t, nil) | |||
| 680 | + | hs.p.createNotify(0x500009, true) | |||
| 681 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 }) | |||
| 682 | + | hs.p.reparentNotify(0x500009, 0x600000) | |||
| 683 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) == 0 }) | |||
| 684 | + | } | |||
| 685 | + | ||||
| 686 | + | func TestReparentIntoRootAdoptsWindow(t *testing.T) { | |||
| 687 | + | hs := newHarness(t, nil) | |||
| 688 | + | const win = 0x500001 | |||
| 689 | + | hs.expectCreated(win) | |||
| 690 | + | hs.p.reparentNotify(win, peerRoot) | |||
| 691 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 }) | |||
| 692 | + | require.Equal(t, kindManaged, hs.x.tab.rec(hs.x.tab.lookup(win)).kind) | |||
| 693 | + | ||||
| 694 | + | // Already known: no second Created. | |||
| 695 | + | hs.p.reparentNotify(win, peerRoot) | |||
| 696 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 }) | |||
| 697 | + | } | |||
| 698 | + | ||||
| 699 | + | func TestReparentIntoRootOfOverrideRedirectWindowStaysUnmanaged(t *testing.T) { | |||
| 700 | + | hs := newHarness(t, nil) | |||
| 701 | + | hs.p.event(21, func(b *[32]byte) { | |||
| 702 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 703 | + | binary.LittleEndian.PutUint32(b[8:], 0x500009) | |||
| 704 | + | binary.LittleEndian.PutUint32(b[12:], peerRoot) | |||
| 705 | + | b[20] = 1 | |||
| 706 | + | }) | |||
| 707 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 }) | |||
| 708 | + | require.Equal(t, kindUnmanaged, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).kind) | |||
| 709 | + | } | |||
| 710 | + | ||||
| 711 | + | func TestReparentOfWMWindowIgnored(t *testing.T) { | |||
| 712 | + | hs := newHarness(t, nil) | |||
| 713 | + | hs.p.reparentNotify(hs.x.wmWin, peerRoot) | |||
| 714 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 1 }) | |||
| 715 | + | require.Zero(t, hs.x.tab.lookup(hs.x.wmWin)) | |||
| 716 | + | } | |||
| 717 | + | ||||
| 718 | + | // Window known only before Attach: Attach replays the map flow. | |||
| 719 | + | func TestAttachMapsWindowsRequestedBeforeAttach(t *testing.T) { | |||
| 720 | + | x, p := bootPeer(t, nil) | |||
| 721 | + | const win = 0x500001 | |||
| 722 | + | p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00")) | |||
| 723 | + | p.createNotify(win, false) | |||
| 724 | + | p.mapRequest(win) | |||
| 725 | + | readUntil(t, x, func() bool { return x.Stats().Events >= 2 }) | |||
| 726 | + | h := newMockhost(t) | |||
| 727 | + | src := newMockfdSource(t) | |||
| 728 | + | src.EXPECT().Remove().Maybe() | |||
| 729 | + | post, _ := newPost() | |||
| 730 | + | var got WindowInfo | |||
| 731 | + | done := false | |||
| 732 | + | h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(1)).Once() | |||
| 733 | + | h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once() | |||
| 734 | + | x.attach(src, h, post) | |||
| 735 | + | readUntil(t, x, func() bool { return done }) | |||
| 736 | + | require.Equal(t, "B", got.AppID) | |||
| 737 | + | } | |||
| 738 | + | ||||
| 739 | + | // Replies complete a map in any order (a server may answer the geometry | |||
| 740 | + | // before the properties it was asked for first): Ready fires on the last one. | |||
| 741 | + | func TestMapFlowReadyFiresOnLastReplyInAnyOrder(t *testing.T) { | |||
| 742 | + | hs := newHarness(t, nil) | |||
| 743 | + | const win = 0x500001 | |||
| 744 | + | hs.classic(win) | |||
| 745 | + | s := hs.create(win) | |||
| 746 | + | r := hs.x.tab.rec(s) | |||
| 747 | + | r.mapping, r.pending = true, propMaskAll&^propClientIDs | |||
| 748 | + | hs.expectReady(win) | |||
| 749 | + | order := []int{propCount, propPID, propClass, propNetName, propName, propHints, propNormalHints, propProtocols, propTransientFor, propWindowType, propState} | |||
| 750 | + | for i, bit := range order { | |||
| 751 | + | require.False(t, hs.x.tab.rec(s).ready, i) | |||
| 752 | + | if bit == propCount { // the geometry | |||
| 753 | + | hs.x.tab.rec(s).w, hs.x.tab.rec(s).h = 300, 200 | |||
| 754 | + | hs.x.tab.rec(s).pending &^= propGeom | |||
| 755 | + | hs.x.checkReady(s) | |||
| 756 | + | continue | |||
| 757 | + | } | |||
| 758 | + | var val []byte | |||
| 759 | + | var format uint8 = 8 | |||
| 760 | + | switch bit { | |||
| 761 | + | case propClass: | |||
| 762 | + | val = []byte("a\x00B\x00") | |||
| 763 | + | case propNetName: | |||
| 764 | + | val = []byte("t") | |||
| 765 | + | case propPID: | |||
| 766 | + | val, format = le32(5), 32 | |||
| 767 | + | default: | |||
| 768 | + | format = 32 | |||
| 769 | + | } | |||
| 770 | + | x := hs.x | |||
| 771 | + | x.applyProp(s, bit, format, val) | |||
| 772 | + | x.tab.rec(s).pending &^= 1 << bit | |||
| 773 | + | x.checkReady(s) | |||
| 774 | + | } | |||
| 775 | + | require.True(t, hs.x.tab.rec(s).ready) | |||
| 776 | + | require.Equal(t, "B", hs.ready[win].AppID) | |||
| 777 | + | require.Equal(t, "t", hs.ready[win].Title) | |||
| 778 | + | require.Equal(t, 5, hs.ready[win].PID) | |||
| 779 | + | } | |||
A internal/adapters/wayland/xwm/props.go +356 -0
| @@ -0,0 +1,356 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "strings" | |||
| 7 | + | ||||
| 8 | + | "bnema.dev/xturbo" | |||
| 9 | + | "bnema.dev/xturbo/res" | |||
| 10 | + | "bnema.dev/xturbo/xproto" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // Property indices (second Expect argument of kindProp) and their bits in | |||
| 14 | + | // record.pending. propGeom is the GetGeometry bit; propClientIDs is raised | |||
| 15 | + | // while a QueryClientIds reply stands in for a missing _NET_WM_PID. | |||
| 16 | + | const ( | |||
| 17 | + | propClass = iota | |||
| 18 | + | propName | |||
| 19 | + | propNetName | |||
| 20 | + | propHints | |||
| 21 | + | propNormalHints | |||
| 22 | + | propProtocols | |||
| 23 | + | propTransientFor | |||
| 24 | + | propWindowType | |||
| 25 | + | propState | |||
| 26 | + | propPID | |||
| 27 | + | propCount | |||
| 28 | + | ) | |||
| 29 | + | ||||
| 30 | + | const ( | |||
| 31 | + | propGeom uint16 = 1 << propCount | |||
| 32 | + | propClientIDs uint16 = propGeom << 1 | |||
| 33 | + | // propMaskAll is what a map waits for: every property and the geometry. | |||
| 34 | + | propMaskAll uint16 = propGeom<<1 - 1 | |||
| 35 | + | ) | |||
| 36 | + | ||||
| 37 | + | // WM_NORMAL_HINTS flags and WM_HINTS flags. | |||
| 38 | + | const ( | |||
| 39 | + | hintInput = 1 << 0 | |||
| 40 | + | hintMinSize = 1 << 4 | |||
| 41 | + | hintMaxSize = 1 << 5 | |||
| 42 | + | ) | |||
| 43 | + | ||||
| 44 | + | // maxPropWords bounds one GetProperty (4 KiB), so titles are truncated there. | |||
| 45 | + | const maxPropWords = 1024 | |||
| 46 | + | ||||
| 47 | + | // propIndex maps a property atom to its index, or -1 when it is not watched. | |||
| 48 | + | func (x *XWM) propIndex(a uint32) int { | |||
| 49 | + | switch a { | |||
| 50 | + | case uint32(xproto.AtomWmClass): | |||
| 51 | + | return propClass | |||
| 52 | + | case uint32(xproto.AtomWmName): | |||
| 53 | + | return propName | |||
| 54 | + | case uint32(xproto.AtomWmHints): | |||
| 55 | + | return propHints | |||
| 56 | + | case uint32(xproto.AtomWmNormalHints): | |||
| 57 | + | return propNormalHints | |||
| 58 | + | case uint32(xproto.AtomWmTransientFor): | |||
| 59 | + | return propTransientFor | |||
| 60 | + | case x.atoms[atomNetWMName]: | |||
| 61 | + | return propNetName | |||
| 62 | + | case x.atoms[atomWMProtocols]: | |||
| 63 | + | return propProtocols | |||
| 64 | + | case x.atoms[atomNetWMWindowType]: | |||
| 65 | + | return propWindowType | |||
| 66 | + | case x.atoms[atomNetWMState]: | |||
| 67 | + | return propState | |||
| 68 | + | case x.atoms[atomNetWMPID]: | |||
| 69 | + | return propPID | |||
| 70 | + | } | |||
| 71 | + | return -1 | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func (x *XWM) propAtom(idx int) xproto.Atom { | |||
| 75 | + | switch idx { | |||
| 76 | + | case propClass: | |||
| 77 | + | return xproto.AtomWmClass | |||
| 78 | + | case propName: | |||
| 79 | + | return xproto.AtomWmName | |||
| 80 | + | case propHints: | |||
| 81 | + | return xproto.AtomWmHints | |||
| 82 | + | case propNormalHints: | |||
| 83 | + | return xproto.AtomWmNormalHints | |||
| 84 | + | case propTransientFor: | |||
| 85 | + | return xproto.AtomWmTransientFor | |||
| 86 | + | case propNetName: | |||
| 87 | + | return xproto.Atom(x.atoms[atomNetWMName]) | |||
| 88 | + | case propProtocols: | |||
| 89 | + | return xproto.Atom(x.atoms[atomWMProtocols]) | |||
| 90 | + | case propWindowType: | |||
| 91 | + | return xproto.Atom(x.atoms[atomNetWMWindowType]) | |||
| 92 | + | case propState: | |||
| 93 | + | return xproto.Atom(x.atoms[atomNetWMState]) | |||
| 94 | + | default: | |||
| 95 | + | return xproto.Atom(x.atoms[atomNetWMPID]) | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // requestProp queues a GetProperty for property idx of s. | |||
| 100 | + | func (x *XWM) requestProp(s Slot, idx int) { | |||
| 101 | + | rec := x.tab.rec(s) | |||
| 102 | + | seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid), x.propAtom(idx), xproto.AtomAny, 0, maxPropWords) | |||
| 103 | + | rec.pending |= 1 << idx | |||
| 104 | + | x.expect(seq, kindProp, x.tab.tag(s), uint32(idx)) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // onPropertyNotify handles a changed property of a managed window that is | |||
| 108 | + | // being mapped or is mapped; unwatched atoms cost nothing. | |||
| 109 | + | func (x *XWM) onPropertyNotify(ev *xturbo.Event) { | |||
| 110 | + | e := xproto.PropertyNotifyOf(ev) | |||
| 111 | + | s := x.tab.lookup(uint32(e.Window())) | |||
| 112 | + | if s == 0 { | |||
| 113 | + | return | |||
| 114 | + | } | |||
| 115 | + | if rec := x.tab.rec(s); rec.kind != kindManaged || !rec.mapping { | |||
| 116 | + | return | |||
| 117 | + | } else if rec.ready && x.propIndex(uint32(e.Atom())) == propState { | |||
| 118 | + | // The XWM owns _NET_WM_STATE of a mapped window: our own writes | |||
| 119 | + | // would feed back, and clients change it with ClientMessages. | |||
| 120 | + | return | |||
| 121 | + | } | |||
| 122 | + | idx := x.propIndex(uint32(e.Atom())) | |||
| 123 | + | if idx < 0 { | |||
| 124 | + | return | |||
| 125 | + | } | |||
| 126 | + | if e.State() == xproto.PropertyDelete { | |||
| 127 | + | x.applyProp(s, idx, 0, nil) | |||
| 128 | + | return | |||
| 129 | + | } | |||
| 130 | + | x.requestProp(s, idx) | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | // onPropReply decodes one GetProperty reply into the record. An absent | |||
| 134 | + | // property (format 0) clears its fields. | |||
| 135 | + | func (x *XWM) onPropReply(s Slot, _ *record, r *xturbo.Reply) { | |||
| 136 | + | idx := int(r.Args[1]) | |||
| 137 | + | if idx < 0 || idx >= propCount { | |||
| 138 | + | return | |||
| 139 | + | } | |||
| 140 | + | var format uint8 | |||
| 141 | + | var val []byte | |||
| 142 | + | if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok { | |||
| 143 | + | format, val = rep.Format(), rep.Value() | |||
| 144 | + | } | |||
| 145 | + | x.applyProp(s, idx, format, val) | |||
| 146 | + | x.tab.rec(s).pending &^= 1 << idx | |||
| 147 | + | x.checkReady(s) | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | func word(b []byte, i int) uint32 { return binary.LittleEndian.Uint32(b[4*i:]) } | |||
| 151 | + | ||||
| 152 | + | // applyProp stores one decoded property. It does not allocate unless a string | |||
| 153 | + | // changed. Host calls come last: the record is not touched after them. | |||
| 154 | + | func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) { | |||
| 155 | + | rec := x.tab.rec(s) | |||
| 156 | + | if idx == propClass || idx == propName || idx == propNetName { | |||
| 157 | + | if format != 8 { | |||
| 158 | + | val = nil | |||
| 159 | + | } | |||
| 160 | + | } else if format != 32 { | |||
| 161 | + | val = nil | |||
| 162 | + | } | |||
| 163 | + | switch idx { | |||
| 164 | + | case propClass: | |||
| 165 | + | inst, class := val, []byte(nil) | |||
| 166 | + | if i := bytes.IndexByte(val, 0); i >= 0 { | |||
| 167 | + | inst, class = val[:i], val[i+1:] | |||
| 168 | + | if j := bytes.IndexByte(class, 0); j >= 0 { | |||
| 169 | + | class = class[:j] | |||
| 170 | + | } | |||
| 171 | + | } | |||
| 172 | + | app := class | |||
| 173 | + | if len(app) == 0 { | |||
| 174 | + | app = inst | |||
| 175 | + | } | |||
| 176 | + | if rec.class == string(app) { | |||
| 177 | + | return | |||
| 178 | + | } | |||
| 179 | + | cls := strings.ToValidUTF8(string(app), "\uFFFD") | |||
| 180 | + | if rec.class == cls { | |||
| 181 | + | return | |||
| 182 | + | } | |||
| 183 | + | rec.class = cls | |||
| 184 | + | if rec.ready { | |||
| 185 | + | x.host.AppID(s, rec.class) | |||
| 186 | + | } | |||
| 187 | + | case propName: | |||
| 188 | + | if !rec.hasNetName { | |||
| 189 | + | x.setTitle(s, val) | |||
| 190 | + | } | |||
| 191 | + | case propNetName: | |||
| 192 | + | had := rec.hasNetName | |||
| 193 | + | rec.hasNetName = len(val) > 0 | |||
| 194 | + | if rec.hasNetName { | |||
| 195 | + | x.setTitle(s, val) | |||
| 196 | + | } else if had && rec.mapping { | |||
| 197 | + | x.requestProp(s, propName) | |||
| 198 | + | } | |||
| 199 | + | case propHints: | |||
| 200 | + | rec.input = true | |||
| 201 | + | if len(val) >= 8 && word(val, 0)&hintInput != 0 { | |||
| 202 | + | rec.input = word(val, 1) != 0 | |||
| 203 | + | } | |||
| 204 | + | case propNormalHints: | |||
| 205 | + | rec.minW, rec.minH, rec.maxW, rec.maxH = 0, 0, 0, 0 | |||
| 206 | + | if len(val) >= 36 { | |||
| 207 | + | fl := word(val, 0) | |||
| 208 | + | if fl&hintMinSize != 0 { | |||
| 209 | + | rec.minW, rec.minH = int32(word(val, 5)), int32(word(val, 6)) | |||
| 210 | + | } | |||
| 211 | + | if fl&hintMaxSize != 0 { | |||
| 212 | + | rec.maxW, rec.maxH = int32(word(val, 7)), int32(word(val, 8)) | |||
| 213 | + | } | |||
| 214 | + | } | |||
| 215 | + | x.classify(rec) | |||
| 216 | + | case propProtocols: | |||
| 217 | + | rec.deleteWindow, rec.takeFocus = false, false | |||
| 218 | + | for i := 0; i+4 <= len(val); i += 4 { | |||
| 219 | + | switch binary.LittleEndian.Uint32(val[i:]) { | |||
| 220 | + | case x.atoms[atomWMDeleteWindow]: | |||
| 221 | + | rec.deleteWindow = true | |||
| 222 | + | case x.atoms[atomWMTakeFocus]: | |||
| 223 | + | rec.takeFocus = true | |||
| 224 | + | } | |||
| 225 | + | } | |||
| 226 | + | case propTransientFor: | |||
| 227 | + | var t uint32 | |||
| 228 | + | if len(val) >= 4 { | |||
| 229 | + | t = word(val, 0) | |||
| 230 | + | } | |||
| 231 | + | if t == rec.transient { | |||
| 232 | + | return | |||
| 233 | + | } | |||
| 234 | + | rec.transient = t | |||
| 235 | + | x.classify(rec) | |||
| 236 | + | if rec.ready { | |||
| 237 | + | x.host.Parent(s, x.managedSlot(t)) | |||
| 238 | + | } | |||
| 239 | + | case propWindowType: | |||
| 240 | + | rec.wtype = 0 | |||
| 241 | + | for i := 0; i+4 <= len(val); i += 4 { | |||
| 242 | + | if t := x.windowType(binary.LittleEndian.Uint32(val[i:])); t != 0 { | |||
| 243 | + | rec.wtype = t | |||
| 244 | + | break | |||
| 245 | + | } | |||
| 246 | + | } | |||
| 247 | + | x.classify(rec) | |||
| 248 | + | case propState: | |||
| 249 | + | var modal, fs bool | |||
| 250 | + | for i := 0; i+4 <= len(val); i += 4 { | |||
| 251 | + | switch binary.LittleEndian.Uint32(val[i:]) { | |||
| 252 | + | case x.atoms[atomNetWMStateModal]: | |||
| 253 | + | modal = true | |||
| 254 | + | case x.atoms[atomNetWMStateFullscreen]: | |||
| 255 | + | fs = true | |||
| 256 | + | } | |||
| 257 | + | } | |||
| 258 | + | changed := fs != rec.fullscreen | |||
| 259 | + | rec.modal, rec.fullscreen = modal, fs | |||
| 260 | + | if changed && rec.ready { | |||
| 261 | + | x.host.Fullscreen(s, fs) | |||
| 262 | + | } | |||
| 263 | + | case propPID: | |||
| 264 | + | if len(val) >= 4 { | |||
| 265 | + | rec.pid = int32(word(val, 0)) | |||
| 266 | + | return | |||
| 267 | + | } | |||
| 268 | + | rec.pid = 0 | |||
| 269 | + | if !rec.ready && rec.pending&propClientIDs == 0 && x.conn.Ext().Info(xturbo.ExtRes).Present { | |||
| 270 | + | x.queryClientIDs(s) | |||
| 271 | + | } | |||
| 272 | + | } | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | // setTitle stores a title when it differs; only a change allocates. | |||
| 276 | + | func (x *XWM) setTitle(s Slot, val []byte) { | |||
| 277 | + | rec := x.tab.rec(s) | |||
| 278 | + | if rec.title == string(val) { | |||
| 279 | + | return | |||
| 280 | + | } | |||
| 281 | + | t := strings.ToValidUTF8(string(val), "\uFFFD") | |||
| 282 | + | if rec.title == t { | |||
| 283 | + | return | |||
| 284 | + | } | |||
| 285 | + | rec.title = t | |||
| 286 | + | if rec.ready { | |||
| 287 | + | x.host.Title(s, rec.title) | |||
| 288 | + | } | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | // windowType returns the atom enum of a recognised _NET_WM_WINDOW_TYPE atom, | |||
| 292 | + | // or 0. | |||
| 293 | + | func (x *XWM) windowType(a uint32) uint8 { | |||
| 294 | + | for t := atomNetWMWindowTypeNormal; t <= atomNetWMWindowTypeDND; t++ { | |||
| 295 | + | if x.atoms[t] == a { | |||
| 296 | + | return uint8(t) | |||
| 297 | + | } | |||
| 298 | + | } | |||
| 299 | + | return 0 | |||
| 300 | + | } | |||
| 301 | + | ||||
| 302 | + | // floatingType reports whether a window type floats by itself. | |||
| 303 | + | func floatingType(t uint8) bool { | |||
| 304 | + | switch int(t) { | |||
| 305 | + | case atomNetWMWindowTypeDialog, atomNetWMWindowTypeUtility, atomNetWMWindowTypeSplash, atomNetWMWindowTypeToolbar: | |||
| 306 | + | return true | |||
| 307 | + | } | |||
| 308 | + | return false | |||
| 309 | + | } | |||
| 310 | + | ||||
| 311 | + | // classify sets rec.floating, the single source of truth: a dialog-like | |||
| 312 | + | // window type, a transient for a managed window, or a fixed size. | |||
| 313 | + | func (x *XWM) classify(rec *record) { | |||
| 314 | + | fixed := rec.minW > 0 && rec.minH > 0 && rec.minW == rec.maxW && rec.minH == rec.maxH | |||
| 315 | + | rec.floating = floatingType(rec.wtype) || x.managedSlot(rec.transient) != 0 || fixed | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | // managedSlot returns the slot of the managed window xid, or 0. | |||
| 319 | + | func (x *XWM) managedSlot(xid uint32) Slot { | |||
| 320 | + | if xid == 0 { | |||
| 321 | + | return 0 | |||
| 322 | + | } | |||
| 323 | + | if s := x.tab.lookup(xid); s != 0 && x.tab.rec(s).kind == kindManaged { | |||
| 324 | + | return s | |||
| 325 | + | } | |||
| 326 | + | return 0 | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | // queryClientIDs asks XRes for the PID of the client owning s, which stands | |||
| 330 | + | // in for a missing _NET_WM_PID: Ready waits for it (propClientIDs). | |||
| 331 | + | func (x *XWM) queryClientIDs(s Slot) { | |||
| 332 | + | rec := x.tab.rec(s) | |||
| 333 | + | specs := [1]res.ClientIdSpec{{Client: rec.xid, Mask: res.ClientIdMaskLocalClientPID}} | |||
| 334 | + | seq := res.QueryClientIds(x.conn.Buf(), specs[:]) | |||
| 335 | + | if seq == 0 { | |||
| 336 | + | return | |||
| 337 | + | } | |||
| 338 | + | rec.pending |= propClientIDs | |||
| 339 | + | x.expect(seq, kindClientIds, x.tab.tag(s), 0) | |||
| 340 | + | } | |||
| 341 | + | ||||
| 342 | + | // onClientIdsReply stores the PID XRes found and releases the map. | |||
| 343 | + | func (x *XWM) onClientIdsReply(s Slot, rec *record, r *xturbo.Reply) { | |||
| 344 | + | if q, ok := res.ParseQueryClientIdsReply(r.Raw); ok { | |||
| 345 | + | it := q.Ids() | |||
| 346 | + | for it.Next() { | |||
| 347 | + | v := it.Value() | |||
| 348 | + | if v.Spec().Mask == res.ClientIdMaskLocalClientPID && v.Value().Len() > 0 { | |||
| 349 | + | rec.pid = int32(v.Value().At(0)) | |||
| 350 | + | break | |||
| 351 | + | } | |||
| 352 | + | } | |||
| 353 | + | } | |||
| 354 | + | rec.pending &^= propClientIDs | |||
| 355 | + | x.checkReady(s) | |||
| 356 | + | } | |||
A internal/adapters/wayland/xwm/space.go +135 -0
| @@ -0,0 +1,135 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/neferwl/internal/ports" | |||
| 4 | + | ||||
| 5 | + | const maxRegions = 16 | |||
| 6 | + | ||||
| 7 | + | // Region is one output's part of the layout and where it is in X space. | |||
| 8 | + | // X = layout - Origin. | |||
| 9 | + | type Region struct { | |||
| 10 | + | Layout, X ports.Rect // logical layout rect; X rect in X pixels | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | // Space maps layout coordinates (which may be negative) to X coordinates | |||
| 14 | + | // (non-negative: the layout top-left is X (0,0)). Fixed storage, no allocs. | |||
| 15 | + | type Space struct { | |||
| 16 | + | regions [maxRegions]Region | |||
| 17 | + | n int | |||
| 18 | + | dropped int // outputs beyond maxRegions in the last Set | |||
| 19 | + | ox, oy int // layout point that is X (0,0) | |||
| 20 | + | w, h int // X screen size: the extents | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // Origin is the layout point that is X (0,0): the top-left of the layout | |||
| 24 | + | // extents. | |||
| 25 | + | func (s *Space) Origin() (ox, oy int) { return s.ox, s.oy } | |||
| 26 | + | ||||
| 27 | + | // Dropped is how many outputs of the last Set did not fit. | |||
| 28 | + | func (s *Space) Dropped() int { return s.dropped } | |||
| 29 | + | ||||
| 30 | + | // Set recomputes the mapping and reports whether it changed. | |||
| 31 | + | func (s *Space) Set(l ports.Layout) bool { | |||
| 32 | + | var next Space | |||
| 33 | + | n := len(l) | |||
| 34 | + | if n > maxRegions { | |||
| 35 | + | next.dropped = n - maxRegions | |||
| 36 | + | n = maxRegions | |||
| 37 | + | } | |||
| 38 | + | next.n = n | |||
| 39 | + | var maxX, maxY int | |||
| 40 | + | for i := 0; i < n; i++ { | |||
| 41 | + | o := l[i] | |||
| 42 | + | if i == 0 || o.X < next.ox { | |||
| 43 | + | next.ox = o.X | |||
| 44 | + | } | |||
| 45 | + | if i == 0 || o.Y < next.oy { | |||
| 46 | + | next.oy = o.Y | |||
| 47 | + | } | |||
| 48 | + | if i == 0 || o.X+o.Width > maxX { | |||
| 49 | + | maxX = o.X + o.Width | |||
| 50 | + | } | |||
| 51 | + | if i == 0 || o.Y+o.Height > maxY { | |||
| 52 | + | maxY = o.Y + o.Height | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | for i := 0; i < n; i++ { | |||
| 56 | + | o := l[i] | |||
| 57 | + | next.regions[i] = Region{ | |||
| 58 | + | Layout: ports.Rect{X: o.X, Y: o.Y, W: o.Width, H: o.Height}, | |||
| 59 | + | X: ports.Rect{X: o.X - next.ox, Y: o.Y - next.oy, W: o.Width, H: o.Height}, | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | next.w, next.h = maxX-next.ox, maxY-next.oy | |||
| 63 | + | if next.regions == s.regions && next.n == s.n && next.ox == s.ox && next.oy == s.oy && | |||
| 64 | + | next.w == s.w && next.h == s.h { | |||
| 65 | + | s.dropped = next.dropped | |||
| 66 | + | return false | |||
| 67 | + | } | |||
| 68 | + | *s = next | |||
| 69 | + | return true | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // Size is the X screen size. | |||
| 73 | + | func (s *Space) Size() (w, h int) { return s.w, s.h } | |||
| 74 | + | ||||
| 75 | + | // nearest returns the index of the region containing the layout point, or | |||
| 76 | + | // the closest one by squared distance when it is in a hole. -1 when empty. | |||
| 77 | + | func (s *Space) nearest(lx, ly int) int { | |||
| 78 | + | best, bestD := -1, 0 | |||
| 79 | + | for i := 0; i < s.n; i++ { | |||
| 80 | + | r := s.regions[i].Layout | |||
| 81 | + | dx := axisGap(lx, r.X, r.X+r.W) | |||
| 82 | + | dy := axisGap(ly, r.Y, r.Y+r.H) | |||
| 83 | + | d := dx*dx + dy*dy | |||
| 84 | + | if best < 0 || d < bestD { | |||
| 85 | + | best, bestD = i, d | |||
| 86 | + | } | |||
| 87 | + | } | |||
| 88 | + | return best | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // nearestX is nearest in X space. | |||
| 92 | + | func (s *Space) nearestX(x, y int) int { | |||
| 93 | + | best, bestD := -1, 0 | |||
| 94 | + | for i := 0; i < s.n; i++ { | |||
| 95 | + | r := s.regions[i].X | |||
| 96 | + | dx := axisGap(x, r.X, r.X+r.W) | |||
| 97 | + | dy := axisGap(y, r.Y, r.Y+r.H) | |||
| 98 | + | d := dx*dx + dy*dy | |||
| 99 | + | if best < 0 || d < bestD { | |||
| 100 | + | best, bestD = i, d | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | return best | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // axisGap is the distance from v to [lo, hi): 0 inside. | |||
| 107 | + | func axisGap(v, lo, hi int) int { | |||
| 108 | + | if v < lo { | |||
| 109 | + | return lo - v | |||
| 110 | + | } | |||
| 111 | + | if v >= hi { | |||
| 112 | + | return v - hi + 1 | |||
| 113 | + | } | |||
| 114 | + | return 0 | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // ToX converts a layout point to X, through the nearest region. | |||
| 118 | + | func (s *Space) ToX(lx, ly int) (x, y int) { | |||
| 119 | + | i := s.nearest(lx, ly) | |||
| 120 | + | if i < 0 { | |||
| 121 | + | return lx, ly | |||
| 122 | + | } | |||
| 123 | + | r := &s.regions[i] | |||
| 124 | + | return r.X.X + lx - r.Layout.X, r.X.Y + ly - r.Layout.Y | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | // ToLayout converts an X point to layout coordinates. | |||
| 128 | + | func (s *Space) ToLayout(x, y int) (lx, ly int) { | |||
| 129 | + | i := s.nearestX(x, y) | |||
| 130 | + | if i < 0 { | |||
| 131 | + | return x, y | |||
| 132 | + | } | |||
| 133 | + | r := &s.regions[i] | |||
| 134 | + | return r.Layout.X + x - r.X.X, r.Layout.Y + y - r.X.Y | |||
| 135 | + | } | |||
A internal/adapters/wayland/xwm/space_test.go +94 -0
| @@ -0,0 +1,94 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func out(x, y, w, h int) ports.OutputPlacement { | |||
| 10 | + | return ports.OutputPlacement{X: x, Y: y, Width: w, Height: h, Scale: 1} | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | func TestSpaceLayouts(t *testing.T) { | |||
| 14 | + | tests := []struct { | |||
| 15 | + | name string | |||
| 16 | + | l ports.Layout | |||
| 17 | + | w, h int | |||
| 18 | + | lx, ly, x, y int | |||
| 19 | + | }{ | |||
| 20 | + | {"single", ports.Layout{out(0, 0, 1920, 1080)}, 1920, 1080, 10, 20, 10, 20}, | |||
| 21 | + | {"side by side", ports.Layout{out(0, 0, 1920, 1080), out(1920, 0, 1280, 1024)}, 3200, 1080, 2000, 5, 2000, 5}, | |||
| 22 | + | {"left-of negative origin", ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}, 2560, 1080, -630, 20, 10, 20}, | |||
| 23 | + | {"above", ports.Layout{out(0, -500, 800, 500), out(0, 0, 800, 600)}, 800, 1100, 3, -400, 3, 100}, | |||
| 24 | + | {"below", ports.Layout{out(0, 0, 800, 600), out(0, 600, 800, 400)}, 800, 1000, 3, 700, 3, 700}, | |||
| 25 | + | {"hole nearest", ports.Layout{out(0, 0, 100, 100), out(300, 0, 100, 100)}, 400, 100, 120, 10, 120, 10}, | |||
| 26 | + | } | |||
| 27 | + | for _, tc := range tests { | |||
| 28 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 29 | + | var s Space | |||
| 30 | + | if !s.Set(tc.l) { | |||
| 31 | + | t.Fatal("first Set not changed") | |||
| 32 | + | } | |||
| 33 | + | if w, h := s.Size(); w != tc.w || h != tc.h { | |||
| 34 | + | t.Fatalf("size %dx%d, want %dx%d", w, h, tc.w, tc.h) | |||
| 35 | + | } | |||
| 36 | + | x, y := s.ToX(tc.lx, tc.ly) | |||
| 37 | + | if x != tc.x || y != tc.y { | |||
| 38 | + | t.Fatalf("ToX = %d,%d want %d,%d", x, y, tc.x, tc.y) | |||
| 39 | + | } | |||
| 40 | + | if lx, ly := s.ToLayout(x, y); lx != tc.lx || ly != tc.ly { | |||
| 41 | + | t.Fatalf("round trip = %d,%d want %d,%d", lx, ly, tc.lx, tc.ly) | |||
| 42 | + | } | |||
| 43 | + | if s.Set(tc.l) { | |||
| 44 | + | t.Fatal("identical Set reported changed") | |||
| 45 | + | } | |||
| 46 | + | }) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func TestSpaceEmptyAndOrigin(t *testing.T) { | |||
| 51 | + | var s Space | |||
| 52 | + | if x, y := s.ToX(5, 6); x != 5 || y != 6 { | |||
| 53 | + | t.Fatalf("empty ToX = %d,%d", x, y) | |||
| 54 | + | } | |||
| 55 | + | s.Set(ports.Layout{out(-640, -20, 640, 480), out(0, 0, 1920, 1080)}) | |||
| 56 | + | if ox, oy := s.Origin(); ox != -640 || oy != -20 { | |||
| 57 | + | t.Fatalf("origin = %d,%d", ox, oy) | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func TestSpaceSeventeenOutputs(t *testing.T) { | |||
| 62 | + | l := make(ports.Layout, 17) | |||
| 63 | + | for i := range l { | |||
| 64 | + | l[i] = out(i*100, 0, 100, 100) | |||
| 65 | + | } | |||
| 66 | + | var s Space | |||
| 67 | + | s.Set(l) | |||
| 68 | + | if s.Dropped() != 1 { | |||
| 69 | + | t.Fatalf("dropped = %d", s.Dropped()) | |||
| 70 | + | } | |||
| 71 | + | if w, _ := s.Size(); w != 1600 { | |||
| 72 | + | t.Fatalf("width = %d", w) | |||
| 73 | + | } | |||
| 74 | + | if x, _ := s.ToX(1550, 5); x != 1550 { | |||
| 75 | + | t.Fatalf("ToX = %d", x) | |||
| 76 | + | } | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | func TestSpaceAllocations(t *testing.T) { | |||
| 80 | + | l := ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)} | |||
| 81 | + | var s Space | |||
| 82 | + | s.Set(l) | |||
| 83 | + | n := testing.AllocsPerRun(100, func() { | |||
| 84 | + | s.Set(l) | |||
| 85 | + | x, y := s.ToX(-600, 10) | |||
| 86 | + | x, y = s.ToLayout(x, y) | |||
| 87 | + | _, _ = x, y | |||
| 88 | + | _, _ = s.Origin() | |||
| 89 | + | _, _ = s.Size() | |||
| 90 | + | }) | |||
| 91 | + | if n != 0 { | |||
| 92 | + | t.Fatalf("allocs = %v", n) | |||
| 93 | + | } | |||
| 94 | + | } | |||
A internal/adapters/wayland/xwm/streams_test.go +183 -0
| @@ -0,0 +1,183 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/hex" | |||
| 5 | + | "os" | |||
| 6 | + | "path/filepath" | |||
| 7 | + | "runtime" | |||
| 8 | + | "strings" | |||
| 9 | + | "testing" | |||
| 10 | + | "time" | |||
| 11 | + | ||||
| 12 | + | "bnema.dev/xturbo/xproto" | |||
| 13 | + | "github.com/stretchr/testify/require" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // The recorded streams are the bytes an X server wrote to the XWM during a | |||
| 17 | + | // steady-state session of one mapped window, captured from the scripted xpeer | |||
| 18 | + | // (real encoded X packets: events, GetProperty replies). The scenario is | |||
| 19 | + | // deterministic, so a replay meets the request sequence numbers it recorded. | |||
| 20 | + | // They are not xtrace captures of a real Xwayland: xpeer speaks the same wire | |||
| 21 | + | // protocol and keeps the fixture reproducible. Regenerate with | |||
| 22 | + | // | |||
| 23 | + | // XWM_UPDATE_STREAMS=1 go test ./internal/adapters/wayland/xwm -run TestRecordedStreams | |||
| 24 | + | const ( | |||
| 25 | + | streamCycles = 400 | |||
| 26 | + | streamWarm = 20 // cycles replayed before measuring | |||
| 27 | + | ) | |||
| 28 | + | ||||
| 29 | + | const streamWin = 0x500001 | |||
| 30 | + | ||||
| 31 | + | type streamKind struct { | |||
| 32 | + | name string | |||
| 33 | + | // perCycle is Events+Replies the XWM sees per cycle. | |||
| 34 | + | perCycle int | |||
| 35 | + | cycle func(hs *harness) | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | var streamKinds = []streamKind{ | |||
| 39 | + | // A title notification the XWM answers with one GetProperty whose reply | |||
| 40 | + | // equals the stored title. | |||
| 41 | + | {"property_unchanged", 2, func(hs *harness) { | |||
| 42 | + | before := hs.x.Stats().Replies | |||
| 43 | + | hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false) | |||
| 44 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |||
| 45 | + | }}, | |||
| 46 | + | // The same plus ConfigureNotify, FocusIn and MapNotify. | |||
| 47 | + | {"steady", 5, func(hs *harness) { | |||
| 48 | + | before := hs.x.Stats().Replies | |||
| 49 | + | hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false) | |||
| 50 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |||
| 51 | + | events := hs.x.Stats().Events | |||
| 52 | + | hs.p.configureNotify(streamWin) | |||
| 53 | + | hs.p.focusIn(streamWin) | |||
| 54 | + | hs.p.mapNotify(streamWin) | |||
| 55 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events >= events+3 }) | |||
| 56 | + | }}, | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // streamScenario boots, maps one window and parks the peer behind a poke so | |||
| 60 | + | // nothing but the test writes to the XWM afterwards (an allocation count is | |||
| 61 | + | // process-wide). The poke is part of the scenario, recorded or replayed. | |||
| 62 | + | func streamScenario(t *testing.T) *harness { | |||
| 63 | + | t.Helper() | |||
| 64 | + | hs := newHarness(t, nil) | |||
| 65 | + | hs.classic(streamWin) | |||
| 66 | + | hs.mapAndWait(streamWin) | |||
| 67 | + | // The steady state is a focused window: a FocusIn on it is not denied, | |||
| 68 | + | // so no request depends on how events are batched into flushes. The | |||
| 69 | + | // poke makes sure the focus requests are out before the peer pauses. | |||
| 70 | + | hs.x.Focus(hs.x.tab.lookup(streamWin)) | |||
| 71 | + | poke(hs) | |||
| 72 | + | return hs | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func poke(hs *harness) { | |||
| 76 | + | hs.t.Helper() | |||
| 77 | + | n := len(hs.p.requests()) | |||
| 78 | + | xproto.GetInputFocus(hs.x.conn.Buf()) | |||
| 79 | + | hs.x.flush() | |||
| 80 | + | hs.p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func recordStream(t *testing.T, k streamKind) []byte { | |||
| 84 | + | hs := streamScenario(t) | |||
| 85 | + | poke(hs) | |||
| 86 | + | hs.p.mu.Lock() | |||
| 87 | + | hs.p.record = true | |||
| 88 | + | hs.p.mu.Unlock() | |||
| 89 | + | for i := 0; i < streamCycles; i++ { | |||
| 90 | + | k.cycle(hs) | |||
| 91 | + | } | |||
| 92 | + | hs.p.mu.Lock() | |||
| 93 | + | defer hs.p.mu.Unlock() | |||
| 94 | + | hs.p.record = false | |||
| 95 | + | return append([]byte(nil), hs.p.tap...) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | func streamPath(k streamKind) string { | |||
| 99 | + | return filepath.Join("testdata", "streams", k.name+".hex") | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func loadStream(t *testing.T, k streamKind) []byte { | |||
| 103 | + | t.Helper() | |||
| 104 | + | text, err := os.ReadFile(streamPath(k)) | |||
| 105 | + | require.NoError(t, err) | |||
| 106 | + | b, err := hex.DecodeString(strings.Join(strings.Fields(string(text)), "")) | |||
| 107 | + | require.NoError(t, err) | |||
| 108 | + | return b | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func encodeHex(b []byte) string { | |||
| 112 | + | var sb strings.Builder | |||
| 113 | + | for len(b) > 0 { | |||
| 114 | + | n := min(32, len(b)) | |||
| 115 | + | sb.WriteString(hex.EncodeToString(b[:n])) | |||
| 116 | + | sb.WriteByte('\n') | |||
| 117 | + | b = b[n:] | |||
| 118 | + | } | |||
| 119 | + | return sb.String() | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | func TestRecordedStreams(t *testing.T) { | |||
| 123 | + | for _, k := range streamKinds { | |||
| 124 | + | t.Run(k.name, func(t *testing.T) { | |||
| 125 | + | got := recordStream(t, k) | |||
| 126 | + | if os.Getenv("XWM_UPDATE_STREAMS") != "" { | |||
| 127 | + | require.NoError(t, os.MkdirAll(filepath.Dir(streamPath(k)), 0o755)) | |||
| 128 | + | require.NoError(t, os.WriteFile(streamPath(k), []byte(encodeHex(got)), 0o644)) | |||
| 129 | + | } | |||
| 130 | + | require.Equal(t, loadStream(t, k), got, "the committed stream no longer matches the protocol the XWM speaks") | |||
| 131 | + | }) | |||
| 132 | + | } | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | // replay feeds the recorded stream and reports the mallocs of the part after | |||
| 136 | + | // the warm-up cycles. | |||
| 137 | + | func replay(t *testing.T, k streamKind) (mallocs uint64, hs *harness) { | |||
| 138 | + | t.Helper() | |||
| 139 | + | stream := loadStream(t, k) | |||
| 140 | + | hs = streamScenario(t) | |||
| 141 | + | hs.p.pause() | |||
| 142 | + | poke(hs) | |||
| 143 | + | _, err := hs.p.f.Write(stream) | |||
| 144 | + | require.NoError(t, err) | |||
| 145 | + | ||||
| 146 | + | x := hs.x | |||
| 147 | + | base := x.Stats().Events + x.Stats().Replies | |||
| 148 | + | drive := func(until uint64) { | |||
| 149 | + | deadline := time.Now().Add(10 * time.Second) | |||
| 150 | + | for x.Stats().Events+x.Stats().Replies < until { | |||
| 151 | + | if time.Now().After(deadline) { // no formatting arguments: boxing them would count as mallocs | |||
| 152 | + | t.Fatal("stream stalled") | |||
| 153 | + | } | |||
| 154 | + | before := x.Stats().Events + x.Stats().Replies | |||
| 155 | + | x.onReadable() | |||
| 156 | + | if x.Stats().Events+x.Stats().Replies == before { | |||
| 157 | + | runtime.Gosched() | |||
| 158 | + | } | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | drive(base + uint64(streamWarm*k.perCycle)) | |||
| 162 | + | target := base + uint64(streamCycles*k.perCycle) | |||
| 163 | + | ||||
| 164 | + | var m0, m1 runtime.MemStats | |||
| 165 | + | runtime.GC() | |||
| 166 | + | runtime.ReadMemStats(&m0) | |||
| 167 | + | drive(target) | |||
| 168 | + | runtime.ReadMemStats(&m1) | |||
| 169 | + | require.Equal(t, target, x.Stats().Events+x.Stats().Replies) | |||
| 170 | + | return m1.Mallocs - m0.Mallocs, hs | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | func TestPropertyUnchangedAllocations(t *testing.T) { | |||
| 174 | + | m, hs := replay(t, streamKinds[0]) | |||
| 175 | + | require.Zero(t, m, "unchanged property cycles must not allocate") | |||
| 176 | + | require.Equal(t, "net title", hs.x.tab.rec(hs.x.tab.lookup(streamWin)).title) | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | func TestDispatchAllocations(t *testing.T) { | |||
| 180 | + | m, hs := replay(t, streamKinds[1]) | |||
| 181 | + | require.Zero(t, m, "steady-state event dispatch must not allocate") | |||
| 182 | + | require.True(t, hs.x.tab.rec(hs.x.tab.lookup(streamWin)).mapped) | |||
| 183 | + | } | |||
A internal/adapters/wayland/xwm/table.go +241 -0
| @@ -0,0 +1,241 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "math/bits" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // Record kinds. | |||
| 10 | + | const ( | |||
| 11 | + | kindFree uint8 = iota | |||
| 12 | + | kindManaged | |||
| 13 | + | kindUnmanaged | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | const ( | |||
| 17 | + | initialSlots = 256 | |||
| 18 | + | // maxSlots bounds the table: a Slot travels in the low 16 bits of an | |||
| 19 | + | // Expect argument. | |||
| 20 | + | maxSlots = 1<<16 - 1 | |||
| 21 | + | indexGone = 0xFFFFFFFF // tombstone in table.index | |||
| 22 | + | ) | |||
| 23 | + | ||||
| 24 | + | // pending is the configure state not yet (or last) sent to the X server. | |||
| 25 | + | type pending struct { | |||
| 26 | + | x, y int32 | |||
| 27 | + | w, h uint32 | |||
| 28 | + | fullscreen, activated, dirty bool | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // ovRect is a layout rectangle. | |||
| 32 | + | type ovRect struct{ x, y, w, h int32 } | |||
| 33 | + | ||||
| 34 | + | // record is one X window known to the XWM. Strings are the only per-record | |||
| 35 | + | // heap data. | |||
| 36 | + | type record struct { | |||
| 37 | + | xid uint32 | |||
| 38 | + | id ports.WindowID | |||
| 39 | + | kind uint8 | |||
| 40 | + | gen uint16 // bumped on insert; stale Expect replies are told apart by it | |||
| 41 | + | ||||
| 42 | + | // X state | |||
| 43 | + | mapped, mapping, ready bool | |||
| 44 | + | x, y int32 | |||
| 45 | + | w, h uint32 | |||
| 46 | + | ||||
| 47 | + | // properties | |||
| 48 | + | pending uint16 // propMask bits still awaited | |||
| 49 | + | input, deleteWindow, takeFocus bool | |||
| 50 | + | wtype uint8 | |||
| 51 | + | modal, fullscreen bool | |||
| 52 | + | transient uint32 // xid | |||
| 53 | + | pid int32 | |||
| 54 | + | minW, minH, maxW, maxH int32 | |||
| 55 | + | title, class string // owned, replaced only on change | |||
| 56 | + | hasNetName bool | |||
| 57 | + | floating bool // dialog-like type; set with the properties | |||
| 58 | + | sized bool // the client asked for its size before Ready | |||
| 59 | + | ||||
| 60 | + | // pairing | |||
| 61 | + | serial uint64 | |||
| 62 | + | surface uint64 | |||
| 63 | + | ||||
| 64 | + | // layout position of a managed window, from Place | |||
| 65 | + | lx, ly int32 | |||
| 66 | + | placed bool | |||
| 67 | + | ||||
| 68 | + | // unmanaged (override-redirect) windows | |||
| 69 | + | noInput bool // tooltip, DND, notification: never takes the pointer | |||
| 70 | + | ovPaired bool // OverrideMapped sent, OverrideGone owed | |||
| 71 | + | ovQueued bool // slot is in XWM.ovDirty | |||
| 72 | + | ov ovRect // layout rect last sent to the host | |||
| 73 | + | ||||
| 74 | + | // configure | |||
| 75 | + | pend, sent pending | |||
| 76 | + | stateSent uint8 // _NET_WM_STATE bits last written | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // table is a slab of records with an open-addressing index by X id. No Go map. | |||
| 80 | + | type table struct { | |||
| 81 | + | recs []record // slab, len == cap, power of two | |||
| 82 | + | free []uint32 // free slot indices (LIFO) | |||
| 83 | + | index []uint32 // slot+1, 0 empty, indexGone tombstone; len == 2*len(recs) | |||
| 84 | + | shift uint8 | |||
| 85 | + | n int | |||
| 86 | + | tombs int | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func newTable(size int) *table { | |||
| 90 | + | t := &table{} | |||
| 91 | + | t.resize(size) | |||
| 92 | + | return t | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | // resize reallocates the slab, free list and index for size records and | |||
| 96 | + | // reindexes the live ones. | |||
| 97 | + | func (t *table) resize(size int) { | |||
| 98 | + | old := t.recs | |||
| 99 | + | t.recs = make([]record, size) | |||
| 100 | + | copy(t.recs, old) | |||
| 101 | + | t.free = make([]uint32, 0, size) | |||
| 102 | + | for i := min(size, maxSlots) - 1; i >= 0; i-- { | |||
| 103 | + | if i >= len(old) || old[i].kind == kindFree { | |||
| 104 | + | t.free = append(t.free, uint32(i)) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | t.index = make([]uint32, 2*size) | |||
| 108 | + | t.shift = uint8(32 - bits.TrailingZeros(uint(len(t.index)))) | |||
| 109 | + | t.reindex() | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // reindex rebuilds the index from the live records, dropping tombstones. | |||
| 113 | + | // It does not allocate. | |||
| 114 | + | func (t *table) reindex() { | |||
| 115 | + | clear(t.index) | |||
| 116 | + | t.tombs = 0 | |||
| 117 | + | for i := range t.recs { | |||
| 118 | + | if t.recs[i].kind != kindFree { | |||
| 119 | + | t.indexAdd(t.recs[i].xid, uint32(i)+1) | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | func (t *table) hash(xid uint32) uint32 { return (xid * 0x9E3779B1) >> t.shift } | |||
| 125 | + | ||||
| 126 | + | // indexAdd places slot for xid, which must be absent, in the first empty or | |||
| 127 | + | // tombstone position of its probe sequence. | |||
| 128 | + | func (t *table) indexAdd(xid, slot uint32) { | |||
| 129 | + | mask := uint32(len(t.index) - 1) | |||
| 130 | + | for i := t.hash(xid); ; i = (i + 1) & mask { | |||
| 131 | + | switch t.index[i] { | |||
| 132 | + | case 0: | |||
| 133 | + | t.index[i] = slot | |||
| 134 | + | return | |||
| 135 | + | case indexGone: | |||
| 136 | + | t.index[i] = slot | |||
| 137 | + | t.tombs-- | |||
| 138 | + | return | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | // grow doubles the table. It reports false at maxSlots. | |||
| 144 | + | func (t *table) grow() bool { | |||
| 145 | + | if len(t.recs) >= 1<<16 { | |||
| 146 | + | return false | |||
| 147 | + | } | |||
| 148 | + | t.resize(2 * len(t.recs)) | |||
| 149 | + | return len(t.free) > 0 | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | // insert returns the slot of xid, creating a managed record (generation | |||
| 153 | + | // bumped) when absent. It returns 0 when the table is full. Growth is the | |||
| 154 | + | // only allocation. | |||
| 155 | + | func (t *table) insert(xid uint32) Slot { | |||
| 156 | + | if s := t.lookup(xid); s != 0 { | |||
| 157 | + | return s | |||
| 158 | + | } | |||
| 159 | + | if len(t.free) == 0 && !t.grow() { | |||
| 160 | + | return 0 | |||
| 161 | + | } | |||
| 162 | + | i := t.free[len(t.free)-1] | |||
| 163 | + | t.free = t.free[:len(t.free)-1] | |||
| 164 | + | if t.n+t.tombs+1 > len(t.index)/4*3 { // keep an empty index cell for lookup to stop at | |||
| 165 | + | t.reindex() | |||
| 166 | + | } | |||
| 167 | + | r := &t.recs[i] | |||
| 168 | + | gen := r.gen + 1 | |||
| 169 | + | if gen == 0 { | |||
| 170 | + | gen = 1 | |||
| 171 | + | } | |||
| 172 | + | *r = record{xid: xid, kind: kindManaged, gen: gen, input: true} | |||
| 173 | + | t.indexAdd(xid, i+1) | |||
| 174 | + | t.n++ | |||
| 175 | + | return Slot(i + 1) | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | // remove frees s. The record is zeroed except its generation. | |||
| 179 | + | func (t *table) remove(s Slot) { | |||
| 180 | + | r := t.rec(s) | |||
| 181 | + | if r == nil || r.kind == kindFree { | |||
| 182 | + | return | |||
| 183 | + | } | |||
| 184 | + | mask := uint32(len(t.index) - 1) | |||
| 185 | + | for i := t.hash(r.xid); ; i = (i + 1) & mask { | |||
| 186 | + | v := t.index[i] | |||
| 187 | + | if v == 0 { | |||
| 188 | + | break | |||
| 189 | + | } | |||
| 190 | + | if v == uint32(s) { | |||
| 191 | + | t.index[i] = indexGone | |||
| 192 | + | t.tombs++ | |||
| 193 | + | break | |||
| 194 | + | } | |||
| 195 | + | } | |||
| 196 | + | *r = record{gen: r.gen} | |||
| 197 | + | t.free = append(t.free, uint32(s)-1) | |||
| 198 | + | t.n-- | |||
| 199 | + | if t.n+t.tombs > len(t.index)/4*3 { | |||
| 200 | + | t.reindex() | |||
| 201 | + | } | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // lookup returns the slot of xid, or 0. | |||
| 205 | + | func (t *table) lookup(xid uint32) Slot { | |||
| 206 | + | mask := uint32(len(t.index) - 1) | |||
| 207 | + | for i := t.hash(xid); ; i = (i + 1) & mask { | |||
| 208 | + | v := t.index[i] | |||
| 209 | + | if v == 0 { | |||
| 210 | + | return 0 | |||
| 211 | + | } | |||
| 212 | + | if v != indexGone && t.recs[v-1].xid == xid { | |||
| 213 | + | return Slot(v) | |||
| 214 | + | } | |||
| 215 | + | } | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | // rec returns the record of s, or nil when s is out of range. The pointer is | |||
| 219 | + | // valid only until the next insert, host call or anything else that can grow | |||
| 220 | + | // the table or free s: never hold it across those, fetch it again with rec(s). | |||
| 221 | + | func (t *table) rec(s Slot) *record { | |||
| 222 | + | if s == 0 || int(s) > len(t.recs) { | |||
| 223 | + | return nil | |||
| 224 | + | } | |||
| 225 | + | return &t.recs[s-1] | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | // tag is the Expect argument naming s: slot in the low 16 bits, generation | |||
| 229 | + | // in the high 16. | |||
| 230 | + | func (t *table) tag(s Slot) uint32 { return uint32(s) | uint32(t.recs[s-1].gen)<<16 } | |||
| 231 | + | ||||
| 232 | + | // live resolves an Expect tag. It returns nil when the slot was freed or | |||
| 233 | + | // reused since the request was sent. | |||
| 234 | + | func (t *table) live(tag uint32) (Slot, *record) { | |||
| 235 | + | s := Slot(tag & 0xFFFF) | |||
| 236 | + | r := t.rec(s) | |||
| 237 | + | if r == nil || r.kind == kindFree || r.gen != uint16(tag>>16) { | |||
| 238 | + | return 0, nil | |||
| 239 | + | } | |||
| 240 | + | return s, r | |||
| 241 | + | } | |||
A internal/adapters/wayland/xwm/table_test.go +138 -0
| @@ -0,0 +1,138 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "math/rand/v2" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestTableAgainstMap(t *testing.T) { | |||
| 11 | + | rng := rand.New(rand.NewPCG(1, 2)) | |||
| 12 | + | tab := newTable(initialSlots) | |||
| 13 | + | model := map[uint32]Slot{} | |||
| 14 | + | var xids []uint32 | |||
| 15 | + | for i := 0; i < 10000; i++ { | |||
| 16 | + | switch op := rng.IntN(3); { | |||
| 17 | + | case op < 2 || len(xids) == 0: | |||
| 18 | + | xid := rng.Uint32()&0x1fffff | 0x200000 | |||
| 19 | + | if _, ok := model[xid]; ok { | |||
| 20 | + | continue | |||
| 21 | + | } | |||
| 22 | + | s := tab.insert(xid) | |||
| 23 | + | require.NotZero(t, s) | |||
| 24 | + | model[xid] = s | |||
| 25 | + | xids = append(xids, xid) | |||
| 26 | + | default: | |||
| 27 | + | k := rng.IntN(len(xids)) | |||
| 28 | + | xid := xids[k] | |||
| 29 | + | xids[k] = xids[len(xids)-1] | |||
| 30 | + | xids = xids[:len(xids)-1] | |||
| 31 | + | tab.remove(model[xid]) | |||
| 32 | + | delete(model, xid) | |||
| 33 | + | } | |||
| 34 | + | if i%500 == 0 { | |||
| 35 | + | for xid, s := range model { | |||
| 36 | + | require.Equal(t, s, tab.lookup(xid)) | |||
| 37 | + | } | |||
| 38 | + | require.Zero(t, tab.lookup(rng.Uint32()|1<<31)) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | require.Equal(t, len(model), tab.n) | |||
| 42 | + | for xid, s := range model { | |||
| 43 | + | require.Equal(t, s, tab.lookup(xid)) | |||
| 44 | + | require.Equal(t, xid, tab.rec(s).xid) | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func TestTableGrowth(t *testing.T) { | |||
| 49 | + | tab := newTable(initialSlots) | |||
| 50 | + | slots := make([]Slot, 0, initialSlots+1) | |||
| 51 | + | for i := 0; i < initialSlots; i++ { | |||
| 52 | + | slots = append(slots, tab.insert(uint32(0x200000+i))) | |||
| 53 | + | } | |||
| 54 | + | require.Len(t, tab.recs, initialSlots) | |||
| 55 | + | tab.rec(slots[7]).title = "kept" | |||
| 56 | + | slots = append(slots, tab.insert(0x300000)) | |||
| 57 | + | require.Len(t, tab.recs, 2*initialSlots) | |||
| 58 | + | require.Len(t, tab.index, 4*initialSlots) | |||
| 59 | + | for i, s := range slots[:initialSlots] { | |||
| 60 | + | require.Equal(t, s, tab.lookup(uint32(0x200000+i))) | |||
| 61 | + | } | |||
| 62 | + | require.Equal(t, "kept", tab.rec(slots[7]).title, "growth keeps records") | |||
| 63 | + | require.Equal(t, slots[initialSlots], tab.lookup(0x300000)) | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func TestTableTombstoneReuse(t *testing.T) { | |||
| 67 | + | tab := newTable(initialSlots) | |||
| 68 | + | for i := 0; i < 5000; i++ { // far more churn than slots: tombstones must be recycled | |||
| 69 | + | s := tab.insert(uint32(0x200000 + i)) | |||
| 70 | + | require.NotZero(t, s) | |||
| 71 | + | require.Equal(t, s, tab.lookup(uint32(0x200000+i))) | |||
| 72 | + | tab.remove(s) | |||
| 73 | + | require.Zero(t, tab.lookup(uint32(0x200000+i))) | |||
| 74 | + | } | |||
| 75 | + | require.Len(t, tab.recs, initialSlots) | |||
| 76 | + | require.Zero(t, tab.n) | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | func TestTableGenerationAndTag(t *testing.T) { | |||
| 80 | + | tab := newTable(initialSlots) | |||
| 81 | + | s := tab.insert(0x200001) | |||
| 82 | + | tag := tab.tag(s) | |||
| 83 | + | got, rec := tab.live(tag) | |||
| 84 | + | require.Equal(t, s, got) | |||
| 85 | + | require.NotNil(t, rec) | |||
| 86 | + | tab.remove(s) | |||
| 87 | + | _, rec = tab.live(tag) | |||
| 88 | + | require.Nil(t, rec, "freed slot") | |||
| 89 | + | s2 := tab.insert(0x200002) | |||
| 90 | + | require.Equal(t, s, s2, "LIFO free list reuses the slot") | |||
| 91 | + | _, rec = tab.live(tag) | |||
| 92 | + | require.Nil(t, rec, "stale tag after slot reuse") | |||
| 93 | + | _, rec = tab.live(tab.tag(s2)) | |||
| 94 | + | require.NotNil(t, rec) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | func TestTableFull(t *testing.T) { | |||
| 98 | + | tab := newTable(initialSlots) | |||
| 99 | + | for i := 0; i < maxSlots; i++ { | |||
| 100 | + | require.NotZero(t, tab.insert(uint32(i+1)), i) | |||
| 101 | + | } | |||
| 102 | + | require.Zero(t, tab.insert(0xFFFFFF), "table is bounded by the 16-bit slot in Expect tags") | |||
| 103 | + | require.Equal(t, Slot(maxSlots), tab.lookup(maxSlots)) | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | func TestTableInsertExisting(t *testing.T) { | |||
| 107 | + | tab := newTable(initialSlots) | |||
| 108 | + | s := tab.insert(5) | |||
| 109 | + | require.Equal(t, s, tab.insert(5)) | |||
| 110 | + | require.Equal(t, 1, tab.n) | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func TestTableLookupAllocations(t *testing.T) { | |||
| 114 | + | tab := newTable(initialSlots) | |||
| 115 | + | for i := 0; i < 200; i++ { | |||
| 116 | + | tab.insert(uint32(0x200000 + i)) | |||
| 117 | + | } | |||
| 118 | + | var sink Slot | |||
| 119 | + | n := testing.AllocsPerRun(1000, func() { | |||
| 120 | + | sink += tab.lookup(0x200010) | |||
| 121 | + | sink += tab.lookup(0x7fffffff) // absent | |||
| 122 | + | _, r := tab.live(tab.tag(3)) | |||
| 123 | + | if r != nil { | |||
| 124 | + | sink++ | |||
| 125 | + | } | |||
| 126 | + | }) | |||
| 127 | + | require.Zero(t, n) | |||
| 128 | + | require.NotZero(t, sink) | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | func TestTableInsertRemoveAllocations(t *testing.T) { | |||
| 132 | + | tab := newTable(initialSlots) | |||
| 133 | + | n := testing.AllocsPerRun(1000, func() { | |||
| 134 | + | s := tab.insert(0x200001) | |||
| 135 | + | tab.remove(s) | |||
| 136 | + | }) | |||
| 137 | + | require.Zero(t, n) | |||
| 138 | + | } | |||
A internal/adapters/wayland/xwm/testdata/streams/property_unchanged.hex +950 -0
| @@ -0,0 +1,950 @@ | |||||
| 1 | + | 1c00650001005000f30300000000000000000000000000000000000000000000 | |||
| 2 | + | 0108660003000000ef0300000000000009000000000000000000000000000000 | |||
| 3 | + | 6e6574207469746c650000001c00660001005000f30300000000000000000000 | |||
| 4 | + | 0000000000000000000000000108670003000000ef0300000000000009000000 | |||
| 5 | + | 0000000000000000000000006e6574207469746c650000001c00670001005000 | |||
| 6 | + | f303000000000000000000000000000000000000000000000108680003000000 | |||
| 7 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 8 | + | 650000001c00680001005000f303000000000000000000000000000000000000 | |||
| 9 | + | 000000000108690003000000ef03000000000000090000000000000000000000 | |||
| 10 | + | 000000006e6574207469746c650000001c00690001005000f303000000000000 | |||
| 11 | + | 0000000000000000000000000000000001086a0003000000ef03000000000000 | |||
| 12 | + | 090000000000000000000000000000006e6574207469746c650000001c006a00 | |||
| 13 | + | 01005000f3030000000000000000000000000000000000000000000001086b00 | |||
| 14 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 15 | + | 7469746c650000001c006b0001005000f3030000000000000000000000000000 | |||
| 16 | + | 000000000000000001086c0003000000ef030000000000000900000000000000 | |||
| 17 | + | 00000000000000006e6574207469746c650000001c006c0001005000f3030000 | |||
| 18 | + | 000000000000000000000000000000000000000001086d0003000000ef030000 | |||
| 19 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 20 | + | 1c006d0001005000f30300000000000000000000000000000000000000000000 | |||
| 21 | + | 01086e0003000000ef0300000000000009000000000000000000000000000000 | |||
| 22 | + | 6e6574207469746c650000001c006e0001005000f30300000000000000000000 | |||
| 23 | + | 00000000000000000000000001086f0003000000ef0300000000000009000000 | |||
| 24 | + | 0000000000000000000000006e6574207469746c650000001c006f0001005000 | |||
| 25 | + | f303000000000000000000000000000000000000000000000108700003000000 | |||
| 26 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 27 | + | 650000001c00700001005000f303000000000000000000000000000000000000 | |||
| 28 | + | 000000000108710003000000ef03000000000000090000000000000000000000 | |||
| 29 | + | 000000006e6574207469746c650000001c00710001005000f303000000000000 | |||
| 30 | + | 000000000000000000000000000000000108720003000000ef03000000000000 | |||
| 31 | + | 090000000000000000000000000000006e6574207469746c650000001c007200 | |||
| 32 | + | 01005000f3030000000000000000000000000000000000000000000001087300 | |||
| 33 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 34 | + | 7469746c650000001c00730001005000f3030000000000000000000000000000 | |||
| 35 | + | 00000000000000000108740003000000ef030000000000000900000000000000 | |||
| 36 | + | 00000000000000006e6574207469746c650000001c00740001005000f3030000 | |||
| 37 | + | 00000000000000000000000000000000000000000108750003000000ef030000 | |||
| 38 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 39 | + | 1c00750001005000f30300000000000000000000000000000000000000000000 | |||
| 40 | + | 0108760003000000ef0300000000000009000000000000000000000000000000 | |||
| 41 | + | 6e6574207469746c650000001c00760001005000f30300000000000000000000 | |||
| 42 | + | 0000000000000000000000000108770003000000ef0300000000000009000000 | |||
| 43 | + | 0000000000000000000000006e6574207469746c650000001c00770001005000 | |||
| 44 | + | f303000000000000000000000000000000000000000000000108780003000000 | |||
| 45 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 46 | + | 650000001c00780001005000f303000000000000000000000000000000000000 | |||
| 47 | + | 000000000108790003000000ef03000000000000090000000000000000000000 | |||
| 48 | + | 000000006e6574207469746c650000001c00790001005000f303000000000000 | |||
| 49 | + | 0000000000000000000000000000000001087a0003000000ef03000000000000 | |||
| 50 | + | 090000000000000000000000000000006e6574207469746c650000001c007a00 | |||
| 51 | + | 01005000f3030000000000000000000000000000000000000000000001087b00 | |||
| 52 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 53 | + | 7469746c650000001c007b0001005000f3030000000000000000000000000000 | |||
| 54 | + | 000000000000000001087c0003000000ef030000000000000900000000000000 | |||
| 55 | + | 00000000000000006e6574207469746c650000001c007c0001005000f3030000 | |||
| 56 | + | 000000000000000000000000000000000000000001087d0003000000ef030000 | |||
| 57 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 58 | + | 1c007d0001005000f30300000000000000000000000000000000000000000000 | |||
| 59 | + | 01087e0003000000ef0300000000000009000000000000000000000000000000 | |||
| 60 | + | 6e6574207469746c650000001c007e0001005000f30300000000000000000000 | |||
| 61 | + | 00000000000000000000000001087f0003000000ef0300000000000009000000 | |||
| 62 | + | 0000000000000000000000006e6574207469746c650000001c007f0001005000 | |||
| 63 | + | f303000000000000000000000000000000000000000000000108800003000000 | |||
| 64 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 65 | + | 650000001c00800001005000f303000000000000000000000000000000000000 | |||
| 66 | + | 000000000108810003000000ef03000000000000090000000000000000000000 | |||
| 67 | + | 000000006e6574207469746c650000001c00810001005000f303000000000000 | |||
| 68 | + | 000000000000000000000000000000000108820003000000ef03000000000000 | |||
| 69 | + | 090000000000000000000000000000006e6574207469746c650000001c008200 | |||
| 70 | + | 01005000f3030000000000000000000000000000000000000000000001088300 | |||
| 71 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 72 | + | 7469746c650000001c00830001005000f3030000000000000000000000000000 | |||
| 73 | + | 00000000000000000108840003000000ef030000000000000900000000000000 | |||
| 74 | + | 00000000000000006e6574207469746c650000001c00840001005000f3030000 | |||
| 75 | + | 00000000000000000000000000000000000000000108850003000000ef030000 | |||
| 76 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 77 | + | 1c00850001005000f30300000000000000000000000000000000000000000000 | |||
| 78 | + | 0108860003000000ef0300000000000009000000000000000000000000000000 | |||
| 79 | + | 6e6574207469746c650000001c00860001005000f30300000000000000000000 | |||
| 80 | + | 0000000000000000000000000108870003000000ef0300000000000009000000 | |||
| 81 | + | 0000000000000000000000006e6574207469746c650000001c00870001005000 | |||
| 82 | + | f303000000000000000000000000000000000000000000000108880003000000 | |||
| 83 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 84 | + | 650000001c00880001005000f303000000000000000000000000000000000000 | |||
| 85 | + | 000000000108890003000000ef03000000000000090000000000000000000000 | |||
| 86 | + | 000000006e6574207469746c650000001c00890001005000f303000000000000 | |||
| 87 | + | 0000000000000000000000000000000001088a0003000000ef03000000000000 | |||
| 88 | + | 090000000000000000000000000000006e6574207469746c650000001c008a00 | |||
| 89 | + | 01005000f3030000000000000000000000000000000000000000000001088b00 | |||
| 90 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 91 | + | 7469746c650000001c008b0001005000f3030000000000000000000000000000 | |||
| 92 | + | 000000000000000001088c0003000000ef030000000000000900000000000000 | |||
| 93 | + | 00000000000000006e6574207469746c650000001c008c0001005000f3030000 | |||
| 94 | + | 000000000000000000000000000000000000000001088d0003000000ef030000 | |||
| 95 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 96 | + | 1c008d0001005000f30300000000000000000000000000000000000000000000 | |||
| 97 | + | 01088e0003000000ef0300000000000009000000000000000000000000000000 | |||
| 98 | + | 6e6574207469746c650000001c008e0001005000f30300000000000000000000 | |||
| 99 | + | 00000000000000000000000001088f0003000000ef0300000000000009000000 | |||
| 100 | + | 0000000000000000000000006e6574207469746c650000001c008f0001005000 | |||
| 101 | + | f303000000000000000000000000000000000000000000000108900003000000 | |||
| 102 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 103 | + | 650000001c00900001005000f303000000000000000000000000000000000000 | |||
| 104 | + | 000000000108910003000000ef03000000000000090000000000000000000000 | |||
| 105 | + | 000000006e6574207469746c650000001c00910001005000f303000000000000 | |||
| 106 | + | 000000000000000000000000000000000108920003000000ef03000000000000 | |||
| 107 | + | 090000000000000000000000000000006e6574207469746c650000001c009200 | |||
| 108 | + | 01005000f3030000000000000000000000000000000000000000000001089300 | |||
| 109 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 110 | + | 7469746c650000001c00930001005000f3030000000000000000000000000000 | |||
| 111 | + | 00000000000000000108940003000000ef030000000000000900000000000000 | |||
| 112 | + | 00000000000000006e6574207469746c650000001c00940001005000f3030000 | |||
| 113 | + | 00000000000000000000000000000000000000000108950003000000ef030000 | |||
| 114 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 115 | + | 1c00950001005000f30300000000000000000000000000000000000000000000 | |||
| 116 | + | 0108960003000000ef0300000000000009000000000000000000000000000000 | |||
| 117 | + | 6e6574207469746c650000001c00960001005000f30300000000000000000000 | |||
| 118 | + | 0000000000000000000000000108970003000000ef0300000000000009000000 | |||
| 119 | + | 0000000000000000000000006e6574207469746c650000001c00970001005000 | |||
| 120 | + | f303000000000000000000000000000000000000000000000108980003000000 | |||
| 121 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 122 | + | 650000001c00980001005000f303000000000000000000000000000000000000 | |||
| 123 | + | 000000000108990003000000ef03000000000000090000000000000000000000 | |||
| 124 | + | 000000006e6574207469746c650000001c00990001005000f303000000000000 | |||
| 125 | + | 0000000000000000000000000000000001089a0003000000ef03000000000000 | |||
| 126 | + | 090000000000000000000000000000006e6574207469746c650000001c009a00 | |||
| 127 | + | 01005000f3030000000000000000000000000000000000000000000001089b00 | |||
| 128 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 129 | + | 7469746c650000001c009b0001005000f3030000000000000000000000000000 | |||
| 130 | + | 000000000000000001089c0003000000ef030000000000000900000000000000 | |||
| 131 | + | 00000000000000006e6574207469746c650000001c009c0001005000f3030000 | |||
| 132 | + | 000000000000000000000000000000000000000001089d0003000000ef030000 | |||
| 133 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 134 | + | 1c009d0001005000f30300000000000000000000000000000000000000000000 | |||
| 135 | + | 01089e0003000000ef0300000000000009000000000000000000000000000000 | |||
| 136 | + | 6e6574207469746c650000001c009e0001005000f30300000000000000000000 | |||
| 137 | + | 00000000000000000000000001089f0003000000ef0300000000000009000000 | |||
| 138 | + | 0000000000000000000000006e6574207469746c650000001c009f0001005000 | |||
| 139 | + | f303000000000000000000000000000000000000000000000108a00003000000 | |||
| 140 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 141 | + | 650000001c00a00001005000f303000000000000000000000000000000000000 | |||
| 142 | + | 000000000108a10003000000ef03000000000000090000000000000000000000 | |||
| 143 | + | 000000006e6574207469746c650000001c00a10001005000f303000000000000 | |||
| 144 | + | 000000000000000000000000000000000108a20003000000ef03000000000000 | |||
| 145 | + | 090000000000000000000000000000006e6574207469746c650000001c00a200 | |||
| 146 | + | 01005000f303000000000000000000000000000000000000000000000108a300 | |||
| 147 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 148 | + | 7469746c650000001c00a30001005000f3030000000000000000000000000000 | |||
| 149 | + | 00000000000000000108a40003000000ef030000000000000900000000000000 | |||
| 150 | + | 00000000000000006e6574207469746c650000001c00a40001005000f3030000 | |||
| 151 | + | 00000000000000000000000000000000000000000108a50003000000ef030000 | |||
| 152 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 153 | + | 1c00a50001005000f30300000000000000000000000000000000000000000000 | |||
| 154 | + | 0108a60003000000ef0300000000000009000000000000000000000000000000 | |||
| 155 | + | 6e6574207469746c650000001c00a60001005000f30300000000000000000000 | |||
| 156 | + | 0000000000000000000000000108a70003000000ef0300000000000009000000 | |||
| 157 | + | 0000000000000000000000006e6574207469746c650000001c00a70001005000 | |||
| 158 | + | f303000000000000000000000000000000000000000000000108a80003000000 | |||
| 159 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 160 | + | 650000001c00a80001005000f303000000000000000000000000000000000000 | |||
| 161 | + | 000000000108a90003000000ef03000000000000090000000000000000000000 | |||
| 162 | + | 000000006e6574207469746c650000001c00a90001005000f303000000000000 | |||
| 163 | + | 000000000000000000000000000000000108aa0003000000ef03000000000000 | |||
| 164 | + | 090000000000000000000000000000006e6574207469746c650000001c00aa00 | |||
| 165 | + | 01005000f303000000000000000000000000000000000000000000000108ab00 | |||
| 166 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 167 | + | 7469746c650000001c00ab0001005000f3030000000000000000000000000000 | |||
| 168 | + | 00000000000000000108ac0003000000ef030000000000000900000000000000 | |||
| 169 | + | 00000000000000006e6574207469746c650000001c00ac0001005000f3030000 | |||
| 170 | + | 00000000000000000000000000000000000000000108ad0003000000ef030000 | |||
| 171 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 172 | + | 1c00ad0001005000f30300000000000000000000000000000000000000000000 | |||
| 173 | + | 0108ae0003000000ef0300000000000009000000000000000000000000000000 | |||
| 174 | + | 6e6574207469746c650000001c00ae0001005000f30300000000000000000000 | |||
| 175 | + | 0000000000000000000000000108af0003000000ef0300000000000009000000 | |||
| 176 | + | 0000000000000000000000006e6574207469746c650000001c00af0001005000 | |||
| 177 | + | f303000000000000000000000000000000000000000000000108b00003000000 | |||
| 178 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 179 | + | 650000001c00b00001005000f303000000000000000000000000000000000000 | |||
| 180 | + | 000000000108b10003000000ef03000000000000090000000000000000000000 | |||
| 181 | + | 000000006e6574207469746c650000001c00b10001005000f303000000000000 | |||
| 182 | + | 000000000000000000000000000000000108b20003000000ef03000000000000 | |||
| 183 | + | 090000000000000000000000000000006e6574207469746c650000001c00b200 | |||
| 184 | + | 01005000f303000000000000000000000000000000000000000000000108b300 | |||
| 185 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 186 | + | 7469746c650000001c00b30001005000f3030000000000000000000000000000 | |||
| 187 | + | 00000000000000000108b40003000000ef030000000000000900000000000000 | |||
| 188 | + | 00000000000000006e6574207469746c650000001c00b40001005000f3030000 | |||
| 189 | + | 00000000000000000000000000000000000000000108b50003000000ef030000 | |||
| 190 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 191 | + | 1c00b50001005000f30300000000000000000000000000000000000000000000 | |||
| 192 | + | 0108b60003000000ef0300000000000009000000000000000000000000000000 | |||
| 193 | + | 6e6574207469746c650000001c00b60001005000f30300000000000000000000 | |||
| 194 | + | 0000000000000000000000000108b70003000000ef0300000000000009000000 | |||
| 195 | + | 0000000000000000000000006e6574207469746c650000001c00b70001005000 | |||
| 196 | + | f303000000000000000000000000000000000000000000000108b80003000000 | |||
| 197 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 198 | + | 650000001c00b80001005000f303000000000000000000000000000000000000 | |||
| 199 | + | 000000000108b90003000000ef03000000000000090000000000000000000000 | |||
| 200 | + | 000000006e6574207469746c650000001c00b90001005000f303000000000000 | |||
| 201 | + | 000000000000000000000000000000000108ba0003000000ef03000000000000 | |||
| 202 | + | 090000000000000000000000000000006e6574207469746c650000001c00ba00 | |||
| 203 | + | 01005000f303000000000000000000000000000000000000000000000108bb00 | |||
| 204 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 205 | + | 7469746c650000001c00bb0001005000f3030000000000000000000000000000 | |||
| 206 | + | 00000000000000000108bc0003000000ef030000000000000900000000000000 | |||
| 207 | + | 00000000000000006e6574207469746c650000001c00bc0001005000f3030000 | |||
| 208 | + | 00000000000000000000000000000000000000000108bd0003000000ef030000 | |||
| 209 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 210 | + | 1c00bd0001005000f30300000000000000000000000000000000000000000000 | |||
| 211 | + | 0108be0003000000ef0300000000000009000000000000000000000000000000 | |||
| 212 | + | 6e6574207469746c650000001c00be0001005000f30300000000000000000000 | |||
| 213 | + | 0000000000000000000000000108bf0003000000ef0300000000000009000000 | |||
| 214 | + | 0000000000000000000000006e6574207469746c650000001c00bf0001005000 | |||
| 215 | + | f303000000000000000000000000000000000000000000000108c00003000000 | |||
| 216 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 217 | + | 650000001c00c00001005000f303000000000000000000000000000000000000 | |||
| 218 | + | 000000000108c10003000000ef03000000000000090000000000000000000000 | |||
| 219 | + | 000000006e6574207469746c650000001c00c10001005000f303000000000000 | |||
| 220 | + | 000000000000000000000000000000000108c20003000000ef03000000000000 | |||
| 221 | + | 090000000000000000000000000000006e6574207469746c650000001c00c200 | |||
| 222 | + | 01005000f303000000000000000000000000000000000000000000000108c300 | |||
| 223 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 224 | + | 7469746c650000001c00c30001005000f3030000000000000000000000000000 | |||
| 225 | + | 00000000000000000108c40003000000ef030000000000000900000000000000 | |||
| 226 | + | 00000000000000006e6574207469746c650000001c00c40001005000f3030000 | |||
| 227 | + | 00000000000000000000000000000000000000000108c50003000000ef030000 | |||
| 228 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 229 | + | 1c00c50001005000f30300000000000000000000000000000000000000000000 | |||
| 230 | + | 0108c60003000000ef0300000000000009000000000000000000000000000000 | |||
| 231 | + | 6e6574207469746c650000001c00c60001005000f30300000000000000000000 | |||
| 232 | + | 0000000000000000000000000108c70003000000ef0300000000000009000000 | |||
| 233 | + | 0000000000000000000000006e6574207469746c650000001c00c70001005000 | |||
| 234 | + | f303000000000000000000000000000000000000000000000108c80003000000 | |||
| 235 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 236 | + | 650000001c00c80001005000f303000000000000000000000000000000000000 | |||
| 237 | + | 000000000108c90003000000ef03000000000000090000000000000000000000 | |||
| 238 | + | 000000006e6574207469746c650000001c00c90001005000f303000000000000 | |||
| 239 | + | 000000000000000000000000000000000108ca0003000000ef03000000000000 | |||
| 240 | + | 090000000000000000000000000000006e6574207469746c650000001c00ca00 | |||
| 241 | + | 01005000f303000000000000000000000000000000000000000000000108cb00 | |||
| 242 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 243 | + | 7469746c650000001c00cb0001005000f3030000000000000000000000000000 | |||
| 244 | + | 00000000000000000108cc0003000000ef030000000000000900000000000000 | |||
| 245 | + | 00000000000000006e6574207469746c650000001c00cc0001005000f3030000 | |||
| 246 | + | 00000000000000000000000000000000000000000108cd0003000000ef030000 | |||
| 247 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 248 | + | 1c00cd0001005000f30300000000000000000000000000000000000000000000 | |||
| 249 | + | 0108ce0003000000ef0300000000000009000000000000000000000000000000 | |||
| 250 | + | 6e6574207469746c650000001c00ce0001005000f30300000000000000000000 | |||
| 251 | + | 0000000000000000000000000108cf0003000000ef0300000000000009000000 | |||
| 252 | + | 0000000000000000000000006e6574207469746c650000001c00cf0001005000 | |||
| 253 | + | f303000000000000000000000000000000000000000000000108d00003000000 | |||
| 254 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 255 | + | 650000001c00d00001005000f303000000000000000000000000000000000000 | |||
| 256 | + | 000000000108d10003000000ef03000000000000090000000000000000000000 | |||
| 257 | + | 000000006e6574207469746c650000001c00d10001005000f303000000000000 | |||
| 258 | + | 000000000000000000000000000000000108d20003000000ef03000000000000 | |||
| 259 | + | 090000000000000000000000000000006e6574207469746c650000001c00d200 | |||
| 260 | + | 01005000f303000000000000000000000000000000000000000000000108d300 | |||
| 261 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 262 | + | 7469746c650000001c00d30001005000f3030000000000000000000000000000 | |||
| 263 | + | 00000000000000000108d40003000000ef030000000000000900000000000000 | |||
| 264 | + | 00000000000000006e6574207469746c650000001c00d40001005000f3030000 | |||
| 265 | + | 00000000000000000000000000000000000000000108d50003000000ef030000 | |||
| 266 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 267 | + | 1c00d50001005000f30300000000000000000000000000000000000000000000 | |||
| 268 | + | 0108d60003000000ef0300000000000009000000000000000000000000000000 | |||
| 269 | + | 6e6574207469746c650000001c00d60001005000f30300000000000000000000 | |||
| 270 | + | 0000000000000000000000000108d70003000000ef0300000000000009000000 | |||
| 271 | + | 0000000000000000000000006e6574207469746c650000001c00d70001005000 | |||
| 272 | + | f303000000000000000000000000000000000000000000000108d80003000000 | |||
| 273 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 274 | + | 650000001c00d80001005000f303000000000000000000000000000000000000 | |||
| 275 | + | 000000000108d90003000000ef03000000000000090000000000000000000000 | |||
| 276 | + | 000000006e6574207469746c650000001c00d90001005000f303000000000000 | |||
| 277 | + | 000000000000000000000000000000000108da0003000000ef03000000000000 | |||
| 278 | + | 090000000000000000000000000000006e6574207469746c650000001c00da00 | |||
| 279 | + | 01005000f303000000000000000000000000000000000000000000000108db00 | |||
| 280 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 281 | + | 7469746c650000001c00db0001005000f3030000000000000000000000000000 | |||
| 282 | + | 00000000000000000108dc0003000000ef030000000000000900000000000000 | |||
| 283 | + | 00000000000000006e6574207469746c650000001c00dc0001005000f3030000 | |||
| 284 | + | 00000000000000000000000000000000000000000108dd0003000000ef030000 | |||
| 285 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 286 | + | 1c00dd0001005000f30300000000000000000000000000000000000000000000 | |||
| 287 | + | 0108de0003000000ef0300000000000009000000000000000000000000000000 | |||
| 288 | + | 6e6574207469746c650000001c00de0001005000f30300000000000000000000 | |||
| 289 | + | 0000000000000000000000000108df0003000000ef0300000000000009000000 | |||
| 290 | + | 0000000000000000000000006e6574207469746c650000001c00df0001005000 | |||
| 291 | + | f303000000000000000000000000000000000000000000000108e00003000000 | |||
| 292 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 293 | + | 650000001c00e00001005000f303000000000000000000000000000000000000 | |||
| 294 | + | 000000000108e10003000000ef03000000000000090000000000000000000000 | |||
| 295 | + | 000000006e6574207469746c650000001c00e10001005000f303000000000000 | |||
| 296 | + | 000000000000000000000000000000000108e20003000000ef03000000000000 | |||
| 297 | + | 090000000000000000000000000000006e6574207469746c650000001c00e200 | |||
| 298 | + | 01005000f303000000000000000000000000000000000000000000000108e300 | |||
| 299 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 300 | + | 7469746c650000001c00e30001005000f3030000000000000000000000000000 | |||
| 301 | + | 00000000000000000108e40003000000ef030000000000000900000000000000 | |||
| 302 | + | 00000000000000006e6574207469746c650000001c00e40001005000f3030000 | |||
| 303 | + | 00000000000000000000000000000000000000000108e50003000000ef030000 | |||
| 304 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 305 | + | 1c00e50001005000f30300000000000000000000000000000000000000000000 | |||
| 306 | + | 0108e60003000000ef0300000000000009000000000000000000000000000000 | |||
| 307 | + | 6e6574207469746c650000001c00e60001005000f30300000000000000000000 | |||
| 308 | + | 0000000000000000000000000108e70003000000ef0300000000000009000000 | |||
| 309 | + | 0000000000000000000000006e6574207469746c650000001c00e70001005000 | |||
| 310 | + | f303000000000000000000000000000000000000000000000108e80003000000 | |||
| 311 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 312 | + | 650000001c00e80001005000f303000000000000000000000000000000000000 | |||
| 313 | + | 000000000108e90003000000ef03000000000000090000000000000000000000 | |||
| 314 | + | 000000006e6574207469746c650000001c00e90001005000f303000000000000 | |||
| 315 | + | 000000000000000000000000000000000108ea0003000000ef03000000000000 | |||
| 316 | + | 090000000000000000000000000000006e6574207469746c650000001c00ea00 | |||
| 317 | + | 01005000f303000000000000000000000000000000000000000000000108eb00 | |||
| 318 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 319 | + | 7469746c650000001c00eb0001005000f3030000000000000000000000000000 | |||
| 320 | + | 00000000000000000108ec0003000000ef030000000000000900000000000000 | |||
| 321 | + | 00000000000000006e6574207469746c650000001c00ec0001005000f3030000 | |||
| 322 | + | 00000000000000000000000000000000000000000108ed0003000000ef030000 | |||
| 323 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 324 | + | 1c00ed0001005000f30300000000000000000000000000000000000000000000 | |||
| 325 | + | 0108ee0003000000ef0300000000000009000000000000000000000000000000 | |||
| 326 | + | 6e6574207469746c650000001c00ee0001005000f30300000000000000000000 | |||
| 327 | + | 0000000000000000000000000108ef0003000000ef0300000000000009000000 | |||
| 328 | + | 0000000000000000000000006e6574207469746c650000001c00ef0001005000 | |||
| 329 | + | f303000000000000000000000000000000000000000000000108f00003000000 | |||
| 330 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 331 | + | 650000001c00f00001005000f303000000000000000000000000000000000000 | |||
| 332 | + | 000000000108f10003000000ef03000000000000090000000000000000000000 | |||
| 333 | + | 000000006e6574207469746c650000001c00f10001005000f303000000000000 | |||
| 334 | + | 000000000000000000000000000000000108f20003000000ef03000000000000 | |||
| 335 | + | 090000000000000000000000000000006e6574207469746c650000001c00f200 | |||
| 336 | + | 01005000f303000000000000000000000000000000000000000000000108f300 | |||
| 337 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 338 | + | 7469746c650000001c00f30001005000f3030000000000000000000000000000 | |||
| 339 | + | 00000000000000000108f40003000000ef030000000000000900000000000000 | |||
| 340 | + | 00000000000000006e6574207469746c650000001c00f40001005000f3030000 | |||
| 341 | + | 00000000000000000000000000000000000000000108f50003000000ef030000 | |||
| 342 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 343 | + | 1c00f50001005000f30300000000000000000000000000000000000000000000 | |||
| 344 | + | 0108f60003000000ef0300000000000009000000000000000000000000000000 | |||
| 345 | + | 6e6574207469746c650000001c00f60001005000f30300000000000000000000 | |||
| 346 | + | 0000000000000000000000000108f70003000000ef0300000000000009000000 | |||
| 347 | + | 0000000000000000000000006e6574207469746c650000001c00f70001005000 | |||
| 348 | + | f303000000000000000000000000000000000000000000000108f80003000000 | |||
| 349 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 350 | + | 650000001c00f80001005000f303000000000000000000000000000000000000 | |||
| 351 | + | 000000000108f90003000000ef03000000000000090000000000000000000000 | |||
| 352 | + | 000000006e6574207469746c650000001c00f90001005000f303000000000000 | |||
| 353 | + | 000000000000000000000000000000000108fa0003000000ef03000000000000 | |||
| 354 | + | 090000000000000000000000000000006e6574207469746c650000001c00fa00 | |||
| 355 | + | 01005000f303000000000000000000000000000000000000000000000108fb00 | |||
| 356 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 357 | + | 7469746c650000001c00fb0001005000f3030000000000000000000000000000 | |||
| 358 | + | 00000000000000000108fc0003000000ef030000000000000900000000000000 | |||
| 359 | + | 00000000000000006e6574207469746c650000001c00fc0001005000f3030000 | |||
| 360 | + | 00000000000000000000000000000000000000000108fd0003000000ef030000 | |||
| 361 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 362 | + | 1c00fd0001005000f30300000000000000000000000000000000000000000000 | |||
| 363 | + | 0108fe0003000000ef0300000000000009000000000000000000000000000000 | |||
| 364 | + | 6e6574207469746c650000001c00fe0001005000f30300000000000000000000 | |||
| 365 | + | 0000000000000000000000000108ff0003000000ef0300000000000009000000 | |||
| 366 | + | 0000000000000000000000006e6574207469746c650000001c00ff0001005000 | |||
| 367 | + | f303000000000000000000000000000000000000000000000108000103000000 | |||
| 368 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 369 | + | 650000001c00000101005000f303000000000000000000000000000000000000 | |||
| 370 | + | 000000000108010103000000ef03000000000000090000000000000000000000 | |||
| 371 | + | 000000006e6574207469746c650000001c00010101005000f303000000000000 | |||
| 372 | + | 000000000000000000000000000000000108020103000000ef03000000000000 | |||
| 373 | + | 090000000000000000000000000000006e6574207469746c650000001c000201 | |||
| 374 | + | 01005000f3030000000000000000000000000000000000000000000001080301 | |||
| 375 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 376 | + | 7469746c650000001c00030101005000f3030000000000000000000000000000 | |||
| 377 | + | 00000000000000000108040103000000ef030000000000000900000000000000 | |||
| 378 | + | 00000000000000006e6574207469746c650000001c00040101005000f3030000 | |||
| 379 | + | 00000000000000000000000000000000000000000108050103000000ef030000 | |||
| 380 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 381 | + | 1c00050101005000f30300000000000000000000000000000000000000000000 | |||
| 382 | + | 0108060103000000ef0300000000000009000000000000000000000000000000 | |||
| 383 | + | 6e6574207469746c650000001c00060101005000f30300000000000000000000 | |||
| 384 | + | 0000000000000000000000000108070103000000ef0300000000000009000000 | |||
| 385 | + | 0000000000000000000000006e6574207469746c650000001c00070101005000 | |||
| 386 | + | f303000000000000000000000000000000000000000000000108080103000000 | |||
| 387 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 388 | + | 650000001c00080101005000f303000000000000000000000000000000000000 | |||
| 389 | + | 000000000108090103000000ef03000000000000090000000000000000000000 | |||
| 390 | + | 000000006e6574207469746c650000001c00090101005000f303000000000000 | |||
| 391 | + | 0000000000000000000000000000000001080a0103000000ef03000000000000 | |||
| 392 | + | 090000000000000000000000000000006e6574207469746c650000001c000a01 | |||
| 393 | + | 01005000f3030000000000000000000000000000000000000000000001080b01 | |||
| 394 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 395 | + | 7469746c650000001c000b0101005000f3030000000000000000000000000000 | |||
| 396 | + | 000000000000000001080c0103000000ef030000000000000900000000000000 | |||
| 397 | + | 00000000000000006e6574207469746c650000001c000c0101005000f3030000 | |||
| 398 | + | 000000000000000000000000000000000000000001080d0103000000ef030000 | |||
| 399 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 400 | + | 1c000d0101005000f30300000000000000000000000000000000000000000000 | |||
| 401 | + | 01080e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 402 | + | 6e6574207469746c650000001c000e0101005000f30300000000000000000000 | |||
| 403 | + | 00000000000000000000000001080f0103000000ef0300000000000009000000 | |||
| 404 | + | 0000000000000000000000006e6574207469746c650000001c000f0101005000 | |||
| 405 | + | f303000000000000000000000000000000000000000000000108100103000000 | |||
| 406 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 407 | + | 650000001c00100101005000f303000000000000000000000000000000000000 | |||
| 408 | + | 000000000108110103000000ef03000000000000090000000000000000000000 | |||
| 409 | + | 000000006e6574207469746c650000001c00110101005000f303000000000000 | |||
| 410 | + | 000000000000000000000000000000000108120103000000ef03000000000000 | |||
| 411 | + | 090000000000000000000000000000006e6574207469746c650000001c001201 | |||
| 412 | + | 01005000f3030000000000000000000000000000000000000000000001081301 | |||
| 413 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 414 | + | 7469746c650000001c00130101005000f3030000000000000000000000000000 | |||
| 415 | + | 00000000000000000108140103000000ef030000000000000900000000000000 | |||
| 416 | + | 00000000000000006e6574207469746c650000001c00140101005000f3030000 | |||
| 417 | + | 00000000000000000000000000000000000000000108150103000000ef030000 | |||
| 418 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 419 | + | 1c00150101005000f30300000000000000000000000000000000000000000000 | |||
| 420 | + | 0108160103000000ef0300000000000009000000000000000000000000000000 | |||
| 421 | + | 6e6574207469746c650000001c00160101005000f30300000000000000000000 | |||
| 422 | + | 0000000000000000000000000108170103000000ef0300000000000009000000 | |||
| 423 | + | 0000000000000000000000006e6574207469746c650000001c00170101005000 | |||
| 424 | + | f303000000000000000000000000000000000000000000000108180103000000 | |||
| 425 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 426 | + | 650000001c00180101005000f303000000000000000000000000000000000000 | |||
| 427 | + | 000000000108190103000000ef03000000000000090000000000000000000000 | |||
| 428 | + | 000000006e6574207469746c650000001c00190101005000f303000000000000 | |||
| 429 | + | 0000000000000000000000000000000001081a0103000000ef03000000000000 | |||
| 430 | + | 090000000000000000000000000000006e6574207469746c650000001c001a01 | |||
| 431 | + | 01005000f3030000000000000000000000000000000000000000000001081b01 | |||
| 432 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 433 | + | 7469746c650000001c001b0101005000f3030000000000000000000000000000 | |||
| 434 | + | 000000000000000001081c0103000000ef030000000000000900000000000000 | |||
| 435 | + | 00000000000000006e6574207469746c650000001c001c0101005000f3030000 | |||
| 436 | + | 000000000000000000000000000000000000000001081d0103000000ef030000 | |||
| 437 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 438 | + | 1c001d0101005000f30300000000000000000000000000000000000000000000 | |||
| 439 | + | 01081e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 440 | + | 6e6574207469746c650000001c001e0101005000f30300000000000000000000 | |||
| 441 | + | 00000000000000000000000001081f0103000000ef0300000000000009000000 | |||
| 442 | + | 0000000000000000000000006e6574207469746c650000001c001f0101005000 | |||
| 443 | + | f303000000000000000000000000000000000000000000000108200103000000 | |||
| 444 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 445 | + | 650000001c00200101005000f303000000000000000000000000000000000000 | |||
| 446 | + | 000000000108210103000000ef03000000000000090000000000000000000000 | |||
| 447 | + | 000000006e6574207469746c650000001c00210101005000f303000000000000 | |||
| 448 | + | 000000000000000000000000000000000108220103000000ef03000000000000 | |||
| 449 | + | 090000000000000000000000000000006e6574207469746c650000001c002201 | |||
| 450 | + | 01005000f3030000000000000000000000000000000000000000000001082301 | |||
| 451 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 452 | + | 7469746c650000001c00230101005000f3030000000000000000000000000000 | |||
| 453 | + | 00000000000000000108240103000000ef030000000000000900000000000000 | |||
| 454 | + | 00000000000000006e6574207469746c650000001c00240101005000f3030000 | |||
| 455 | + | 00000000000000000000000000000000000000000108250103000000ef030000 | |||
| 456 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 457 | + | 1c00250101005000f30300000000000000000000000000000000000000000000 | |||
| 458 | + | 0108260103000000ef0300000000000009000000000000000000000000000000 | |||
| 459 | + | 6e6574207469746c650000001c00260101005000f30300000000000000000000 | |||
| 460 | + | 0000000000000000000000000108270103000000ef0300000000000009000000 | |||
| 461 | + | 0000000000000000000000006e6574207469746c650000001c00270101005000 | |||
| 462 | + | f303000000000000000000000000000000000000000000000108280103000000 | |||
| 463 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 464 | + | 650000001c00280101005000f303000000000000000000000000000000000000 | |||
| 465 | + | 000000000108290103000000ef03000000000000090000000000000000000000 | |||
| 466 | + | 000000006e6574207469746c650000001c00290101005000f303000000000000 | |||
| 467 | + | 0000000000000000000000000000000001082a0103000000ef03000000000000 | |||
| 468 | + | 090000000000000000000000000000006e6574207469746c650000001c002a01 | |||
| 469 | + | 01005000f3030000000000000000000000000000000000000000000001082b01 | |||
| 470 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 471 | + | 7469746c650000001c002b0101005000f3030000000000000000000000000000 | |||
| 472 | + | 000000000000000001082c0103000000ef030000000000000900000000000000 | |||
| 473 | + | 00000000000000006e6574207469746c650000001c002c0101005000f3030000 | |||
| 474 | + | 000000000000000000000000000000000000000001082d0103000000ef030000 | |||
| 475 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 476 | + | 1c002d0101005000f30300000000000000000000000000000000000000000000 | |||
| 477 | + | 01082e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 478 | + | 6e6574207469746c650000001c002e0101005000f30300000000000000000000 | |||
| 479 | + | 00000000000000000000000001082f0103000000ef0300000000000009000000 | |||
| 480 | + | 0000000000000000000000006e6574207469746c650000001c002f0101005000 | |||
| 481 | + | f303000000000000000000000000000000000000000000000108300103000000 | |||
| 482 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 483 | + | 650000001c00300101005000f303000000000000000000000000000000000000 | |||
| 484 | + | 000000000108310103000000ef03000000000000090000000000000000000000 | |||
| 485 | + | 000000006e6574207469746c650000001c00310101005000f303000000000000 | |||
| 486 | + | 000000000000000000000000000000000108320103000000ef03000000000000 | |||
| 487 | + | 090000000000000000000000000000006e6574207469746c650000001c003201 | |||
| 488 | + | 01005000f3030000000000000000000000000000000000000000000001083301 | |||
| 489 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 490 | + | 7469746c650000001c00330101005000f3030000000000000000000000000000 | |||
| 491 | + | 00000000000000000108340103000000ef030000000000000900000000000000 | |||
| 492 | + | 00000000000000006e6574207469746c650000001c00340101005000f3030000 | |||
| 493 | + | 00000000000000000000000000000000000000000108350103000000ef030000 | |||
| 494 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 495 | + | 1c00350101005000f30300000000000000000000000000000000000000000000 | |||
| 496 | + | 0108360103000000ef0300000000000009000000000000000000000000000000 | |||
| 497 | + | 6e6574207469746c650000001c00360101005000f30300000000000000000000 | |||
| 498 | + | 0000000000000000000000000108370103000000ef0300000000000009000000 | |||
| 499 | + | 0000000000000000000000006e6574207469746c650000001c00370101005000 | |||
| 500 | + | f303000000000000000000000000000000000000000000000108380103000000 | |||
| 501 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 502 | + | 650000001c00380101005000f303000000000000000000000000000000000000 | |||
| 503 | + | 000000000108390103000000ef03000000000000090000000000000000000000 | |||
| 504 | + | 000000006e6574207469746c650000001c00390101005000f303000000000000 | |||
| 505 | + | 0000000000000000000000000000000001083a0103000000ef03000000000000 | |||
| 506 | + | 090000000000000000000000000000006e6574207469746c650000001c003a01 | |||
| 507 | + | 01005000f3030000000000000000000000000000000000000000000001083b01 | |||
| 508 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 509 | + | 7469746c650000001c003b0101005000f3030000000000000000000000000000 | |||
| 510 | + | 000000000000000001083c0103000000ef030000000000000900000000000000 | |||
| 511 | + | 00000000000000006e6574207469746c650000001c003c0101005000f3030000 | |||
| 512 | + | 000000000000000000000000000000000000000001083d0103000000ef030000 | |||
| 513 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 514 | + | 1c003d0101005000f30300000000000000000000000000000000000000000000 | |||
| 515 | + | 01083e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 516 | + | 6e6574207469746c650000001c003e0101005000f30300000000000000000000 | |||
| 517 | + | 00000000000000000000000001083f0103000000ef0300000000000009000000 | |||
| 518 | + | 0000000000000000000000006e6574207469746c650000001c003f0101005000 | |||
| 519 | + | f303000000000000000000000000000000000000000000000108400103000000 | |||
| 520 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 521 | + | 650000001c00400101005000f303000000000000000000000000000000000000 | |||
| 522 | + | 000000000108410103000000ef03000000000000090000000000000000000000 | |||
| 523 | + | 000000006e6574207469746c650000001c00410101005000f303000000000000 | |||
| 524 | + | 000000000000000000000000000000000108420103000000ef03000000000000 | |||
| 525 | + | 090000000000000000000000000000006e6574207469746c650000001c004201 | |||
| 526 | + | 01005000f3030000000000000000000000000000000000000000000001084301 | |||
| 527 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 528 | + | 7469746c650000001c00430101005000f3030000000000000000000000000000 | |||
| 529 | + | 00000000000000000108440103000000ef030000000000000900000000000000 | |||
| 530 | + | 00000000000000006e6574207469746c650000001c00440101005000f3030000 | |||
| 531 | + | 00000000000000000000000000000000000000000108450103000000ef030000 | |||
| 532 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 533 | + | 1c00450101005000f30300000000000000000000000000000000000000000000 | |||
| 534 | + | 0108460103000000ef0300000000000009000000000000000000000000000000 | |||
| 535 | + | 6e6574207469746c650000001c00460101005000f30300000000000000000000 | |||
| 536 | + | 0000000000000000000000000108470103000000ef0300000000000009000000 | |||
| 537 | + | 0000000000000000000000006e6574207469746c650000001c00470101005000 | |||
| 538 | + | f303000000000000000000000000000000000000000000000108480103000000 | |||
| 539 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 540 | + | 650000001c00480101005000f303000000000000000000000000000000000000 | |||
| 541 | + | 000000000108490103000000ef03000000000000090000000000000000000000 | |||
| 542 | + | 000000006e6574207469746c650000001c00490101005000f303000000000000 | |||
| 543 | + | 0000000000000000000000000000000001084a0103000000ef03000000000000 | |||
| 544 | + | 090000000000000000000000000000006e6574207469746c650000001c004a01 | |||
| 545 | + | 01005000f3030000000000000000000000000000000000000000000001084b01 | |||
| 546 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 547 | + | 7469746c650000001c004b0101005000f3030000000000000000000000000000 | |||
| 548 | + | 000000000000000001084c0103000000ef030000000000000900000000000000 | |||
| 549 | + | 00000000000000006e6574207469746c650000001c004c0101005000f3030000 | |||
| 550 | + | 000000000000000000000000000000000000000001084d0103000000ef030000 | |||
| 551 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 552 | + | 1c004d0101005000f30300000000000000000000000000000000000000000000 | |||
| 553 | + | 01084e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 554 | + | 6e6574207469746c650000001c004e0101005000f30300000000000000000000 | |||
| 555 | + | 00000000000000000000000001084f0103000000ef0300000000000009000000 | |||
| 556 | + | 0000000000000000000000006e6574207469746c650000001c004f0101005000 | |||
| 557 | + | f303000000000000000000000000000000000000000000000108500103000000 | |||
| 558 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 559 | + | 650000001c00500101005000f303000000000000000000000000000000000000 | |||
| 560 | + | 000000000108510103000000ef03000000000000090000000000000000000000 | |||
| 561 | + | 000000006e6574207469746c650000001c00510101005000f303000000000000 | |||
| 562 | + | 000000000000000000000000000000000108520103000000ef03000000000000 | |||
| 563 | + | 090000000000000000000000000000006e6574207469746c650000001c005201 | |||
| 564 | + | 01005000f3030000000000000000000000000000000000000000000001085301 | |||
| 565 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 566 | + | 7469746c650000001c00530101005000f3030000000000000000000000000000 | |||
| 567 | + | 00000000000000000108540103000000ef030000000000000900000000000000 | |||
| 568 | + | 00000000000000006e6574207469746c650000001c00540101005000f3030000 | |||
| 569 | + | 00000000000000000000000000000000000000000108550103000000ef030000 | |||
| 570 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 571 | + | 1c00550101005000f30300000000000000000000000000000000000000000000 | |||
| 572 | + | 0108560103000000ef0300000000000009000000000000000000000000000000 | |||
| 573 | + | 6e6574207469746c650000001c00560101005000f30300000000000000000000 | |||
| 574 | + | 0000000000000000000000000108570103000000ef0300000000000009000000 | |||
| 575 | + | 0000000000000000000000006e6574207469746c650000001c00570101005000 | |||
| 576 | + | f303000000000000000000000000000000000000000000000108580103000000 | |||
| 577 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 578 | + | 650000001c00580101005000f303000000000000000000000000000000000000 | |||
| 579 | + | 000000000108590103000000ef03000000000000090000000000000000000000 | |||
| 580 | + | 000000006e6574207469746c650000001c00590101005000f303000000000000 | |||
| 581 | + | 0000000000000000000000000000000001085a0103000000ef03000000000000 | |||
| 582 | + | 090000000000000000000000000000006e6574207469746c650000001c005a01 | |||
| 583 | + | 01005000f3030000000000000000000000000000000000000000000001085b01 | |||
| 584 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 585 | + | 7469746c650000001c005b0101005000f3030000000000000000000000000000 | |||
| 586 | + | 000000000000000001085c0103000000ef030000000000000900000000000000 | |||
| 587 | + | 00000000000000006e6574207469746c650000001c005c0101005000f3030000 | |||
| 588 | + | 000000000000000000000000000000000000000001085d0103000000ef030000 | |||
| 589 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 590 | + | 1c005d0101005000f30300000000000000000000000000000000000000000000 | |||
| 591 | + | 01085e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 592 | + | 6e6574207469746c650000001c005e0101005000f30300000000000000000000 | |||
| 593 | + | 00000000000000000000000001085f0103000000ef0300000000000009000000 | |||
| 594 | + | 0000000000000000000000006e6574207469746c650000001c005f0101005000 | |||
| 595 | + | f303000000000000000000000000000000000000000000000108600103000000 | |||
| 596 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 597 | + | 650000001c00600101005000f303000000000000000000000000000000000000 | |||
| 598 | + | 000000000108610103000000ef03000000000000090000000000000000000000 | |||
| 599 | + | 000000006e6574207469746c650000001c00610101005000f303000000000000 | |||
| 600 | + | 000000000000000000000000000000000108620103000000ef03000000000000 | |||
| 601 | + | 090000000000000000000000000000006e6574207469746c650000001c006201 | |||
| 602 | + | 01005000f3030000000000000000000000000000000000000000000001086301 | |||
| 603 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 604 | + | 7469746c650000001c00630101005000f3030000000000000000000000000000 | |||
| 605 | + | 00000000000000000108640103000000ef030000000000000900000000000000 | |||
| 606 | + | 00000000000000006e6574207469746c650000001c00640101005000f3030000 | |||
| 607 | + | 00000000000000000000000000000000000000000108650103000000ef030000 | |||
| 608 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 609 | + | 1c00650101005000f30300000000000000000000000000000000000000000000 | |||
| 610 | + | 0108660103000000ef0300000000000009000000000000000000000000000000 | |||
| 611 | + | 6e6574207469746c650000001c00660101005000f30300000000000000000000 | |||
| 612 | + | 0000000000000000000000000108670103000000ef0300000000000009000000 | |||
| 613 | + | 0000000000000000000000006e6574207469746c650000001c00670101005000 | |||
| 614 | + | f303000000000000000000000000000000000000000000000108680103000000 | |||
| 615 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 616 | + | 650000001c00680101005000f303000000000000000000000000000000000000 | |||
| 617 | + | 000000000108690103000000ef03000000000000090000000000000000000000 | |||
| 618 | + | 000000006e6574207469746c650000001c00690101005000f303000000000000 | |||
| 619 | + | 0000000000000000000000000000000001086a0103000000ef03000000000000 | |||
| 620 | + | 090000000000000000000000000000006e6574207469746c650000001c006a01 | |||
| 621 | + | 01005000f3030000000000000000000000000000000000000000000001086b01 | |||
| 622 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 623 | + | 7469746c650000001c006b0101005000f3030000000000000000000000000000 | |||
| 624 | + | 000000000000000001086c0103000000ef030000000000000900000000000000 | |||
| 625 | + | 00000000000000006e6574207469746c650000001c006c0101005000f3030000 | |||
| 626 | + | 000000000000000000000000000000000000000001086d0103000000ef030000 | |||
| 627 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 628 | + | 1c006d0101005000f30300000000000000000000000000000000000000000000 | |||
| 629 | + | 01086e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 630 | + | 6e6574207469746c650000001c006e0101005000f30300000000000000000000 | |||
| 631 | + | 00000000000000000000000001086f0103000000ef0300000000000009000000 | |||
| 632 | + | 0000000000000000000000006e6574207469746c650000001c006f0101005000 | |||
| 633 | + | f303000000000000000000000000000000000000000000000108700103000000 | |||
| 634 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 635 | + | 650000001c00700101005000f303000000000000000000000000000000000000 | |||
| 636 | + | 000000000108710103000000ef03000000000000090000000000000000000000 | |||
| 637 | + | 000000006e6574207469746c650000001c00710101005000f303000000000000 | |||
| 638 | + | 000000000000000000000000000000000108720103000000ef03000000000000 | |||
| 639 | + | 090000000000000000000000000000006e6574207469746c650000001c007201 | |||
| 640 | + | 01005000f3030000000000000000000000000000000000000000000001087301 | |||
| 641 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 642 | + | 7469746c650000001c00730101005000f3030000000000000000000000000000 | |||
| 643 | + | 00000000000000000108740103000000ef030000000000000900000000000000 | |||
| 644 | + | 00000000000000006e6574207469746c650000001c00740101005000f3030000 | |||
| 645 | + | 00000000000000000000000000000000000000000108750103000000ef030000 | |||
| 646 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 647 | + | 1c00750101005000f30300000000000000000000000000000000000000000000 | |||
| 648 | + | 0108760103000000ef0300000000000009000000000000000000000000000000 | |||
| 649 | + | 6e6574207469746c650000001c00760101005000f30300000000000000000000 | |||
| 650 | + | 0000000000000000000000000108770103000000ef0300000000000009000000 | |||
| 651 | + | 0000000000000000000000006e6574207469746c650000001c00770101005000 | |||
| 652 | + | f303000000000000000000000000000000000000000000000108780103000000 | |||
| 653 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 654 | + | 650000001c00780101005000f303000000000000000000000000000000000000 | |||
| 655 | + | 000000000108790103000000ef03000000000000090000000000000000000000 | |||
| 656 | + | 000000006e6574207469746c650000001c00790101005000f303000000000000 | |||
| 657 | + | 0000000000000000000000000000000001087a0103000000ef03000000000000 | |||
| 658 | + | 090000000000000000000000000000006e6574207469746c650000001c007a01 | |||
| 659 | + | 01005000f3030000000000000000000000000000000000000000000001087b01 | |||
| 660 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 661 | + | 7469746c650000001c007b0101005000f3030000000000000000000000000000 | |||
| 662 | + | 000000000000000001087c0103000000ef030000000000000900000000000000 | |||
| 663 | + | 00000000000000006e6574207469746c650000001c007c0101005000f3030000 | |||
| 664 | + | 000000000000000000000000000000000000000001087d0103000000ef030000 | |||
| 665 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 666 | + | 1c007d0101005000f30300000000000000000000000000000000000000000000 | |||
| 667 | + | 01087e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 668 | + | 6e6574207469746c650000001c007e0101005000f30300000000000000000000 | |||
| 669 | + | 00000000000000000000000001087f0103000000ef0300000000000009000000 | |||
| 670 | + | 0000000000000000000000006e6574207469746c650000001c007f0101005000 | |||
| 671 | + | f303000000000000000000000000000000000000000000000108800103000000 | |||
| 672 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 673 | + | 650000001c00800101005000f303000000000000000000000000000000000000 | |||
| 674 | + | 000000000108810103000000ef03000000000000090000000000000000000000 | |||
| 675 | + | 000000006e6574207469746c650000001c00810101005000f303000000000000 | |||
| 676 | + | 000000000000000000000000000000000108820103000000ef03000000000000 | |||
| 677 | + | 090000000000000000000000000000006e6574207469746c650000001c008201 | |||
| 678 | + | 01005000f3030000000000000000000000000000000000000000000001088301 | |||
| 679 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 680 | + | 7469746c650000001c00830101005000f3030000000000000000000000000000 | |||
| 681 | + | 00000000000000000108840103000000ef030000000000000900000000000000 | |||
| 682 | + | 00000000000000006e6574207469746c650000001c00840101005000f3030000 | |||
| 683 | + | 00000000000000000000000000000000000000000108850103000000ef030000 | |||
| 684 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 685 | + | 1c00850101005000f30300000000000000000000000000000000000000000000 | |||
| 686 | + | 0108860103000000ef0300000000000009000000000000000000000000000000 | |||
| 687 | + | 6e6574207469746c650000001c00860101005000f30300000000000000000000 | |||
| 688 | + | 0000000000000000000000000108870103000000ef0300000000000009000000 | |||
| 689 | + | 0000000000000000000000006e6574207469746c650000001c00870101005000 | |||
| 690 | + | f303000000000000000000000000000000000000000000000108880103000000 | |||
| 691 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 692 | + | 650000001c00880101005000f303000000000000000000000000000000000000 | |||
| 693 | + | 000000000108890103000000ef03000000000000090000000000000000000000 | |||
| 694 | + | 000000006e6574207469746c650000001c00890101005000f303000000000000 | |||
| 695 | + | 0000000000000000000000000000000001088a0103000000ef03000000000000 | |||
| 696 | + | 090000000000000000000000000000006e6574207469746c650000001c008a01 | |||
| 697 | + | 01005000f3030000000000000000000000000000000000000000000001088b01 | |||
| 698 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 699 | + | 7469746c650000001c008b0101005000f3030000000000000000000000000000 | |||
| 700 | + | 000000000000000001088c0103000000ef030000000000000900000000000000 | |||
| 701 | + | 00000000000000006e6574207469746c650000001c008c0101005000f3030000 | |||
| 702 | + | 000000000000000000000000000000000000000001088d0103000000ef030000 | |||
| 703 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 704 | + | 1c008d0101005000f30300000000000000000000000000000000000000000000 | |||
| 705 | + | 01088e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 706 | + | 6e6574207469746c650000001c008e0101005000f30300000000000000000000 | |||
| 707 | + | 00000000000000000000000001088f0103000000ef0300000000000009000000 | |||
| 708 | + | 0000000000000000000000006e6574207469746c650000001c008f0101005000 | |||
| 709 | + | f303000000000000000000000000000000000000000000000108900103000000 | |||
| 710 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 711 | + | 650000001c00900101005000f303000000000000000000000000000000000000 | |||
| 712 | + | 000000000108910103000000ef03000000000000090000000000000000000000 | |||
| 713 | + | 000000006e6574207469746c650000001c00910101005000f303000000000000 | |||
| 714 | + | 000000000000000000000000000000000108920103000000ef03000000000000 | |||
| 715 | + | 090000000000000000000000000000006e6574207469746c650000001c009201 | |||
| 716 | + | 01005000f3030000000000000000000000000000000000000000000001089301 | |||
| 717 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 718 | + | 7469746c650000001c00930101005000f3030000000000000000000000000000 | |||
| 719 | + | 00000000000000000108940103000000ef030000000000000900000000000000 | |||
| 720 | + | 00000000000000006e6574207469746c650000001c00940101005000f3030000 | |||
| 721 | + | 00000000000000000000000000000000000000000108950103000000ef030000 | |||
| 722 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 723 | + | 1c00950101005000f30300000000000000000000000000000000000000000000 | |||
| 724 | + | 0108960103000000ef0300000000000009000000000000000000000000000000 | |||
| 725 | + | 6e6574207469746c650000001c00960101005000f30300000000000000000000 | |||
| 726 | + | 0000000000000000000000000108970103000000ef0300000000000009000000 | |||
| 727 | + | 0000000000000000000000006e6574207469746c650000001c00970101005000 | |||
| 728 | + | f303000000000000000000000000000000000000000000000108980103000000 | |||
| 729 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 730 | + | 650000001c00980101005000f303000000000000000000000000000000000000 | |||
| 731 | + | 000000000108990103000000ef03000000000000090000000000000000000000 | |||
| 732 | + | 000000006e6574207469746c650000001c00990101005000f303000000000000 | |||
| 733 | + | 0000000000000000000000000000000001089a0103000000ef03000000000000 | |||
| 734 | + | 090000000000000000000000000000006e6574207469746c650000001c009a01 | |||
| 735 | + | 01005000f3030000000000000000000000000000000000000000000001089b01 | |||
| 736 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 737 | + | 7469746c650000001c009b0101005000f3030000000000000000000000000000 | |||
| 738 | + | 000000000000000001089c0103000000ef030000000000000900000000000000 | |||
| 739 | + | 00000000000000006e6574207469746c650000001c009c0101005000f3030000 | |||
| 740 | + | 000000000000000000000000000000000000000001089d0103000000ef030000 | |||
| 741 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 742 | + | 1c009d0101005000f30300000000000000000000000000000000000000000000 | |||
| 743 | + | 01089e0103000000ef0300000000000009000000000000000000000000000000 | |||
| 744 | + | 6e6574207469746c650000001c009e0101005000f30300000000000000000000 | |||
| 745 | + | 00000000000000000000000001089f0103000000ef0300000000000009000000 | |||
| 746 | + | 0000000000000000000000006e6574207469746c650000001c009f0101005000 | |||
| 747 | + | f303000000000000000000000000000000000000000000000108a00103000000 | |||
| 748 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 749 | + | 650000001c00a00101005000f303000000000000000000000000000000000000 | |||
| 750 | + | 000000000108a10103000000ef03000000000000090000000000000000000000 | |||
| 751 | + | 000000006e6574207469746c650000001c00a10101005000f303000000000000 | |||
| 752 | + | 000000000000000000000000000000000108a20103000000ef03000000000000 | |||
| 753 | + | 090000000000000000000000000000006e6574207469746c650000001c00a201 | |||
| 754 | + | 01005000f303000000000000000000000000000000000000000000000108a301 | |||
| 755 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 756 | + | 7469746c650000001c00a30101005000f3030000000000000000000000000000 | |||
| 757 | + | 00000000000000000108a40103000000ef030000000000000900000000000000 | |||
| 758 | + | 00000000000000006e6574207469746c650000001c00a40101005000f3030000 | |||
| 759 | + | 00000000000000000000000000000000000000000108a50103000000ef030000 | |||
| 760 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 761 | + | 1c00a50101005000f30300000000000000000000000000000000000000000000 | |||
| 762 | + | 0108a60103000000ef0300000000000009000000000000000000000000000000 | |||
| 763 | + | 6e6574207469746c650000001c00a60101005000f30300000000000000000000 | |||
| 764 | + | 0000000000000000000000000108a70103000000ef0300000000000009000000 | |||
| 765 | + | 0000000000000000000000006e6574207469746c650000001c00a70101005000 | |||
| 766 | + | f303000000000000000000000000000000000000000000000108a80103000000 | |||
| 767 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 768 | + | 650000001c00a80101005000f303000000000000000000000000000000000000 | |||
| 769 | + | 000000000108a90103000000ef03000000000000090000000000000000000000 | |||
| 770 | + | 000000006e6574207469746c650000001c00a90101005000f303000000000000 | |||
| 771 | + | 000000000000000000000000000000000108aa0103000000ef03000000000000 | |||
| 772 | + | 090000000000000000000000000000006e6574207469746c650000001c00aa01 | |||
| 773 | + | 01005000f303000000000000000000000000000000000000000000000108ab01 | |||
| 774 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 775 | + | 7469746c650000001c00ab0101005000f3030000000000000000000000000000 | |||
| 776 | + | 00000000000000000108ac0103000000ef030000000000000900000000000000 | |||
| 777 | + | 00000000000000006e6574207469746c650000001c00ac0101005000f3030000 | |||
| 778 | + | 00000000000000000000000000000000000000000108ad0103000000ef030000 | |||
| 779 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 780 | + | 1c00ad0101005000f30300000000000000000000000000000000000000000000 | |||
| 781 | + | 0108ae0103000000ef0300000000000009000000000000000000000000000000 | |||
| 782 | + | 6e6574207469746c650000001c00ae0101005000f30300000000000000000000 | |||
| 783 | + | 0000000000000000000000000108af0103000000ef0300000000000009000000 | |||
| 784 | + | 0000000000000000000000006e6574207469746c650000001c00af0101005000 | |||
| 785 | + | f303000000000000000000000000000000000000000000000108b00103000000 | |||
| 786 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 787 | + | 650000001c00b00101005000f303000000000000000000000000000000000000 | |||
| 788 | + | 000000000108b10103000000ef03000000000000090000000000000000000000 | |||
| 789 | + | 000000006e6574207469746c650000001c00b10101005000f303000000000000 | |||
| 790 | + | 000000000000000000000000000000000108b20103000000ef03000000000000 | |||
| 791 | + | 090000000000000000000000000000006e6574207469746c650000001c00b201 | |||
| 792 | + | 01005000f303000000000000000000000000000000000000000000000108b301 | |||
| 793 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 794 | + | 7469746c650000001c00b30101005000f3030000000000000000000000000000 | |||
| 795 | + | 00000000000000000108b40103000000ef030000000000000900000000000000 | |||
| 796 | + | 00000000000000006e6574207469746c650000001c00b40101005000f3030000 | |||
| 797 | + | 00000000000000000000000000000000000000000108b50103000000ef030000 | |||
| 798 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 799 | + | 1c00b50101005000f30300000000000000000000000000000000000000000000 | |||
| 800 | + | 0108b60103000000ef0300000000000009000000000000000000000000000000 | |||
| 801 | + | 6e6574207469746c650000001c00b60101005000f30300000000000000000000 | |||
| 802 | + | 0000000000000000000000000108b70103000000ef0300000000000009000000 | |||
| 803 | + | 0000000000000000000000006e6574207469746c650000001c00b70101005000 | |||
| 804 | + | f303000000000000000000000000000000000000000000000108b80103000000 | |||
| 805 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 806 | + | 650000001c00b80101005000f303000000000000000000000000000000000000 | |||
| 807 | + | 000000000108b90103000000ef03000000000000090000000000000000000000 | |||
| 808 | + | 000000006e6574207469746c650000001c00b90101005000f303000000000000 | |||
| 809 | + | 000000000000000000000000000000000108ba0103000000ef03000000000000 | |||
| 810 | + | 090000000000000000000000000000006e6574207469746c650000001c00ba01 | |||
| 811 | + | 01005000f303000000000000000000000000000000000000000000000108bb01 | |||
| 812 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 813 | + | 7469746c650000001c00bb0101005000f3030000000000000000000000000000 | |||
| 814 | + | 00000000000000000108bc0103000000ef030000000000000900000000000000 | |||
| 815 | + | 00000000000000006e6574207469746c650000001c00bc0101005000f3030000 | |||
| 816 | + | 00000000000000000000000000000000000000000108bd0103000000ef030000 | |||
| 817 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 818 | + | 1c00bd0101005000f30300000000000000000000000000000000000000000000 | |||
| 819 | + | 0108be0103000000ef0300000000000009000000000000000000000000000000 | |||
| 820 | + | 6e6574207469746c650000001c00be0101005000f30300000000000000000000 | |||
| 821 | + | 0000000000000000000000000108bf0103000000ef0300000000000009000000 | |||
| 822 | + | 0000000000000000000000006e6574207469746c650000001c00bf0101005000 | |||
| 823 | + | f303000000000000000000000000000000000000000000000108c00103000000 | |||
| 824 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 825 | + | 650000001c00c00101005000f303000000000000000000000000000000000000 | |||
| 826 | + | 000000000108c10103000000ef03000000000000090000000000000000000000 | |||
| 827 | + | 000000006e6574207469746c650000001c00c10101005000f303000000000000 | |||
| 828 | + | 000000000000000000000000000000000108c20103000000ef03000000000000 | |||
| 829 | + | 090000000000000000000000000000006e6574207469746c650000001c00c201 | |||
| 830 | + | 01005000f303000000000000000000000000000000000000000000000108c301 | |||
| 831 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 832 | + | 7469746c650000001c00c30101005000f3030000000000000000000000000000 | |||
| 833 | + | 00000000000000000108c40103000000ef030000000000000900000000000000 | |||
| 834 | + | 00000000000000006e6574207469746c650000001c00c40101005000f3030000 | |||
| 835 | + | 00000000000000000000000000000000000000000108c50103000000ef030000 | |||
| 836 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 837 | + | 1c00c50101005000f30300000000000000000000000000000000000000000000 | |||
| 838 | + | 0108c60103000000ef0300000000000009000000000000000000000000000000 | |||
| 839 | + | 6e6574207469746c650000001c00c60101005000f30300000000000000000000 | |||
| 840 | + | 0000000000000000000000000108c70103000000ef0300000000000009000000 | |||
| 841 | + | 0000000000000000000000006e6574207469746c650000001c00c70101005000 | |||
| 842 | + | f303000000000000000000000000000000000000000000000108c80103000000 | |||
| 843 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 844 | + | 650000001c00c80101005000f303000000000000000000000000000000000000 | |||
| 845 | + | 000000000108c90103000000ef03000000000000090000000000000000000000 | |||
| 846 | + | 000000006e6574207469746c650000001c00c90101005000f303000000000000 | |||
| 847 | + | 000000000000000000000000000000000108ca0103000000ef03000000000000 | |||
| 848 | + | 090000000000000000000000000000006e6574207469746c650000001c00ca01 | |||
| 849 | + | 01005000f303000000000000000000000000000000000000000000000108cb01 | |||
| 850 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 851 | + | 7469746c650000001c00cb0101005000f3030000000000000000000000000000 | |||
| 852 | + | 00000000000000000108cc0103000000ef030000000000000900000000000000 | |||
| 853 | + | 00000000000000006e6574207469746c650000001c00cc0101005000f3030000 | |||
| 854 | + | 00000000000000000000000000000000000000000108cd0103000000ef030000 | |||
| 855 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 856 | + | 1c00cd0101005000f30300000000000000000000000000000000000000000000 | |||
| 857 | + | 0108ce0103000000ef0300000000000009000000000000000000000000000000 | |||
| 858 | + | 6e6574207469746c650000001c00ce0101005000f30300000000000000000000 | |||
| 859 | + | 0000000000000000000000000108cf0103000000ef0300000000000009000000 | |||
| 860 | + | 0000000000000000000000006e6574207469746c650000001c00cf0101005000 | |||
| 861 | + | f303000000000000000000000000000000000000000000000108d00103000000 | |||
| 862 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 863 | + | 650000001c00d00101005000f303000000000000000000000000000000000000 | |||
| 864 | + | 000000000108d10103000000ef03000000000000090000000000000000000000 | |||
| 865 | + | 000000006e6574207469746c650000001c00d10101005000f303000000000000 | |||
| 866 | + | 000000000000000000000000000000000108d20103000000ef03000000000000 | |||
| 867 | + | 090000000000000000000000000000006e6574207469746c650000001c00d201 | |||
| 868 | + | 01005000f303000000000000000000000000000000000000000000000108d301 | |||
| 869 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 870 | + | 7469746c650000001c00d30101005000f3030000000000000000000000000000 | |||
| 871 | + | 00000000000000000108d40103000000ef030000000000000900000000000000 | |||
| 872 | + | 00000000000000006e6574207469746c650000001c00d40101005000f3030000 | |||
| 873 | + | 00000000000000000000000000000000000000000108d50103000000ef030000 | |||
| 874 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 875 | + | 1c00d50101005000f30300000000000000000000000000000000000000000000 | |||
| 876 | + | 0108d60103000000ef0300000000000009000000000000000000000000000000 | |||
| 877 | + | 6e6574207469746c650000001c00d60101005000f30300000000000000000000 | |||
| 878 | + | 0000000000000000000000000108d70103000000ef0300000000000009000000 | |||
| 879 | + | 0000000000000000000000006e6574207469746c650000001c00d70101005000 | |||
| 880 | + | f303000000000000000000000000000000000000000000000108d80103000000 | |||
| 881 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 882 | + | 650000001c00d80101005000f303000000000000000000000000000000000000 | |||
| 883 | + | 000000000108d90103000000ef03000000000000090000000000000000000000 | |||
| 884 | + | 000000006e6574207469746c650000001c00d90101005000f303000000000000 | |||
| 885 | + | 000000000000000000000000000000000108da0103000000ef03000000000000 | |||
| 886 | + | 090000000000000000000000000000006e6574207469746c650000001c00da01 | |||
| 887 | + | 01005000f303000000000000000000000000000000000000000000000108db01 | |||
| 888 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 889 | + | 7469746c650000001c00db0101005000f3030000000000000000000000000000 | |||
| 890 | + | 00000000000000000108dc0103000000ef030000000000000900000000000000 | |||
| 891 | + | 00000000000000006e6574207469746c650000001c00dc0101005000f3030000 | |||
| 892 | + | 00000000000000000000000000000000000000000108dd0103000000ef030000 | |||
| 893 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 894 | + | 1c00dd0101005000f30300000000000000000000000000000000000000000000 | |||
| 895 | + | 0108de0103000000ef0300000000000009000000000000000000000000000000 | |||
| 896 | + | 6e6574207469746c650000001c00de0101005000f30300000000000000000000 | |||
| 897 | + | 0000000000000000000000000108df0103000000ef0300000000000009000000 | |||
| 898 | + | 0000000000000000000000006e6574207469746c650000001c00df0101005000 | |||
| 899 | + | f303000000000000000000000000000000000000000000000108e00103000000 | |||
| 900 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 901 | + | 650000001c00e00101005000f303000000000000000000000000000000000000 | |||
| 902 | + | 000000000108e10103000000ef03000000000000090000000000000000000000 | |||
| 903 | + | 000000006e6574207469746c650000001c00e10101005000f303000000000000 | |||
| 904 | + | 000000000000000000000000000000000108e20103000000ef03000000000000 | |||
| 905 | + | 090000000000000000000000000000006e6574207469746c650000001c00e201 | |||
| 906 | + | 01005000f303000000000000000000000000000000000000000000000108e301 | |||
| 907 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 908 | + | 7469746c650000001c00e30101005000f3030000000000000000000000000000 | |||
| 909 | + | 00000000000000000108e40103000000ef030000000000000900000000000000 | |||
| 910 | + | 00000000000000006e6574207469746c650000001c00e40101005000f3030000 | |||
| 911 | + | 00000000000000000000000000000000000000000108e50103000000ef030000 | |||
| 912 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 913 | + | 1c00e50101005000f30300000000000000000000000000000000000000000000 | |||
| 914 | + | 0108e60103000000ef0300000000000009000000000000000000000000000000 | |||
| 915 | + | 6e6574207469746c650000001c00e60101005000f30300000000000000000000 | |||
| 916 | + | 0000000000000000000000000108e70103000000ef0300000000000009000000 | |||
| 917 | + | 0000000000000000000000006e6574207469746c650000001c00e70101005000 | |||
| 918 | + | f303000000000000000000000000000000000000000000000108e80103000000 | |||
| 919 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 920 | + | 650000001c00e80101005000f303000000000000000000000000000000000000 | |||
| 921 | + | 000000000108e90103000000ef03000000000000090000000000000000000000 | |||
| 922 | + | 000000006e6574207469746c650000001c00e90101005000f303000000000000 | |||
| 923 | + | 000000000000000000000000000000000108ea0103000000ef03000000000000 | |||
| 924 | + | 090000000000000000000000000000006e6574207469746c650000001c00ea01 | |||
| 925 | + | 01005000f303000000000000000000000000000000000000000000000108eb01 | |||
| 926 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 927 | + | 7469746c650000001c00eb0101005000f3030000000000000000000000000000 | |||
| 928 | + | 00000000000000000108ec0103000000ef030000000000000900000000000000 | |||
| 929 | + | 00000000000000006e6574207469746c650000001c00ec0101005000f3030000 | |||
| 930 | + | 00000000000000000000000000000000000000000108ed0103000000ef030000 | |||
| 931 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 932 | + | 1c00ed0101005000f30300000000000000000000000000000000000000000000 | |||
| 933 | + | 0108ee0103000000ef0300000000000009000000000000000000000000000000 | |||
| 934 | + | 6e6574207469746c650000001c00ee0101005000f30300000000000000000000 | |||
| 935 | + | 0000000000000000000000000108ef0103000000ef0300000000000009000000 | |||
| 936 | + | 0000000000000000000000006e6574207469746c650000001c00ef0101005000 | |||
| 937 | + | f303000000000000000000000000000000000000000000000108f00103000000 | |||
| 938 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 939 | + | 650000001c00f00101005000f303000000000000000000000000000000000000 | |||
| 940 | + | 000000000108f10103000000ef03000000000000090000000000000000000000 | |||
| 941 | + | 000000006e6574207469746c650000001c00f10101005000f303000000000000 | |||
| 942 | + | 000000000000000000000000000000000108f20103000000ef03000000000000 | |||
| 943 | + | 090000000000000000000000000000006e6574207469746c650000001c00f201 | |||
| 944 | + | 01005000f303000000000000000000000000000000000000000000000108f301 | |||
| 945 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 946 | + | 7469746c650000001c00f30101005000f3030000000000000000000000000000 | |||
| 947 | + | 00000000000000000108f40103000000ef030000000000000900000000000000 | |||
| 948 | + | 00000000000000006e6574207469746c650000001c00f40101005000f3030000 | |||
| 949 | + | 00000000000000000000000000000000000000000108f50103000000ef030000 | |||
| 950 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
A internal/adapters/wayland/xwm/testdata/streams/steady.hex +2150 -0 large diff (2150 lines changed): collapsed
A internal/adapters/wayland/xwm/unmanaged.go +109 -0
| @@ -0,0 +1,109 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bnema.dev/xturbo" | |||
| 5 | + | "bnema.dev/xturbo/xproto" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // Override-redirect windows are tracked, never managed: the XWM reads their | |||
| 9 | + | // window type once at map, pairs them with their wl_surface and reports the | |||
| 10 | + | // mapped rect to the host in layout coordinates. | |||
| 11 | + | ||||
| 12 | + | // maxTypeWords bounds the _NET_WM_WINDOW_TYPE read. | |||
| 13 | + | const maxTypeWords = 32 | |||
| 14 | + | ||||
| 15 | + | // requestOverrideType asks for the window type of a freshly mapped override | |||
| 16 | + | // window; the reply completes its readiness. | |||
| 17 | + | func (x *XWM) requestOverrideType(s Slot) { | |||
| 18 | + | rec := x.tab.rec(s) | |||
| 19 | + | seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid), | |||
| 20 | + | xproto.Atom(x.atoms[atomNetWMWindowType]), xproto.AtomAny, 0, maxTypeWords) | |||
| 21 | + | x.expect(seq, kindOverrideType, x.tab.tag(s), 0) | |||
| 22 | + | x.wake() | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | func (x *XWM) onOverrideType(s Slot, rec *record, r *xturbo.Reply) { | |||
| 26 | + | rec.noInput = false | |||
| 27 | + | if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok && rep.Format() == 32 { | |||
| 28 | + | val := rep.Value() | |||
| 29 | + | for i := 0; i+4 <= len(val); i += 4 { | |||
| 30 | + | switch x.windowType(word(val, i/4)) { | |||
| 31 | + | case atomNetWMWindowTypeTooltip, atomNetWMWindowTypeNotification, atomNetWMWindowTypeDND: | |||
| 32 | + | rec.noInput = true | |||
| 33 | + | } | |||
| 34 | + | } | |||
| 35 | + | } | |||
| 36 | + | x.overrideReady(s) | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // overrideReady marks a mapped override window typed and pairs it. | |||
| 40 | + | func (x *XWM) overrideReady(s Slot) { | |||
| 41 | + | rec := x.tab.rec(s) | |||
| 42 | + | if rec == nil || rec.kind != kindUnmanaged || !rec.mapped { | |||
| 43 | + | return | |||
| 44 | + | } | |||
| 45 | + | rec.ready = true | |||
| 46 | + | x.tryPair(s) | |||
| 47 | + | x.overrideMapped(s, rec) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // overrideMapped reports the window once it is mapped, typed and paired. | |||
| 51 | + | func (x *XWM) overrideMapped(s Slot, rec *record) { | |||
| 52 | + | if x.host == nil || rec.ovPaired || !rec.mapped || !rec.ready || rec.surface == 0 { | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | rec.ovPaired = true | |||
| 56 | + | rec.ov = x.layoutRect(rec) | |||
| 57 | + | o := rec.ov | |||
| 58 | + | x.host.OverrideMapped(s, rec.xid, rec.surface, int(o.x), int(o.y), int(o.w), int(o.h), rec.noInput) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // overrideUnmapped ends the mapped life of an override window; a remap reads | |||
| 62 | + | // the type again and pairs with a new surface. | |||
| 63 | + | func (x *XWM) overrideUnmapped(s Slot, rec *record) { | |||
| 64 | + | rec.ready, rec.serial, rec.surface, rec.noInput = false, 0, 0, false | |||
| 65 | + | rec.gen++ // a type reply still in flight names a dead tag | |||
| 66 | + | if rec.gen == 0 { | |||
| 67 | + | rec.gen = 1 | |||
| 68 | + | } | |||
| 69 | + | if rec.ovPaired { | |||
| 70 | + | rec.ovPaired = false | |||
| 71 | + | x.host.OverrideGone(s) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (x *XWM) layoutRect(rec *record) ovRect { | |||
| 76 | + | lx, ly := x.space.ToLayout(int(rec.x), int(rec.y)) | |||
| 77 | + | return ovRect{int32(lx), int32(ly), int32(rec.w), int32(rec.h)} | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // queueOverride schedules a geometry report for s; flushOverrides sends it | |||
| 81 | + | // once per iteration, only if the layout rect changed. | |||
| 82 | + | func (x *XWM) queueOverride(s Slot, rec *record) { | |||
| 83 | + | if rec.ovQueued || s == 0 { | |||
| 84 | + | return | |||
| 85 | + | } | |||
| 86 | + | if len(x.ovDirty) == cap(x.ovDirty) { | |||
| 87 | + | x.flushOverrides() | |||
| 88 | + | } | |||
| 89 | + | rec.ovQueued = true | |||
| 90 | + | x.ovDirty = append(x.ovDirty, s) | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | func (x *XWM) flushOverrides() { | |||
| 94 | + | for _, s := range x.ovDirty { | |||
| 95 | + | rec := x.tab.rec(s) | |||
| 96 | + | if rec == nil || rec.kind != kindUnmanaged || !rec.ovQueued { | |||
| 97 | + | continue | |||
| 98 | + | } | |||
| 99 | + | rec.ovQueued = false | |||
| 100 | + | if !rec.ovPaired { | |||
| 101 | + | continue | |||
| 102 | + | } | |||
| 103 | + | if o := x.layoutRect(rec); o != rec.ov { | |||
| 104 | + | rec.ov = o | |||
| 105 | + | x.host.OverrideMoved(s, int(o.x), int(o.y), int(o.w), int(o.h)) | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | x.ovDirty = x.ovDirty[:0] | |||
| 109 | + | } | |||
A internal/adapters/wayland/xwm/unmanaged_test.go +247 -0
| @@ -0,0 +1,247 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "bnema.dev/xturbo" | |||
| 8 | + | "bnema.dev/xturbo/xproto" | |||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | + | "github.com/stretchr/testify/require" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func layoutOf(os ...ports.OutputPlacement) ports.Layout { return ports.Layout(os) } | |||
| 14 | + | ||||
| 15 | + | func rootConfigureNotify(win uint32, x, y int16, w, h uint16) xturbo.Event { | |||
| 16 | + | ev := xturbo.Event{Kind: xproto.KindConfigureNotify} | |||
| 17 | + | binary.LittleEndian.PutUint32(ev.Raw[4:], peerRoot) | |||
| 18 | + | binary.LittleEndian.PutUint32(ev.Raw[8:], win) | |||
| 19 | + | binary.LittleEndian.PutUint16(ev.Raw[16:], uint16(x)) | |||
| 20 | + | binary.LittleEndian.PutUint16(ev.Raw[18:], uint16(y)) | |||
| 21 | + | binary.LittleEndian.PutUint16(ev.Raw[20:], w) | |||
| 22 | + | binary.LittleEndian.PutUint16(ev.Raw[22:], h) | |||
| 23 | + | return ev | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func (p *xpeer) rootConfigureNotify(win uint32, x, y int16, w, h uint16) { | |||
| 27 | + | p.event(22, func(b *[32]byte) { | |||
| 28 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 29 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 30 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |||
| 31 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |||
| 32 | + | binary.LittleEndian.PutUint16(b[20:], w) | |||
| 33 | + | binary.LittleEndian.PutUint16(b[22:], h) | |||
| 34 | + | }) | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func TestPlaceConvertsLayoutToXCoordinates(t *testing.T) { | |||
| 38 | + | x, p, h, _, _ := attached(t) | |||
| 39 | + | s := newWin(t, x, p, h, 0x500001) | |||
| 40 | + | x.SetLayout(layoutOf(out(-640, 0, 640, 480), out(0, 0, 1920, 1080))) | |||
| 41 | + | n := settle(t, x, p) | |||
| 42 | + | ||||
| 43 | + | x.Place(s, -640+10, 20) | |||
| 44 | + | settle(t, x, p) | |||
| 45 | + | cw := since(p, n, opConfigureWindow) | |||
| 46 | + | require.Len(t, cw, 1) | |||
| 47 | + | require.EqualValues(t, 10, cw[0].u32(12)) | |||
| 48 | + | require.EqualValues(t, 20, cw[0].u32(16)) | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | func TestSetLayoutRepositionsPlacedWindows(t *testing.T) { | |||
| 52 | + | x, p, h, _, _ := attached(t) | |||
| 53 | + | s := newWin(t, x, p, h, 0x500001) | |||
| 54 | + | x.SetLayout(layoutOf(out(0, 0, 1920, 1080))) | |||
| 55 | + | x.Place(s, 100, 50) | |||
| 56 | + | n := settle(t, x, p) | |||
| 57 | + | ||||
| 58 | + | x.SetLayout(layoutOf(out(0, 0, 1920, 1080))) // unchanged: nothing resent | |||
| 59 | + | settle(t, x, p) | |||
| 60 | + | require.Empty(t, since(p, n, opConfigureWindow)) | |||
| 61 | + | ||||
| 62 | + | x.SetLayout(layoutOf(out(-100, 0, 100, 100), out(0, 0, 1920, 1080))) | |||
| 63 | + | settle(t, x, p) | |||
| 64 | + | cw := since(p, n, opConfigureWindow) | |||
| 65 | + | require.Len(t, cw, 1) | |||
| 66 | + | require.EqualValues(t, 200, cw[0].u32(12)) | |||
| 67 | + | require.EqualValues(t, 50, cw[0].u32(16)) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func TestSetLayoutWarnsOnceWhenOutputsDropped(t *testing.T) { | |||
| 71 | + | x, _, _, _, _ := attached(t) | |||
| 72 | + | var l ports.Layout | |||
| 73 | + | for i := 0; i < maxRegions+2; i++ { | |||
| 74 | + | l = append(l, out(i*100, 0, 100, 100)) | |||
| 75 | + | } | |||
| 76 | + | x.SetLayout(l) | |||
| 77 | + | require.True(t, x.warnedSpace) | |||
| 78 | + | require.Equal(t, 2, x.space.Dropped()) | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // overrideWindow creates and maps an override window, answers its type read | |||
| 82 | + | // and pairs it; the mocked host must expect OverrideMapped beforehand. | |||
| 83 | + | func overrideWindow(t *testing.T, x *XWM, p *xpeer, win uint32, typ string) Slot { | |||
| 84 | + | t.Helper() | |||
| 85 | + | if typ != "" { | |||
| 86 | + | p.setProp(win, p.atom("_NET_WM_WINDOW_TYPE"), uint32(xproto.AtomAtom), 32, le32(p.atom(typ))) | |||
| 87 | + | } | |||
| 88 | + | p.createNotify(win, true) | |||
| 89 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 }) | |||
| 90 | + | s := x.tab.lookup(win) | |||
| 91 | + | x.feed(t, p, func() { p.rootConfigureNotify(win, 30, 40, 100, 50) }) | |||
| 92 | + | x.feed(t, p, func() { p.mapNotify(win) }) | |||
| 93 | + | return s | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func TestUnmanagedMapReadsTypeThenPairsThenReports(t *testing.T) { | |||
| 97 | + | x, p, h, _, _ := attached(t) | |||
| 98 | + | x.SetLayout(layoutOf(out(-640, 0, 640, 480), out(0, 0, 1920, 1080))) | |||
| 99 | + | const win = 0x500009 | |||
| 100 | + | s := overrideWindow(t, x, p, win, "_NET_WM_WINDOW_TYPE_TOOLTIP") | |||
| 101 | + | x.flush() | |||
| 102 | + | readUntil(t, x, func() bool { return x.tab.rec(s).ready }) | |||
| 103 | + | typeReads := 0 | |||
| 104 | + | for _, q := range p.requests() { | |||
| 105 | + | if q.major == opGetProperty && q.u32(4) == win { | |||
| 106 | + | typeReads++ | |||
| 107 | + | } | |||
| 108 | + | } | |||
| 109 | + | require.Equal(t, 1, typeReads) | |||
| 110 | + | ||||
| 111 | + | h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), -640+30, 40, 100, 50, true).Once() | |||
| 112 | + | x.SetSerial(7, 9) | |||
| 113 | + | x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) }) | |||
| 114 | + | require.True(t, x.tab.rec(s).ovPaired) | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func TestUnmanagedDefaultTypeTakesInput(t *testing.T) { | |||
| 118 | + | x, p, h, _, _ := attached(t) | |||
| 119 | + | x.SetLayout(layoutOf(out(0, 0, 1920, 1080))) | |||
| 120 | + | const win = 0x500009 | |||
| 121 | + | s := overrideWindow(t, x, p, win, "_NET_WM_WINDOW_TYPE_POPUP_MENU") | |||
| 122 | + | x.SetSerial(7, 9) | |||
| 123 | + | h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once() | |||
| 124 | + | x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) }) | |||
| 125 | + | readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired }) | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | func TestUnmanagedMoveAndLayoutChangeAndUnmap(t *testing.T) { | |||
| 129 | + | x, p, h, _, _ := attached(t) | |||
| 130 | + | x.SetLayout(layoutOf(out(0, 0, 1920, 1080))) | |||
| 131 | + | const win = 0x500009 | |||
| 132 | + | s := overrideWindow(t, x, p, win, "") | |||
| 133 | + | x.SetSerial(7, 9) | |||
| 134 | + | h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once() | |||
| 135 | + | x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) }) | |||
| 136 | + | readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired }) | |||
| 137 | + | ||||
| 138 | + | h.EXPECT().OverrideMoved(s, 35, 45, 100, 60).Once() | |||
| 139 | + | x.feed(t, p, func() { p.rootConfigureNotify(win, 35, 45, 100, 60) }) | |||
| 140 | + | x.flush() | |||
| 141 | + | x.feed(t, p, func() { p.rootConfigureNotify(win, 35, 45, 100, 60) }) // same rect: no call | |||
| 142 | + | x.flush() | |||
| 143 | + | ||||
| 144 | + | h.EXPECT().OverrideMoved(s, -65, 45, 100, 60).Once() // the layout origin moved: same X point, new layout point | |||
| 145 | + | x.SetLayout(layoutOf(out(-100, 0, 100, 100), out(0, 0, 1920, 1080))) | |||
| 146 | + | x.flush() | |||
| 147 | + | ||||
| 148 | + | h.EXPECT().OverrideGone(s).Once() | |||
| 149 | + | x.feed(t, p, func() { p.unmapNotify(win, false) }) | |||
| 150 | + | require.False(t, x.tab.rec(s).ovPaired) | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | func TestUnmanagedDestroyReportsGone(t *testing.T) { | |||
| 154 | + | x, p, h, _, _ := attached(t) | |||
| 155 | + | const win = 0x500009 | |||
| 156 | + | s := overrideWindow(t, x, p, win, "") | |||
| 157 | + | x.SetSerial(7, 9) | |||
| 158 | + | h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once() | |||
| 159 | + | x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) }) | |||
| 160 | + | readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired }) | |||
| 161 | + | h.EXPECT().OverrideGone(s).Once() | |||
| 162 | + | x.feed(t, p, func() { p.destroyNotify(win) }) | |||
| 163 | + | require.Zero(t, x.tab.lookup(win)) | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | func TestFocusInIgnoresUnmanagedAndGrabModes(t *testing.T) { | |||
| 167 | + | x, p, h, _, _ := attached(t) | |||
| 168 | + | sa := newWin(t, x, p, h, 0x500001) | |||
| 169 | + | newWin(t, x, p, h, 0x500002) | |||
| 170 | + | x.Focus(sa) | |||
| 171 | + | p.createNotify(0x500009, true) | |||
| 172 | + | readUntil(t, x, func() bool { return x.tab.lookup(0x500009) != 0 }) | |||
| 173 | + | n := settle(t, x, p) | |||
| 174 | + | ||||
| 175 | + | focusIn := func(win uint32, mode byte) { | |||
| 176 | + | p.event(9, func(e *[32]byte) { | |||
| 177 | + | binary.LittleEndian.PutUint32(e[4:], win) | |||
| 178 | + | e[8] = mode | |||
| 179 | + | }) | |||
| 180 | + | } | |||
| 181 | + | x.feed(t, p, func() { focusIn(0x500009, 0) }) | |||
| 182 | + | x.feed(t, p, func() { focusIn(0x500002, byte(xproto.NotifyModeGrab)) }) | |||
| 183 | + | x.feed(t, p, func() { focusIn(0x500002, byte(xproto.NotifyModeUngrab)) }) | |||
| 184 | + | require.False(t, x.refocus) | |||
| 185 | + | settle(t, x, p) | |||
| 186 | + | require.Empty(t, since(p, n, opSetInputFocus)) | |||
| 187 | + | ||||
| 188 | + | x.feed(t, p, func() { focusIn(0x500002, 0) }) | |||
| 189 | + | settle(t, x, p) | |||
| 190 | + | require.Len(t, since(p, n, opSetInputFocus), 1, "a normal FocusIn on another managed window is still denied") | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | func TestWMChangeStateIgnored(t *testing.T) { | |||
| 194 | + | x, p, h, _, _ := attached(t) | |||
| 195 | + | newWin(t, x, p, h, 0x500001) | |||
| 196 | + | n := settle(t, x, p) | |||
| 197 | + | x.feed(t, p, func() { p.clientMessage(0x500001, p.atom("WM_CHANGE_STATE"), 3) }) | |||
| 198 | + | settle(t, x, p) | |||
| 199 | + | require.Empty(t, since(p, n, opConfigureWindow)) // the mocked host sees no call either | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | func TestUnmanagedGeometryAllocations(t *testing.T) { | |||
| 203 | + | x, p, _, _, _ := attached(t) | |||
| 204 | + | x.SetLayout(layoutOf(out(0, 0, 1920, 1080))) | |||
| 205 | + | s := x.insert(0x500009) | |||
| 206 | + | r := x.tab.rec(s) | |||
| 207 | + | r.kind, r.mapped, r.ready, r.ovPaired = kindUnmanaged, true, true, true | |||
| 208 | + | r.x, r.y, r.w, r.h = 30, 40, 100, 50 | |||
| 209 | + | r.ov = x.layoutRect(r) | |||
| 210 | + | sinkPeer(t, x, p) | |||
| 211 | + | a := rootConfigureNotify(0x500009, 31, 41, 100, 50) | |||
| 212 | + | b := rootConfigureNotify(0x500009, 30, 40, 100, 50) // back to the reported rect | |||
| 213 | + | allocs := testing.AllocsPerRun(1000, func() { | |||
| 214 | + | x.onEvent(&a) | |||
| 215 | + | x.onEvent(&b) | |||
| 216 | + | x.flush() | |||
| 217 | + | }) | |||
| 218 | + | require.Zero(t, allocs) | |||
| 219 | + | require.True(t, r.ovPaired) | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | func TestFocusInUnmanagedAllocations(t *testing.T) { | |||
| 223 | + | x, p, _, _, _ := attached(t) | |||
| 224 | + | s := x.insert(0x500009) | |||
| 225 | + | x.tab.rec(s).kind = kindUnmanaged | |||
| 226 | + | sinkPeer(t, x, p) | |||
| 227 | + | ev := xturbo.Event{Kind: xproto.KindFocusIn} | |||
| 228 | + | binary.LittleEndian.PutUint32(ev.Raw[4:], 0x500009) | |||
| 229 | + | grab := ev | |||
| 230 | + | grab.Raw[8] = byte(xproto.NotifyModeGrab) | |||
| 231 | + | allocs := testing.AllocsPerRun(1000, func() { | |||
| 232 | + | x.onEvent(&ev) | |||
| 233 | + | x.onEvent(&grab) | |||
| 234 | + | x.flush() | |||
| 235 | + | }) | |||
| 236 | + | require.Zero(t, allocs) | |||
| 237 | + | require.False(t, x.refocus) | |||
| 238 | + | } | |||
| 239 | + | ||||
| 240 | + | func TestDropWindowClearsQueuedOverride(t *testing.T) { | |||
| 241 | + | x, _, _, _, _ := attached(t) | |||
| 242 | + | s := x.insert(0x500009) | |||
| 243 | + | r := x.tab.rec(s) | |||
| 244 | + | r.kind, r.ovQueued = kindUnmanaged, true | |||
| 245 | + | x.dropWindow(s) | |||
| 246 | + | require.False(t, r.ovQueued) | |||
| 247 | + | } | |||
A internal/adapters/wayland/xwm/xpeer_test.go +594 -0
| @@ -0,0 +1,594 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "io" | |||
| 6 | + | "os" | |||
| 7 | + | "sync" | |||
| 8 | + | "sync/atomic" | |||
| 9 | + | "testing" | |||
| 10 | + | "time" | |||
| 11 | + | ||||
| 12 | + | "golang.org/x/sys/unix" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // Opcodes the peer understands. | |||
| 16 | + | const ( | |||
| 17 | + | opCreateWindow = 1 | |||
| 18 | + | opChangeWindowAttributes = 2 | |||
| 19 | + | opGetWindowAttributes = 3 | |||
| 20 | + | opMapWindow = 8 | |||
| 21 | + | opDeleteProperty = 19 | |||
| 22 | + | opConfigureWindow = 12 | |||
| 23 | + | opGetGeometry = 14 | |||
| 24 | + | opQueryTree = 15 | |||
| 25 | + | opInternAtom = 16 | |||
| 26 | + | opChangeProperty = 18 | |||
| 27 | + | opGetProperty = 20 | |||
| 28 | + | opSetSelectionOwner = 22 | |||
| 29 | + | opSendEvent = 25 | |||
| 30 | + | opSetInputFocus = 42 | |||
| 31 | + | opGetInputFocus = 43 | |||
| 32 | + | opQueryExtension = 98 | |||
| 33 | + | opKillClient = 113 | |||
| 34 | + | ) | |||
| 35 | + | ||||
| 36 | + | const ( | |||
| 37 | + | peerRoot = 0x100 | |||
| 38 | + | peerXIDBase = 0x200000 | |||
| 39 | + | peerXIDMask = 0x1fffff | |||
| 40 | + | peerMaxWords = 0x100000 | |||
| 41 | + | ) | |||
| 42 | + | ||||
| 43 | + | type xpeerExt struct{ major, firstEvent, firstError uint8 } | |||
| 44 | + | ||||
| 45 | + | // xpeerReq is one request the peer received. | |||
| 46 | + | type xpeerReq struct { | |||
| 47 | + | seq uint16 | |||
| 48 | + | major uint8 | |||
| 49 | + | minor uint8 | |||
| 50 | + | raw []byte // whole request, header included | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | func (r xpeerReq) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) } | |||
| 54 | + | ||||
| 55 | + | type xpeerProp struct { | |||
| 56 | + | typ uint32 | |||
| 57 | + | format uint8 | |||
| 58 | + | data []byte | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | type xpeerGeom struct { | |||
| 62 | + | x, y int16 | |||
| 63 | + | w, h, border uint16 | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // xpeer is a scripted X server speaking the real wire protocol over a | |||
| 67 | + | // socketpair: setup, extension queries, atoms, properties, geometry, window | |||
| 68 | + | // tree. It records every request. It is a protocol peer, not a test double of | |||
| 69 | + | // a project interface. | |||
| 70 | + | type xpeer struct { | |||
| 71 | + | t *testing.T | |||
| 72 | + | f *os.File | |||
| 73 | + | ||||
| 74 | + | mu sync.Mutex | |||
| 75 | + | seq uint16 | |||
| 76 | + | reqs []xpeerReq | |||
| 77 | + | exts map[string]xpeerExt | |||
| 78 | + | atoms map[string]uint32 | |||
| 79 | + | nextAtom uint32 | |||
| 80 | + | props map[uint32]map[uint32]xpeerProp | |||
| 81 | + | geoms map[uint32]xpeerGeom | |||
| 82 | + | attrs map[uint32][2]byte // window -> {overrideRedirect, mapState} | |||
| 83 | + | children []uint32 | |||
| 84 | + | denyRoot bool // answer the root event-mask request with BadAccess | |||
| 85 | + | pids map[uint32]uint32 // XRes QueryClientIds answers, by window | |||
| 86 | + | bad map[uint32]bool // windows whose GetProperty/GetGeometry fail with BadWindow | |||
| 87 | + | hold bool // keep replies until release | |||
| 88 | + | held [][]byte | |||
| 89 | + | tap []byte // everything written to the client, when recording | |||
| 90 | + | record bool | |||
| 91 | + | ||||
| 92 | + | gate atomic.Pointer[chan struct{}] // closed = reading allowed | |||
| 93 | + | sbuf [4096]byte | |||
| 94 | + | sink atomic.Bool // discard requests without recording or allocating | |||
| 95 | + | done chan struct{} | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | func fullPeerExts() map[string]xpeerExt { | |||
| 99 | + | return map[string]xpeerExt{ | |||
| 100 | + | "BIG-REQUESTS": {133, 0, 0}, | |||
| 101 | + | "XC-MISC": {136, 0, 0}, | |||
| 102 | + | "Composite": {142, 0, 0}, | |||
| 103 | + | "XFIXES": {138, 87, 140}, | |||
| 104 | + | "X-Resource": {144, 0, 0}, | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // startXPeer returns the client end of a socketpair, to give to Boot, and the | |||
| 109 | + | // peer serving the other end. | |||
| 110 | + | func startXPeer(t *testing.T, exts map[string]xpeerExt) (int, *xpeer) { | |||
| 111 | + | t.Helper() | |||
| 112 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 113 | + | if err != nil { | |||
| 114 | + | t.Fatal(err) | |||
| 115 | + | } | |||
| 116 | + | p := &xpeer{ | |||
| 117 | + | t: t, f: os.NewFile(uintptr(fds[1]), "xpeer"), | |||
| 118 | + | exts: exts, atoms: map[string]uint32{}, nextAtom: 1000, | |||
| 119 | + | props: map[uint32]map[uint32]xpeerProp{}, geoms: map[uint32]xpeerGeom{}, attrs: map[uint32][2]byte{}, | |||
| 120 | + | pids: map[uint32]uint32{}, bad: map[uint32]bool{}, | |||
| 121 | + | done: make(chan struct{}), | |||
| 122 | + | } | |||
| 123 | + | open := make(chan struct{}) | |||
| 124 | + | close(open) | |||
| 125 | + | p.gate.Store(&open) | |||
| 126 | + | t.Cleanup(func() { | |||
| 127 | + | p.resume() // a paused peer would never see the close | |||
| 128 | + | p.f.Close() | |||
| 129 | + | <-p.done | |||
| 130 | + | }) | |||
| 131 | + | go p.serve() | |||
| 132 | + | return fds[0], p | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | func (p *xpeer) setupReply() []byte { | |||
| 136 | + | vendor := []byte("xpeer\x00\x00\x00") | |||
| 137 | + | b := make([]byte, 40+len(vendor)+40) | |||
| 138 | + | b[0], b[2] = 1, 11 | |||
| 139 | + | binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4)) | |||
| 140 | + | binary.LittleEndian.PutUint32(b[12:], peerXIDBase) | |||
| 141 | + | binary.LittleEndian.PutUint32(b[16:], peerXIDMask) | |||
| 142 | + | binary.LittleEndian.PutUint16(b[24:], 5) | |||
| 143 | + | b[28] = 1 | |||
| 144 | + | copy(b[40:], vendor) | |||
| 145 | + | scr := b[40+len(vendor):] | |||
| 146 | + | binary.LittleEndian.PutUint32(scr, peerRoot) | |||
| 147 | + | binary.LittleEndian.PutUint32(scr[32:], 0x22) // root visual | |||
| 148 | + | scr[38] = 24 // root depth | |||
| 149 | + | return b | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | func (p *xpeer) serve() { | |||
| 153 | + | defer close(p.done) | |||
| 154 | + | var hdr [12]byte | |||
| 155 | + | if _, err := io.ReadFull(p.f, hdr[:]); err != nil { | |||
| 156 | + | return | |||
| 157 | + | } | |||
| 158 | + | // Auth name and data are empty (NewConn sends none). | |||
| 159 | + | if _, err := p.f.Write(p.setupReply()); err != nil { | |||
| 160 | + | return | |||
| 161 | + | } | |||
| 162 | + | for { | |||
| 163 | + | <-*p.gate.Load() | |||
| 164 | + | if p.sink.Load() { | |||
| 165 | + | if !p.discard() { | |||
| 166 | + | return | |||
| 167 | + | } | |||
| 168 | + | continue | |||
| 169 | + | } | |||
| 170 | + | var h [4]byte | |||
| 171 | + | if _, err := io.ReadFull(p.f, h[:]); err != nil { | |||
| 172 | + | return | |||
| 173 | + | } | |||
| 174 | + | raw := h[:] | |||
| 175 | + | n := int(binary.LittleEndian.Uint16(h[2:])) * 4 | |||
| 176 | + | if n == 0 { // BIG-REQUESTS header | |||
| 177 | + | var l [4]byte | |||
| 178 | + | if _, err := io.ReadFull(p.f, l[:]); err != nil { | |||
| 179 | + | return | |||
| 180 | + | } | |||
| 181 | + | raw = append(raw, l[:]...) | |||
| 182 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |||
| 183 | + | } | |||
| 184 | + | rest := make([]byte, n-len(raw)) | |||
| 185 | + | if _, err := io.ReadFull(p.f, rest); err != nil { | |||
| 186 | + | return | |||
| 187 | + | } | |||
| 188 | + | raw = append(append([]byte(nil), raw...), rest...) | |||
| 189 | + | p.mu.Lock() | |||
| 190 | + | p.seq++ | |||
| 191 | + | r := xpeerReq{p.seq, h[0], h[1], raw} | |||
| 192 | + | p.reqs = append(p.reqs, r) | |||
| 193 | + | p.answer(r, raw[len(raw)-len(rest):]) | |||
| 194 | + | p.mu.Unlock() | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | // discard reads and drops one request without allocating. It reports false | |||
| 199 | + | // when the connection ended. | |||
| 200 | + | func (p *xpeer) discard() bool { | |||
| 201 | + | if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil { | |||
| 202 | + | return false | |||
| 203 | + | } | |||
| 204 | + | n := int(binary.LittleEndian.Uint16(p.sbuf[2:])) * 4 | |||
| 205 | + | left := n - 4 | |||
| 206 | + | if n == 0 { | |||
| 207 | + | if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil { | |||
| 208 | + | return false | |||
| 209 | + | } | |||
| 210 | + | left = int(binary.LittleEndian.Uint32(p.sbuf[:4]))*4 - 8 | |||
| 211 | + | } | |||
| 212 | + | for left > 0 { | |||
| 213 | + | m, err := p.f.Read(p.sbuf[:min(left, len(p.sbuf))]) | |||
| 214 | + | if err != nil { | |||
| 215 | + | return false | |||
| 216 | + | } | |||
| 217 | + | left -= m | |||
| 218 | + | } | |||
| 219 | + | return true | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | // write sends bytes to the client. The caller holds p.mu. | |||
| 223 | + | func (p *xpeer) write(b []byte) { | |||
| 224 | + | if p.record { | |||
| 225 | + | p.tap = append(p.tap, b...) | |||
| 226 | + | } | |||
| 227 | + | _, _ = p.f.Write(b) | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | func (p *xpeer) reply(data uint8, body []byte) { | |||
| 231 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |||
| 232 | + | b := make([]byte, 32+4*words) | |||
| 233 | + | b[0], b[1] = 1, data | |||
| 234 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 235 | + | binary.LittleEndian.PutUint32(b[4:], uint32(words)) | |||
| 236 | + | copy(b[8:], body) | |||
| 237 | + | if p.hold { | |||
| 238 | + | p.held = append(p.held, b) | |||
| 239 | + | return | |||
| 240 | + | } | |||
| 241 | + | p.write(b) | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | // release sends the held replies, in request order as a real server does. | |||
| 245 | + | func (p *xpeer) release() { | |||
| 246 | + | p.mu.Lock() | |||
| 247 | + | defer p.mu.Unlock() | |||
| 248 | + | p.hold = false | |||
| 249 | + | for _, b := range p.held { | |||
| 250 | + | p.write(b) | |||
| 251 | + | } | |||
| 252 | + | p.held = nil | |||
| 253 | + | } | |||
| 254 | + | ||||
| 255 | + | // holdReplies keeps the replies of the following requests back. | |||
| 256 | + | func (p *xpeer) holdReplies() { | |||
| 257 | + | p.mu.Lock() | |||
| 258 | + | p.hold = true | |||
| 259 | + | p.mu.Unlock() | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | func (p *xpeer) errorPacket(code, major uint8, bad uint32) { | |||
| 263 | + | b := make([]byte, 32) | |||
| 264 | + | b[1] = code | |||
| 265 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 266 | + | binary.LittleEndian.PutUint32(b[4:], bad) | |||
| 267 | + | b[10] = major | |||
| 268 | + | p.write(b) | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | func (p *xpeer) extName(major uint8) string { | |||
| 272 | + | for name, e := range p.exts { | |||
| 273 | + | if e.major == major { | |||
| 274 | + | return name | |||
| 275 | + | } | |||
| 276 | + | } | |||
| 277 | + | return "" | |||
| 278 | + | } | |||
| 279 | + | ||||
| 280 | + | func le32(v ...uint32) []byte { | |||
| 281 | + | b := make([]byte, 0, 4*len(v)) | |||
| 282 | + | for _, x := range v { | |||
| 283 | + | b = binary.LittleEndian.AppendUint32(b, x) | |||
| 284 | + | } | |||
| 285 | + | return b | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | // answer runs with p.mu held. | |||
| 289 | + | func (p *xpeer) answer(r xpeerReq, body []byte) { | |||
| 290 | + | switch { | |||
| 291 | + | case r.major == opQueryExtension: | |||
| 292 | + | e, ok := p.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |||
| 293 | + | var present uint8 | |||
| 294 | + | if ok { | |||
| 295 | + | present = 1 | |||
| 296 | + | } | |||
| 297 | + | p.reply(0, []byte{present, e.major, e.firstEvent, e.firstError}) | |||
| 298 | + | case r.major == opInternAtom: | |||
| 299 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |||
| 300 | + | a, ok := p.atoms[name] | |||
| 301 | + | if !ok { | |||
| 302 | + | p.nextAtom++ | |||
| 303 | + | a = p.nextAtom | |||
| 304 | + | p.atoms[name] = a | |||
| 305 | + | } | |||
| 306 | + | p.reply(0, le32(a)) | |||
| 307 | + | case r.major == opGetInputFocus: | |||
| 308 | + | p.reply(1, le32(0)) | |||
| 309 | + | case r.major == opQueryTree: | |||
| 310 | + | b := make([]byte, 24+4*len(p.children)) | |||
| 311 | + | binary.LittleEndian.PutUint32(b[0:], peerRoot) | |||
| 312 | + | binary.LittleEndian.PutUint16(b[8:], uint16(len(p.children))) | |||
| 313 | + | for i, c := range p.children { | |||
| 314 | + | binary.LittleEndian.PutUint32(b[24+4*i:], c) | |||
| 315 | + | } | |||
| 316 | + | p.reply(0, b) | |||
| 317 | + | case r.major == opGetWindowAttributes: | |||
| 318 | + | a := p.attrs[binary.LittleEndian.Uint32(body)] | |||
| 319 | + | b := make([]byte, 36) // reply length 3 | |||
| 320 | + | b[18] = a[1] // map_state is byte 26 of the reply | |||
| 321 | + | b[19] = a[0] // override_redirect, byte 27 | |||
| 322 | + | p.reply(0, b) | |||
| 323 | + | case r.major == opGetGeometry && p.bad[binary.LittleEndian.Uint32(body)]: | |||
| 324 | + | p.errorPacket(9, r.major, binary.LittleEndian.Uint32(body)) // BadDrawable | |||
| 325 | + | case r.major == opGetProperty && p.bad[binary.LittleEndian.Uint32(body)]: | |||
| 326 | + | p.errorPacket(3, r.major, binary.LittleEndian.Uint32(body)) // BadWindow | |||
| 327 | + | case r.major == opConfigureWindow: | |||
| 328 | + | g := p.geoms[binary.LittleEndian.Uint32(body)] | |||
| 329 | + | mask, vals := binary.LittleEndian.Uint16(body[4:]), body[8:] | |||
| 330 | + | for bit, dst := 0, []any{&g.x, &g.y, &g.w, &g.h, &g.border}; bit < 5; bit++ { | |||
| 331 | + | if mask&(1<<bit) == 0 { | |||
| 332 | + | continue | |||
| 333 | + | } | |||
| 334 | + | v := uint16(binary.LittleEndian.Uint32(vals)) | |||
| 335 | + | vals = vals[4:] | |||
| 336 | + | switch d := dst[bit].(type) { | |||
| 337 | + | case *int16: | |||
| 338 | + | *d = int16(v) | |||
| 339 | + | case *uint16: | |||
| 340 | + | *d = v | |||
| 341 | + | } | |||
| 342 | + | } | |||
| 343 | + | p.geoms[binary.LittleEndian.Uint32(body)] = g | |||
| 344 | + | case r.major == opGetGeometry: | |||
| 345 | + | g := p.geoms[binary.LittleEndian.Uint32(body)] | |||
| 346 | + | b := make([]byte, 24) | |||
| 347 | + | binary.LittleEndian.PutUint32(b[0:], peerRoot) | |||
| 348 | + | binary.LittleEndian.PutUint16(b[4:], uint16(g.x)) | |||
| 349 | + | binary.LittleEndian.PutUint16(b[6:], uint16(g.y)) | |||
| 350 | + | binary.LittleEndian.PutUint16(b[8:], g.w) | |||
| 351 | + | binary.LittleEndian.PutUint16(b[10:], g.h) | |||
| 352 | + | binary.LittleEndian.PutUint16(b[12:], g.border) | |||
| 353 | + | p.reply(24, b) | |||
| 354 | + | case r.major == opChangeProperty: | |||
| 355 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 356 | + | format, n := body[12], int(binary.LittleEndian.Uint32(body[16:])) | |||
| 357 | + | if p.props[win] == nil { | |||
| 358 | + | p.props[win] = map[uint32]xpeerProp{} | |||
| 359 | + | } | |||
| 360 | + | p.props[win][prop] = xpeerProp{ | |||
| 361 | + | typ: binary.LittleEndian.Uint32(body[8:]), format: format, | |||
| 362 | + | data: append([]byte(nil), body[20:20+n*int(format)/8]...), | |||
| 363 | + | } | |||
| 364 | + | case r.major == opDeleteProperty: | |||
| 365 | + | delete(p.props[binary.LittleEndian.Uint32(body)], binary.LittleEndian.Uint32(body[4:])) | |||
| 366 | + | case r.major == opGetProperty: | |||
| 367 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 368 | + | v, ok := p.props[win][prop] | |||
| 369 | + | b := make([]byte, 24+(len(v.data)+3)&^3) | |||
| 370 | + | if ok && v.format != 0 { | |||
| 371 | + | binary.LittleEndian.PutUint32(b[0:], v.typ) | |||
| 372 | + | binary.LittleEndian.PutUint32(b[8:], uint32(len(v.data)/(int(v.format)/8))) | |||
| 373 | + | copy(b[24:], v.data) | |||
| 374 | + | } | |||
| 375 | + | p.reply(v.format, b) | |||
| 376 | + | case r.major == opChangeWindowAttributes: | |||
| 377 | + | const cwEventMask = 1 << 11 | |||
| 378 | + | if p.denyRoot && binary.LittleEndian.Uint32(body) == peerRoot && binary.LittleEndian.Uint32(body[4:])&cwEventMask != 0 { | |||
| 379 | + | p.errorPacket(10, opChangeWindowAttributes, peerRoot) // BadAccess | |||
| 380 | + | } | |||
| 381 | + | case r.major >= 128: | |||
| 382 | + | switch p.extName(r.major) { | |||
| 383 | + | case "BIG-REQUESTS": | |||
| 384 | + | p.reply(0, le32(peerMaxWords)) | |||
| 385 | + | case "XC-MISC": | |||
| 386 | + | if r.minor == 1 { | |||
| 387 | + | p.reply(0, le32(0, 1)) | |||
| 388 | + | } | |||
| 389 | + | case "Composite", "XFIXES": | |||
| 390 | + | if r.minor == 0 { | |||
| 391 | + | p.reply(0, le32(5, 4)) | |||
| 392 | + | } | |||
| 393 | + | case "X-Resource": | |||
| 394 | + | switch r.minor { | |||
| 395 | + | case 0: | |||
| 396 | + | p.reply(0, []byte{1, 0, 2, 0}) | |||
| 397 | + | case 4: // QueryClientIds: one spec, answered when a pid is scripted | |||
| 398 | + | pid, ok := p.pids[r.u32(8)] | |||
| 399 | + | if !ok { | |||
| 400 | + | p.reply(0, le32(0)) | |||
| 401 | + | break | |||
| 402 | + | } | |||
| 403 | + | b := make([]byte, 40) | |||
| 404 | + | binary.LittleEndian.PutUint32(b[0:], 1) | |||
| 405 | + | binary.LittleEndian.PutUint32(b[24:], r.u32(8)) | |||
| 406 | + | binary.LittleEndian.PutUint32(b[28:], 2) // LocalClientPID | |||
| 407 | + | binary.LittleEndian.PutUint32(b[32:], 4) | |||
| 408 | + | binary.LittleEndian.PutUint32(b[36:], pid) | |||
| 409 | + | p.reply(0, b) | |||
| 410 | + | } | |||
| 411 | + | } | |||
| 412 | + | } | |||
| 413 | + | } | |||
| 414 | + | ||||
| 415 | + | // requests returns a snapshot of every request received after setup. | |||
| 416 | + | func (p *xpeer) requests() []xpeerReq { | |||
| 417 | + | p.mu.Lock() | |||
| 418 | + | defer p.mu.Unlock() | |||
| 419 | + | return append([]xpeerReq(nil), p.reqs...) | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | // waitReq waits for a request satisfying match among those after the first | |||
| 423 | + | // skip requests, and returns it with the new count. | |||
| 424 | + | func (p *xpeer) waitReq(skip int, match func(xpeerReq) bool) (xpeerReq, int) { | |||
| 425 | + | p.t.Helper() | |||
| 426 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 427 | + | for time.Now().Before(deadline) { | |||
| 428 | + | reqs := p.requests() | |||
| 429 | + | for i := skip; i < len(reqs); i++ { | |||
| 430 | + | if match(reqs[i]) { | |||
| 431 | + | return reqs[i], i + 1 | |||
| 432 | + | } | |||
| 433 | + | } | |||
| 434 | + | time.Sleep(time.Millisecond) | |||
| 435 | + | } | |||
| 436 | + | p.t.Fatal("xpeer: expected request never arrived") | |||
| 437 | + | return xpeerReq{}, 0 | |||
| 438 | + | } | |||
| 439 | + | ||||
| 440 | + | func (p *xpeer) atom(name string) uint32 { | |||
| 441 | + | p.mu.Lock() | |||
| 442 | + | defer p.mu.Unlock() | |||
| 443 | + | return p.atoms[name] | |||
| 444 | + | } | |||
| 445 | + | ||||
| 446 | + | func (p *xpeer) prop(win uint32, atom string) (xpeerProp, bool) { | |||
| 447 | + | p.mu.Lock() | |||
| 448 | + | defer p.mu.Unlock() | |||
| 449 | + | v, ok := p.props[win][p.atoms[atom]] | |||
| 450 | + | return v, ok | |||
| 451 | + | } | |||
| 452 | + | ||||
| 453 | + | // event sends a 32-byte core event stamped with the current sequence number. | |||
| 454 | + | func (p *xpeer) event(code uint8, fill func(b *[32]byte)) { | |||
| 455 | + | p.mu.Lock() | |||
| 456 | + | defer p.mu.Unlock() | |||
| 457 | + | var b [32]byte | |||
| 458 | + | b[0] = code | |||
| 459 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 460 | + | if fill != nil { | |||
| 461 | + | fill(&b) | |||
| 462 | + | } | |||
| 463 | + | p.write(b[:]) | |||
| 464 | + | } | |||
| 465 | + | ||||
| 466 | + | func (p *xpeer) createNotify(win uint32, override bool) { | |||
| 467 | + | p.event(16, func(b *[32]byte) { | |||
| 468 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 469 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 470 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |||
| 471 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |||
| 472 | + | if override { | |||
| 473 | + | b[22] = 1 | |||
| 474 | + | } | |||
| 475 | + | }) | |||
| 476 | + | } | |||
| 477 | + | ||||
| 478 | + | func (p *xpeer) destroyNotify(win uint32) { | |||
| 479 | + | p.event(17, func(b *[32]byte) { | |||
| 480 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 481 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 482 | + | }) | |||
| 483 | + | } | |||
| 484 | + | ||||
| 485 | + | // pause makes the peer stop reading after the request it is serving. | |||
| 486 | + | func (p *xpeer) pause() { | |||
| 487 | + | g := make(chan struct{}) | |||
| 488 | + | p.gate.Store(&g) | |||
| 489 | + | } | |||
| 490 | + | ||||
| 491 | + | // resume lets the peer read again. | |||
| 492 | + | func (p *xpeer) resume() { | |||
| 493 | + | g := *p.gate.Load() | |||
| 494 | + | open := make(chan struct{}) | |||
| 495 | + | close(open) | |||
| 496 | + | p.gate.Store(&open) | |||
| 497 | + | select { | |||
| 498 | + | case <-g: | |||
| 499 | + | default: | |||
| 500 | + | close(g) | |||
| 501 | + | } | |||
| 502 | + | } | |||
| 503 | + | ||||
| 504 | + | // set mutates the peer's script under its lock. | |||
| 505 | + | func (p *xpeer) set(fn func(p *xpeer)) { | |||
| 506 | + | p.mu.Lock() | |||
| 507 | + | defer p.mu.Unlock() | |||
| 508 | + | fn(p) | |||
| 509 | + | } | |||
| 510 | + | ||||
| 511 | + | // hangup closes the server end; the client reads EOF. | |||
| 512 | + | func (p *xpeer) hangup() { | |||
| 513 | + | if rc, err := p.f.SyscallConn(); err == nil { | |||
| 514 | + | _ = rc.Control(func(fd uintptr) { _ = unix.Shutdown(int(fd), unix.SHUT_RDWR) }) | |||
| 515 | + | } | |||
| 516 | + | } | |||
| 517 | + | ||||
| 518 | + | // setProp scripts a window property for GetProperty. | |||
| 519 | + | func (p *xpeer) setProp(win, atom, typ uint32, format uint8, data []byte) { | |||
| 520 | + | p.mu.Lock() | |||
| 521 | + | defer p.mu.Unlock() | |||
| 522 | + | if p.props[win] == nil { | |||
| 523 | + | p.props[win] = map[uint32]xpeerProp{} | |||
| 524 | + | } | |||
| 525 | + | p.props[win][atom] = xpeerProp{typ: typ, format: format, data: data} | |||
| 526 | + | } | |||
| 527 | + | ||||
| 528 | + | func (p *xpeer) mapRequest(win uint32) { | |||
| 529 | + | p.event(20, func(b *[32]byte) { | |||
| 530 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 531 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 532 | + | }) | |||
| 533 | + | } | |||
| 534 | + | ||||
| 535 | + | func (p *xpeer) mapNotify(win uint32) { | |||
| 536 | + | p.event(19, func(b *[32]byte) { | |||
| 537 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 538 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 539 | + | }) | |||
| 540 | + | } | |||
| 541 | + | ||||
| 542 | + | func (p *xpeer) unmapNotify(win uint32, synthetic bool) { | |||
| 543 | + | p.event(18, func(b *[32]byte) { | |||
| 544 | + | if synthetic { | |||
| 545 | + | b[0] |= 0x80 | |||
| 546 | + | } | |||
| 547 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 548 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 549 | + | }) | |||
| 550 | + | } | |||
| 551 | + | ||||
| 552 | + | func (p *xpeer) propertyNotify(win, atom uint32, deleted bool) { | |||
| 553 | + | p.event(28, func(b *[32]byte) { | |||
| 554 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 555 | + | binary.LittleEndian.PutUint32(b[8:], atom) | |||
| 556 | + | if deleted { | |||
| 557 | + | b[16] = 1 | |||
| 558 | + | } | |||
| 559 | + | }) | |||
| 560 | + | } | |||
| 561 | + | ||||
| 562 | + | func (p *xpeer) reparentNotify(win, parent uint32) { | |||
| 563 | + | p.event(21, func(b *[32]byte) { | |||
| 564 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 565 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 566 | + | binary.LittleEndian.PutUint32(b[12:], parent) | |||
| 567 | + | }) | |||
| 568 | + | } | |||
| 569 | + | ||||
| 570 | + | func (p *xpeer) clientMessage(win, typ uint32, data ...uint32) { | |||
| 571 | + | p.event(33, func(b *[32]byte) { | |||
| 572 | + | b[1] = 32 | |||
| 573 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 574 | + | binary.LittleEndian.PutUint32(b[8:], typ) | |||
| 575 | + | for i, d := range data { | |||
| 576 | + | binary.LittleEndian.PutUint32(b[12+4*i:], d) | |||
| 577 | + | } | |||
| 578 | + | }) | |||
| 579 | + | } | |||
| 580 | + | ||||
| 581 | + | func (p *xpeer) focusIn(win uint32) { | |||
| 582 | + | p.event(9, func(b *[32]byte) { binary.LittleEndian.PutUint32(b[4:], win) }) | |||
| 583 | + | } | |||
| 584 | + | ||||
| 585 | + | func (p *xpeer) configureNotify(win uint32) { | |||
| 586 | + | p.event(22, func(b *[32]byte) { | |||
| 587 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 588 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 589 | + | binary.LittleEndian.PutUint16(b[20:], 10) | |||
| 590 | + | binary.LittleEndian.PutUint16(b[22:], 20) | |||
| 591 | + | binary.LittleEndian.PutUint16(b[24:], 300) | |||
| 592 | + | binary.LittleEndian.PutUint16(b[26:], 200) | |||
| 593 | + | }) | |||
| 594 | + | } | |||
A internal/adapters/wayland/xwm/xwm.go +451 -0
| @@ -0,0 +1,451 @@ | |||||
| 1 | + | // Package xwm is the built-in X11 window manager for Xwayland: it speaks the | |||
| 2 | + | // X protocol over the -wm socket with xturbo and reports managed windows to | |||
| 3 | + | // the Wayland server through the unexported host interface. | |||
| 4 | + | // | |||
| 5 | + | // Threading: Boot runs on the starter goroutine and blocks. The XWM it returns | |||
| 6 | + | // is then handed to the display goroutine, which owns it exclusively; nothing | |||
| 7 | + | // in this package is locked. | |||
| 8 | + | package xwm | |||
| 9 | + | ||||
| 10 | + | import ( | |||
| 11 | + | "context" | |||
| 12 | + | "encoding/binary" | |||
| 13 | + | "errors" | |||
| 14 | + | "fmt" | |||
| 15 | + | "io" | |||
| 16 | + | "os" | |||
| 17 | + | ||||
| 18 | + | "bnema.dev/xturbo" | |||
| 19 | + | "bnema.dev/xturbo/composite" | |||
| 20 | + | "bnema.dev/xturbo/res" | |||
| 21 | + | "bnema.dev/xturbo/xfixes" | |||
| 22 | + | "bnema.dev/xturbo/xproto" | |||
| 23 | + | "github.com/bnema/purego-libwayland/server" | |||
| 24 | + | "github.com/bnema/zerowrap" | |||
| 25 | + | "golang.org/x/sys/unix" | |||
| 26 | + | ) | |||
| 27 | + | ||||
| 28 | + | var ( | |||
| 29 | + | // ErrWMExists is returned by Boot when another client already redirects | |||
| 30 | + | // the root window's children. | |||
| 31 | + | ErrWMExists = errors.New("xwm: another window manager owns the root window") | |||
| 32 | + | // ErrNoComposite is returned by Boot when the X server lacks Composite. | |||
| 33 | + | ErrNoComposite = errors.New("xwm: X server has no Composite extension") | |||
| 34 | + | ) | |||
| 35 | + | ||||
| 36 | + | // Stats are connection and event counters, for diagnostics. | |||
| 37 | + | type Stats struct { | |||
| 38 | + | Events, Replies, Errors, Unattributed, Stale, RingFull, LegacyID, UnmanagedSeen uint64 | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // Version request ids, carried in the first Expect argument of kindVersion. | |||
| 42 | + | const ( | |||
| 43 | + | versionComposite uint32 = iota + 1 | |||
| 44 | + | versionXFixes | |||
| 45 | + | versionRes | |||
| 46 | + | ) | |||
| 47 | + | ||||
| 48 | + | const unpairedCap = 64 | |||
| 49 | + | ||||
| 50 | + | // XWM manages the windows of one Xwayland. Display goroutine only, after Boot. | |||
| 51 | + | type XWM struct { | |||
| 52 | + | conn *xturbo.Conn | |||
| 53 | + | log zerowrap.Logger | |||
| 54 | + | root uint32 | |||
| 55 | + | wmWin uint32 | |||
| 56 | + | atoms [atomCount]uint32 | |||
| 57 | + | tab *table | |||
| 58 | + | h xturbo.Handler | |||
| 59 | + | stats Stats | |||
| 60 | + | ||||
| 61 | + | // attachment | |||
| 62 | + | host host | |||
| 63 | + | src fdSource | |||
| 64 | + | post func(func()) | |||
| 65 | + | flushFn func() | |||
| 66 | + | ||||
| 67 | + | posted, writable, closed bool | |||
| 68 | + | bootErr error | |||
| 69 | + | warnedRing, warnedFull bool | |||
| 70 | + | warnedUnpaired bool | |||
| 71 | + | ||||
| 72 | + | scratch []byte // encode buffer for 32-bit property values, 4 bytes per slot | |||
| 73 | + | ||||
| 74 | + | // Shared by the map, configure and focus flows. | |||
| 75 | + | dirty []Slot // slots with a pending configure, capacity == table size | |||
| 76 | + | clients []uint32 // _NET_CLIENT_LIST, map order | |||
| 77 | + | stacking []uint32 // _NET_CLIENT_LIST_STACKING, bottom to top | |||
| 78 | + | active Slot // slot named by _NET_ACTIVE_WINDOW, 0 = none | |||
| 79 | + | refocus bool // a FocusIn was denied; focus is re-issued in flush | |||
| 80 | + | listsDirty uint8 // client lists to rewrite in flush | |||
| 81 | + | unpaired [unpairedCap]struct{ serial, surface uint64 } | |||
| 82 | + | unpairedN int | |||
| 83 | + | ||||
| 84 | + | space *Space // layout <-> X coordinates, shared with the host | |||
| 85 | + | warnedSpace bool // more outputs than the Space holds, logged once | |||
| 86 | + | ovDirty []Slot // mapped unmanaged slots whose geometry changed, capacity == table size | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM { | |||
| 90 | + | x := &XWM{conn: conn, log: log, root: conn.Root(), tab: newTable(initialSlots), space: new(Space)} | |||
| 91 | + | x.h = xturbo.Handler{OnEvent: x.onEvent, OnReply: x.onReply, OnError: x.onError} | |||
| 92 | + | x.fit() | |||
| 93 | + | return x | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // fit sizes the per-slot buffers to the table; called after every insert. | |||
| 97 | + | func (x *XWM) fit() { | |||
| 98 | + | n := len(x.tab.recs) | |||
| 99 | + | if cap(x.dirty) < n { | |||
| 100 | + | x.dirty = append(make([]Slot, 0, n), x.dirty...) | |||
| 101 | + | x.clients = append(make([]uint32, 0, n), x.clients...) | |||
| 102 | + | x.ovDirty = append(make([]Slot, 0, n), x.ovDirty...) | |||
| 103 | + | x.stacking = append(make([]uint32, 0, n), x.stacking...) | |||
| 104 | + | x.scratch = make([]byte, 0, 4*n) | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // insert adds a record for xid; 0 means the table is full. | |||
| 109 | + | func (x *XWM) insert(xid uint32) Slot { | |||
| 110 | + | s := x.tab.insert(xid) | |||
| 111 | + | if s == 0 { | |||
| 112 | + | if !x.warnedFull { | |||
| 113 | + | x.warnedFull = true | |||
| 114 | + | x.log.Warn().Int("windows", x.tab.n).Msg("X11 window table full, window ignored") | |||
| 115 | + | } | |||
| 116 | + | return 0 | |||
| 117 | + | } | |||
| 118 | + | x.fit() | |||
| 119 | + | return s | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | // expect registers seq, tolerating a full ring: the reply then arrives | |||
| 123 | + | // unattributed and is dropped. | |||
| 124 | + | func (x *XWM) expect(seq xturbo.Seq, kind uint16, a0, a1 uint32) { | |||
| 125 | + | if seq == 0 { | |||
| 126 | + | return | |||
| 127 | + | } | |||
| 128 | + | if err := x.conn.Expect(seq, kind, a0, a1, 0, 0); err != nil { | |||
| 129 | + | x.stats.RingFull++ | |||
| 130 | + | if !x.warnedRing { | |||
| 131 | + | x.warnedRing = true | |||
| 132 | + | x.log.Warn().Err(err).Msg("X11 request not tracked") | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | // setProp32 queues ChangeProperty (replace, format 32) for vals. | |||
| 138 | + | func (x *XWM) setProp32(win uint32, prop int, typ xproto.Atom, vals []uint32) { | |||
| 139 | + | n := 4 * len(vals) | |||
| 140 | + | if cap(x.scratch) < n { | |||
| 141 | + | x.scratch = make([]byte, 0, n) | |||
| 142 | + | } | |||
| 143 | + | p := x.scratch[:n] | |||
| 144 | + | for i, v := range vals { | |||
| 145 | + | binary.LittleEndian.PutUint32(p[4*i:], v) | |||
| 146 | + | } | |||
| 147 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(win), | |||
| 148 | + | xproto.Atom(x.atoms[prop]), typ, 32, uint32(len(vals)), p) | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | // Boot connects to the X server on wmFD and becomes its window manager. It | |||
| 152 | + | // blocks (about 25 s at worst) and must not run on the display goroutine: the | |||
| 153 | + | // handshake needs Xwayland, which needs the display goroutine to serve its | |||
| 154 | + | // Wayland requests. It takes ownership of wmFD and closes it on failure. | |||
| 155 | + | // Boot gives up promptly when ctx ends (the Xwayland process died or the run | |||
| 156 | + | // was cancelled). log is scoped by the caller with logging.For(ctx, "xwm"). | |||
| 157 | + | func Boot(ctx context.Context, wmFD int, log zerowrap.Logger) (*XWM, error) { | |||
| 158 | + | // The watcher shuts the socket down when ctx ends, which wakes any blocked | |||
| 159 | + | // handshake, Sync or InternAtoms. It holds its own duplicate so a shutdown | |||
| 160 | + | // can never reach a descriptor number reused after Boot closed wmFD. | |||
| 161 | + | var watch *os.File | |||
| 162 | + | if dup, err := unix.FcntlInt(uintptr(wmFD), unix.F_DUPFD_CLOEXEC, 0); err == nil { | |||
| 163 | + | watch = os.NewFile(uintptr(dup), "xwm abort") | |||
| 164 | + | } | |||
| 165 | + | stopWatch := func() {} | |||
| 166 | + | if watch != nil { | |||
| 167 | + | quit, joined := make(chan struct{}), make(chan struct{}) | |||
| 168 | + | go func() { | |||
| 169 | + | defer close(joined) | |||
| 170 | + | select { | |||
| 171 | + | case <-ctx.Done(): | |||
| 172 | + | if rc, err := watch.SyscallConn(); err == nil { | |||
| 173 | + | _ = rc.Control(func(fd uintptr) { _ = unix.Shutdown(int(fd), unix.SHUT_RDWR) }) | |||
| 174 | + | } | |||
| 175 | + | case <-quit: | |||
| 176 | + | } | |||
| 177 | + | _ = watch.Close() | |||
| 178 | + | }() | |||
| 179 | + | stopWatch = func() { close(quit); <-joined } | |||
| 180 | + | } | |||
| 181 | + | x, err := boot(wmFD, log) | |||
| 182 | + | stopWatch() | |||
| 183 | + | if err == nil && ctx.Err() != nil { | |||
| 184 | + | x.CloseConn() | |||
| 185 | + | x, err = nil, ctx.Err() | |||
| 186 | + | } | |||
| 187 | + | if err != nil && ctx.Err() != nil { | |||
| 188 | + | err = fmt.Errorf("%w (%w)", err, ctx.Err()) | |||
| 189 | + | } | |||
| 190 | + | return x, err | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | func boot(wmFD int, log zerowrap.Logger) (*XWM, error) { | |||
| 194 | + | conn, err := xturbo.NewConn(wmFD) | |||
| 195 | + | if err != nil { | |||
| 196 | + | return nil, fmt.Errorf("xwm: connect: %w", err) | |||
| 197 | + | } | |||
| 198 | + | x := newXWM(conn, log) | |||
| 199 | + | if err := x.boot(); err != nil { | |||
| 200 | + | _ = conn.Close() | |||
| 201 | + | return nil, err | |||
| 202 | + | } | |||
| 203 | + | return x, nil | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | func (x *XWM) boot() error { | |||
| 207 | + | conn := x.conn | |||
| 208 | + | ext := conn.Ext() | |||
| 209 | + | if !ext.Info(xturbo.ExtComposite).Present { | |||
| 210 | + | return ErrNoComposite | |||
| 211 | + | } | |||
| 212 | + | var visual, depth uint32 | |||
| 213 | + | if it := xproto.NewSetupIter(conn.SetupBytes(), 1); it.Next() { | |||
| 214 | + | if roots := it.Value().Roots(); roots.Next() { | |||
| 215 | + | visual, depth = uint32(roots.Value().RootVisual()), uint32(roots.Value().RootDepth()) | |||
| 216 | + | } | |||
| 217 | + | } | |||
| 218 | + | x.log.Info().Uint32("root", x.root).Uint32("visual", visual).Uint32("depth", depth). | |||
| 219 | + | Bool("xfixes", ext.Info(xturbo.ExtXFixes).Present). | |||
| 220 | + | Bool("res", ext.Info(xturbo.ExtRes).Present).Msg("X server") | |||
| 221 | + | ||||
| 222 | + | if err := conn.InternAtoms(atomNames[:], false, x.atoms[:]); err != nil { | |||
| 223 | + | return fmt.Errorf("xwm: intern atoms: %w", err) | |||
| 224 | + | } | |||
| 225 | + | xid, err := conn.NewXID() | |||
| 226 | + | if err != nil { | |||
| 227 | + | return fmt.Errorf("xwm: allocate window id: %w", err) | |||
| 228 | + | } | |||
| 229 | + | x.wmWin = xid | |||
| 230 | + | ||||
| 231 | + | b := conn.Buf() | |||
| 232 | + | x.expect(composite.QueryVersion(b, 0, 4), kindVersion, versionComposite, 0) | |||
| 233 | + | if ext.Info(xturbo.ExtXFixes).Present { | |||
| 234 | + | x.expect(xfixes.QueryVersion(b, 6, 0), kindVersion, versionXFixes, 0) | |||
| 235 | + | } | |||
| 236 | + | if ext.Info(xturbo.ExtRes).Present { | |||
| 237 | + | x.expect(res.QueryVersion(b, 1, 2), kindVersion, versionRes, 0) | |||
| 238 | + | } | |||
| 239 | + | ||||
| 240 | + | root := xproto.Window(x.root) | |||
| 241 | + | var rootMask xproto.ChangeWindowAttributesValues | |||
| 242 | + | rootMask.SetEventMask(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange) | |||
| 243 | + | x.expect(xproto.ChangeWindowAttributes(b, root, &rootMask), kindRootMask, 0, 0) | |||
| 244 | + | // Only one client may select SubstructureRedirect on the root: wait for | |||
| 245 | + | // the answer before touching anything another window manager owns. | |||
| 246 | + | if err := b.Err(); err != nil { | |||
| 247 | + | return fmt.Errorf("xwm: encode root mask: %w", err) | |||
| 248 | + | } | |||
| 249 | + | if err := conn.Sync(&x.h); err != nil { | |||
| 250 | + | return fmt.Errorf("xwm: root mask: %w", err) | |||
| 251 | + | } | |||
| 252 | + | if x.bootErr != nil { | |||
| 253 | + | return x.bootErr | |||
| 254 | + | } | |||
| 255 | + | composite.RedirectSubwindows(b, root, composite.RedirectManual) | |||
| 256 | + | ||||
| 257 | + | var wv xproto.CreateWindowValues | |||
| 258 | + | wv.SetEventMask(0) | |||
| 259 | + | xproto.CreateWindow(b, 0, xproto.Window(x.wmWin), root, -1, -1, 1, 1, 0, xproto.WindowClassInputOutput, 0, &wv) | |||
| 260 | + | name := []byte("neferwl") | |||
| 261 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMName]), | |||
| 262 | + | xproto.Atom(x.atoms[atomUTF8String]), 8, uint32(len(name)), name) | |||
| 263 | + | x.setProp32(x.root, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin}) | |||
| 264 | + | x.setProp32(x.wmWin, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin}) | |||
| 265 | + | // CurrentTime and no MANAGER announcement: this Xwayland is private to | |||
| 266 | + | // neferwl and nobody waits on these selections. | |||
| 267 | + | xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomWMS0]), 0) | |||
| 268 | + | xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMCMS0]), 0) | |||
| 269 | + | ||||
| 270 | + | supported := [...]int{ | |||
| 271 | + | atomNetWMState, atomNetActiveWindow, atomNetWMStateFocused, | |||
| 272 | + | atomNetWMStateModal, atomNetWMStateFullscreen, atomNetClientList, | |||
| 273 | + | atomNetClientListStacking, atomNetSupportingWMCheck, atomNetWMName, atomNetWMPID, | |||
| 274 | + | atomNetWMWindowType, | |||
| 275 | + | } | |||
| 276 | + | var list [len(supported)]uint32 | |||
| 277 | + | for i, a := range supported { | |||
| 278 | + | list[i] = x.atoms[a] | |||
| 279 | + | } | |||
| 280 | + | x.setProp32(x.root, atomNetSupported, xproto.AtomAtom, list[:]) | |||
| 281 | + | x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, []uint32{0}) | |||
| 282 | + | x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, nil) | |||
| 283 | + | x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, nil) | |||
| 284 | + | xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0) | |||
| 285 | + | if err := b.Err(); err != nil { | |||
| 286 | + | return fmt.Errorf("xwm: encode setup requests: %w", err) | |||
| 287 | + | } | |||
| 288 | + | if err := conn.Sync(&x.h); err != nil { | |||
| 289 | + | return fmt.Errorf("xwm: root setup: %w", err) | |||
| 290 | + | } | |||
| 291 | + | if x.bootErr != nil { | |||
| 292 | + | return x.bootErr | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | // Windows that exist already (Xwayland restarted under a live client set). | |||
| 296 | + | x.expect(xproto.QueryTree(b, root), kindTree, 0, 0) | |||
| 297 | + | if err := conn.Sync(&x.h); err != nil { | |||
| 298 | + | return fmt.Errorf("xwm: query tree: %w", err) | |||
| 299 | + | } | |||
| 300 | + | return x.bootErr | |||
| 301 | + | } | |||
| 302 | + | ||||
| 303 | + | // Attach registers the X socket with the display and hands the windows found | |||
| 304 | + | // at boot to h. Display goroutine only. | |||
| 305 | + | func (x *XWM) Attach(d *server.Display, h host) error { | |||
| 306 | + | src, err := d.AddFDRW(x.conn.Fd(), x.onReadable, x.onWritable) | |||
| 307 | + | if err != nil { | |||
| 308 | + | return fmt.Errorf("xwm: watch X socket: %w", err) | |||
| 309 | + | } | |||
| 310 | + | x.attach(src, h, d.Post) | |||
| 311 | + | return nil | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | // attach is Attach over its seams: the event source and the display's Post. | |||
| 315 | + | func (x *XWM) attach(src fdSource, h host, post func(func())) { | |||
| 316 | + | x.src, x.host, x.post = src, h, post | |||
| 317 | + | x.flushFn = func() { | |||
| 318 | + | x.posted = false | |||
| 319 | + | x.flush() | |||
| 320 | + | } | |||
| 321 | + | for i := range x.tab.recs { | |||
| 322 | + | r := &x.tab.recs[i] | |||
| 323 | + | if r.kind == kindUnmanaged && r.mapped && !r.ready { // mapped before Attach | |||
| 324 | + | x.requestOverrideType(Slot(i + 1)) | |||
| 325 | + | } | |||
| 326 | + | if r.kind != kindManaged || r.id != 0 { | |||
| 327 | + | continue | |||
| 328 | + | } | |||
| 329 | + | s := Slot(i + 1) | |||
| 330 | + | x.adopt(s) | |||
| 331 | + | if r.mapping { // MapRequest or viewable before Attach | |||
| 332 | + | r.mapping = false | |||
| 333 | + | x.beginMap(s) | |||
| 334 | + | } | |||
| 335 | + | } | |||
| 336 | + | x.flush() | |||
| 337 | + | } | |||
| 338 | + | ||||
| 339 | + | // Close detaches from the display, drops every window from the host and closes | |||
| 340 | + | // the X connection. Idempotent. | |||
| 341 | + | func (x *XWM) Close() { | |||
| 342 | + | if x.closed { | |||
| 343 | + | return | |||
| 344 | + | } | |||
| 345 | + | x.closed = true | |||
| 346 | + | s := x.stats | |||
| 347 | + | x.log.Info().Uint64("events", s.Events).Uint64("replies", s.Replies).Uint64("errors", s.Errors). | |||
| 348 | + | Uint64("unattributed", s.Unattributed).Uint64("stale", s.Stale).Uint64("ring_full", s.RingFull). | |||
| 349 | + | Uint64("legacy_id", s.LegacyID).Uint64("unmanaged_seen", s.UnmanagedSeen).Msg("X11 window management closed") | |||
| 350 | + | if x.src != nil { | |||
| 351 | + | x.src.Remove() | |||
| 352 | + | x.src = nil | |||
| 353 | + | } | |||
| 354 | + | if x.host != nil { | |||
| 355 | + | for i := range x.tab.recs { | |||
| 356 | + | r := &x.tab.recs[i] | |||
| 357 | + | switch { | |||
| 358 | + | case r.kind == kindManaged && r.id != 0: | |||
| 359 | + | r.id = 0 | |||
| 360 | + | x.host.Dissociate(Slot(i+1), true) | |||
| 361 | + | case r.kind == kindUnmanaged && r.ovPaired: | |||
| 362 | + | r.ovPaired = false | |||
| 363 | + | x.host.OverrideGone(Slot(i + 1)) | |||
| 364 | + | } | |||
| 365 | + | } | |||
| 366 | + | } | |||
| 367 | + | x.unpairedN = 0 | |||
| 368 | + | _ = x.conn.Close() | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | // CloseConn closes the connection of an XWM that was never attached. | |||
| 372 | + | func (x *XWM) CloseConn() { | |||
| 373 | + | if !x.closed { | |||
| 374 | + | x.closed = true | |||
| 375 | + | _ = x.conn.Close() | |||
| 376 | + | } | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // fail ends window management after an unrecoverable connection error. Xwayland | |||
| 380 | + | // keeps running; its windows no longer map. | |||
| 381 | + | func (x *XWM) fail(err error) { | |||
| 382 | + | if x.closed { | |||
| 383 | + | return | |||
| 384 | + | } | |||
| 385 | + | if errors.Is(err, io.EOF) || errors.Is(err, xturbo.ErrClosed) { | |||
| 386 | + | x.log.Info().Err(err).Msg("X11 window management stopped: Xwayland closed the connection") | |||
| 387 | + | } else { | |||
| 388 | + | x.log.Warn().Err(err).Msg("X11 window management stopped") | |||
| 389 | + | } | |||
| 390 | + | x.Close() | |||
| 391 | + | } | |||
| 392 | + | ||||
| 393 | + | // Stats returns the counters; Close logs them. | |||
| 394 | + | func (x *XWM) Stats() Stats { return x.stats } | |||
| 395 | + | ||||
| 396 | + | func (x *XWM) onReadable() { | |||
| 397 | + | if x.closed { | |||
| 398 | + | return | |||
| 399 | + | } | |||
| 400 | + | if err := x.conn.Dispatch(&x.h); err != nil { | |||
| 401 | + | x.fail(err) | |||
| 402 | + | return | |||
| 403 | + | } | |||
| 404 | + | x.flush() | |||
| 405 | + | } | |||
| 406 | + | ||||
| 407 | + | func (x *XWM) onWritable() { x.flush() } | |||
| 408 | + | ||||
| 409 | + | // wake schedules one flush at the end of the current display iteration. | |||
| 410 | + | func (x *XWM) wake() { | |||
| 411 | + | if x.posted || x.closed || x.post == nil { | |||
| 412 | + | return | |||
| 413 | + | } | |||
| 414 | + | x.posted = true | |||
| 415 | + | x.post(x.flushFn) | |||
| 416 | + | } | |||
| 417 | + | ||||
| 418 | + | // flush sends what the handlers queued: pending configures, an XID prefetch, | |||
| 419 | + | // then the socket, arming POLLOUT only while it is full. | |||
| 420 | + | func (x *XWM) flush() { | |||
| 421 | + | if x.closed { | |||
| 422 | + | return | |||
| 423 | + | } | |||
| 424 | + | x.flushConfigures() | |||
| 425 | + | if x.conn.XIDsLow() { | |||
| 426 | + | _ = x.conn.PrefetchXIDs() | |||
| 427 | + | } | |||
| 428 | + | err := x.conn.Flush() | |||
| 429 | + | switch { | |||
| 430 | + | case err == nil: | |||
| 431 | + | if x.writable { | |||
| 432 | + | x.setWritable(false) | |||
| 433 | + | } | |||
| 434 | + | case errors.Is(err, xturbo.ErrWouldBlock): | |||
| 435 | + | if !x.writable { | |||
| 436 | + | x.setWritable(true) | |||
| 437 | + | } | |||
| 438 | + | default: | |||
| 439 | + | x.fail(err) | |||
| 440 | + | } | |||
| 441 | + | } | |||
| 442 | + | ||||
| 443 | + | func (x *XWM) setWritable(on bool) { | |||
| 444 | + | x.writable = on | |||
| 445 | + | if x.src == nil { | |||
| 446 | + | return | |||
| 447 | + | } | |||
| 448 | + | if err := x.src.SetWritable(on); err != nil { | |||
| 449 | + | x.fail(err) | |||
| 450 | + | } | |||
| 451 | + | } | |||
A internal/adapters/wayland/xwm/xwm_test.go +381 -0
| @@ -0,0 +1,381 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "os" | |||
| 7 | + | "strings" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "bnema.dev/xturbo/xproto" | |||
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 13 | + | "github.com/bnema/zerowrap" | |||
| 14 | + | "github.com/rs/zerolog" | |||
| 15 | + | "github.com/stretchr/testify/mock" | |||
| 16 | + | "github.com/stretchr/testify/require" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | func testLog() zerowrap.Logger { return zerowrap.Logger{Logger: zerolog.Nop()} } | |||
| 20 | + | ||||
| 21 | + | func bootPeer(t *testing.T, mutate func(p *xpeer)) (*XWM, *xpeer) { | |||
| 22 | + | t.Helper() | |||
| 23 | + | fd, p := startXPeer(t, fullPeerExts()) | |||
| 24 | + | if mutate != nil { | |||
| 25 | + | p.set(mutate) | |||
| 26 | + | } | |||
| 27 | + | x, err := Boot(context.Background(), fd, testLog()) | |||
| 28 | + | require.NoError(t, err) | |||
| 29 | + | t.Cleanup(x.CloseConn) | |||
| 30 | + | return x, p | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func atomsOf(p xpeerProp) []uint32 { | |||
| 34 | + | out := make([]uint32, len(p.data)/4) | |||
| 35 | + | for i := range out { | |||
| 36 | + | out[i] = binary.LittleEndian.Uint32(p.data[4*i:]) | |||
| 37 | + | } | |||
| 38 | + | return out | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func TestBootRequests(t *testing.T) { | |||
| 42 | + | x, p := bootPeer(t, nil) | |||
| 43 | + | reqs := p.requests() | |||
| 44 | + | ||||
| 45 | + | first := -1 | |||
| 46 | + | for i, r := range reqs { | |||
| 47 | + | if r.major == opInternAtom { | |||
| 48 | + | first = i | |||
| 49 | + | break | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | require.NotEqual(t, -1, first) | |||
| 53 | + | for i := 0; i < atomCount; i++ { | |||
| 54 | + | r := reqs[first+i] | |||
| 55 | + | require.EqualValues(t, opInternAtom, r.major, i) | |||
| 56 | + | n := int(binary.LittleEndian.Uint16(r.raw[4:])) | |||
| 57 | + | require.Equal(t, atomNames[i], string(r.raw[8:8+n]), i) | |||
| 58 | + | require.Equal(t, p.atom(atomNames[i]), x.atoms[i]) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // Root mask, then composite redirect, then the WM window and ownership. | |||
| 62 | + | var rootMask, redirect, create, s0, cm, focus int | |||
| 63 | + | rootMask, redirect, create, s0, cm, focus = -1, -1, -1, -1, -1, -1 | |||
| 64 | + | wm := x.wmWin | |||
| 65 | + | for i := first + atomCount; i < len(reqs); i++ { | |||
| 66 | + | r := reqs[i] | |||
| 67 | + | switch { | |||
| 68 | + | case r.major == opChangeWindowAttributes && r.u32(4) == peerRoot: | |||
| 69 | + | rootMask = i | |||
| 70 | + | require.EqualValues(t, 1<<11, r.u32(8)) | |||
| 71 | + | want := uint32(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange) | |||
| 72 | + | require.Equal(t, want, r.u32(12)) | |||
| 73 | + | case r.major == 142 && r.minor == 2: | |||
| 74 | + | redirect = i | |||
| 75 | + | require.EqualValues(t, peerRoot, r.u32(4)) | |||
| 76 | + | require.EqualValues(t, 1, r.raw[8], "manual") | |||
| 77 | + | case r.major == opCreateWindow: | |||
| 78 | + | create = i | |||
| 79 | + | require.Equal(t, wm, r.u32(4)) | |||
| 80 | + | case r.major == opSetSelectionOwner && r.u32(8) == p.atom("WM_S0"): | |||
| 81 | + | s0 = i | |||
| 82 | + | require.Equal(t, wm, r.u32(4)) | |||
| 83 | + | case r.major == opSetSelectionOwner && r.u32(8) == p.atom("_NET_WM_CM_S0"): | |||
| 84 | + | cm = i | |||
| 85 | + | case r.major == opSetInputFocus: | |||
| 86 | + | focus = i | |||
| 87 | + | require.EqualValues(t, 0, r.u32(4), "focus None") | |||
| 88 | + | } | |||
| 89 | + | } | |||
| 90 | + | for name, i := range map[string]int{"rootMask": rootMask, "redirect": redirect, "create": create, "WM_S0": s0, "CM_S0": cm, "focus": focus} { | |||
| 91 | + | require.NotEqual(t, -1, i, name) | |||
| 92 | + | } | |||
| 93 | + | require.Less(t, rootMask, redirect) | |||
| 94 | + | require.Less(t, redirect, create) | |||
| 95 | + | require.Less(t, create, s0) | |||
| 96 | + | ||||
| 97 | + | sup, ok := p.prop(peerRoot, "_NET_SUPPORTED") | |||
| 98 | + | require.True(t, ok) | |||
| 99 | + | var names []string | |||
| 100 | + | byAtom := map[uint32]string{} | |||
| 101 | + | for n, a := range p.atoms { | |||
| 102 | + | byAtom[a] = n | |||
| 103 | + | } | |||
| 104 | + | for _, a := range atomsOf(sup) { | |||
| 105 | + | names = append(names, byAtom[a]) | |||
| 106 | + | } | |||
| 107 | + | require.Equal(t, []string{ | |||
| 108 | + | "_NET_WM_STATE", "_NET_ACTIVE_WINDOW", "_NET_WM_STATE_FOCUSED", | |||
| 109 | + | "_NET_WM_STATE_MODAL", "_NET_WM_STATE_FULLSCREEN", "_NET_CLIENT_LIST", | |||
| 110 | + | "_NET_CLIENT_LIST_STACKING", "_NET_SUPPORTING_WM_CHECK", "_NET_WM_NAME", "_NET_WM_PID", | |||
| 111 | + | "_NET_WM_WINDOW_TYPE", | |||
| 112 | + | }, names) | |||
| 113 | + | require.NotContains(t, names, "_NET_WM_MOVERESIZE") | |||
| 114 | + | require.NotContains(t, names, "_NET_WM_STATE_MAXIMIZED_VERT") | |||
| 115 | + | ||||
| 116 | + | check, ok := p.prop(peerRoot, "_NET_SUPPORTING_WM_CHECK") | |||
| 117 | + | require.True(t, ok) | |||
| 118 | + | require.Equal(t, []uint32{wm}, atomsOf(check)) | |||
| 119 | + | check, ok = p.prop(wm, "_NET_SUPPORTING_WM_CHECK") | |||
| 120 | + | require.True(t, ok) | |||
| 121 | + | require.Equal(t, []uint32{wm}, atomsOf(check)) | |||
| 122 | + | name, ok := p.prop(wm, "_NET_WM_NAME") | |||
| 123 | + | require.True(t, ok) | |||
| 124 | + | require.Equal(t, "neferwl", string(name.data)) | |||
| 125 | + | active, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 126 | + | require.True(t, ok) | |||
| 127 | + | require.Equal(t, []uint32{0}, atomsOf(active)) | |||
| 128 | + | for _, l := range []string{"_NET_CLIENT_LIST", "_NET_CLIENT_LIST_STACKING"} { | |||
| 129 | + | v, ok := p.prop(peerRoot, l) | |||
| 130 | + | require.True(t, ok, l) | |||
| 131 | + | require.Empty(t, v.data, l) | |||
| 132 | + | } | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | func TestBootWMExists(t *testing.T) { | |||
| 136 | + | fd, p := startXPeer(t, fullPeerExts()) | |||
| 137 | + | p.set(func(p *xpeer) { p.denyRoot = true }) | |||
| 138 | + | x, err := Boot(context.Background(), fd, testLog()) | |||
| 139 | + | require.ErrorIs(t, err, ErrWMExists) | |||
| 140 | + | require.Nil(t, x) | |||
| 141 | + | // Nothing the running window manager owns was touched. | |||
| 142 | + | for _, r := range p.requests() { | |||
| 143 | + | require.NotEqual(t, uint8(opSetSelectionOwner), r.major) | |||
| 144 | + | if r.major == opChangeProperty { | |||
| 145 | + | require.NotEqual(t, uint32(peerRoot), r.u32(4)) | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | func TestBootNoComposite(t *testing.T) { | |||
| 151 | + | exts := fullPeerExts() | |||
| 152 | + | delete(exts, "Composite") | |||
| 153 | + | fd, _ := startXPeer(t, exts) | |||
| 154 | + | _, err := Boot(context.Background(), fd, testLog()) | |||
| 155 | + | require.ErrorIs(t, err, ErrNoComposite) | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | func TestBootOptionalExtensions(t *testing.T) { | |||
| 159 | + | exts := fullPeerExts() | |||
| 160 | + | delete(exts, "XFIXES") | |||
| 161 | + | delete(exts, "X-Resource") | |||
| 162 | + | fd, _ := startXPeer(t, exts) | |||
| 163 | + | x, err := Boot(context.Background(), fd, testLog()) | |||
| 164 | + | require.NoError(t, err) | |||
| 165 | + | x.CloseConn() | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | func TestBootConnectFailure(t *testing.T) { | |||
| 169 | + | fd, p := startXPeer(t, fullPeerExts()) | |||
| 170 | + | p.hangup() | |||
| 171 | + | _, err := Boot(context.Background(), fd, testLog()) | |||
| 172 | + | require.Error(t, err) | |||
| 173 | + | } | |||
| 174 | + | ||||
| 175 | + | func TestBootExistingWindows(t *testing.T) { | |||
| 176 | + | x, p := bootPeer(t, func(p *xpeer) { | |||
| 177 | + | p.children = []uint32{0x400001, 0x400002} | |||
| 178 | + | p.attrs[0x400002] = [2]byte{1, 2} // override-redirect, viewable | |||
| 179 | + | }) | |||
| 180 | + | _ = p | |||
| 181 | + | require.NotZero(t, x.tab.lookup(0x400001)) | |||
| 182 | + | require.NotZero(t, x.tab.lookup(0x400002)) | |||
| 183 | + | require.Equal(t, kindManaged, x.tab.rec(x.tab.lookup(0x400001)).kind) | |||
| 184 | + | require.Equal(t, kindUnmanaged, x.tab.rec(x.tab.lookup(0x400002)).kind) | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | // readUntil dispatches the socket until cond holds. | |||
| 188 | + | func readUntil(t *testing.T, x *XWM, cond func() bool) { | |||
| 189 | + | t.Helper() | |||
| 190 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 191 | + | for !cond() { | |||
| 192 | + | if time.Now().After(deadline) { | |||
| 193 | + | t.Fatal("condition never held") | |||
| 194 | + | } | |||
| 195 | + | x.onReadable() | |||
| 196 | + | time.Sleep(time.Millisecond) | |||
| 197 | + | } | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | func newPost() (post func(func()), runs *[]func()) { | |||
| 201 | + | var q []func() | |||
| 202 | + | return func(f func()) { q = append(q, f) }, &q | |||
| 203 | + | } | |||
| 204 | + | ||||
| 205 | + | func TestAttachAnnouncesWindowsFoundAtBoot(t *testing.T) { | |||
| 206 | + | x, _ := bootPeer(t, func(p *xpeer) { | |||
| 207 | + | p.children = []uint32{0x400001, 0x400002} | |||
| 208 | + | p.attrs[0x400002] = [2]byte{1, 2} | |||
| 209 | + | }) | |||
| 210 | + | h := newMockhost(t) | |||
| 211 | + | src := newMockfdSource(t) | |||
| 212 | + | h.EXPECT().Created(x.tab.lookup(0x400001), uint32(0x400001)).Return(ports.WindowID(7)).Once() | |||
| 213 | + | post, _ := newPost() | |||
| 214 | + | x.attach(src, h, post) | |||
| 215 | + | require.Equal(t, ports.WindowID(7), x.tab.rec(x.tab.lookup(0x400001)).id) | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | func attached(t *testing.T) (*XWM, *xpeer, *mockhost, *mockfdSource, *[]func()) { | |||
| 219 | + | t.Helper() | |||
| 220 | + | x, p := bootPeer(t, nil) | |||
| 221 | + | h := newMockhost(t) | |||
| 222 | + | src := newMockfdSource(t) | |||
| 223 | + | post, runs := newPost() | |||
| 224 | + | x.attach(src, h, post) | |||
| 225 | + | return x, p, h, src, runs | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | func TestCreateAndDestroyManaged(t *testing.T) { | |||
| 229 | + | x, p, h, src, _ := attached(t) | |||
| 230 | + | const win = 0x500001 | |||
| 231 | + | h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(3)).Once() | |||
| 232 | + | p.createNotify(win, false) | |||
| 233 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 }) | |||
| 234 | + | s := x.tab.lookup(win) | |||
| 235 | + | require.Equal(t, kindManaged, x.tab.rec(s).kind) | |||
| 236 | + | require.Equal(t, ports.WindowID(3), x.tab.rec(s).id) | |||
| 237 | + | ||||
| 238 | + | // The window is watched for property and focus changes. | |||
| 239 | + | req, _ := p.waitReq(0, func(r xpeerReq) bool { return r.major == opChangeWindowAttributes && r.u32(4) == win }) | |||
| 240 | + | require.EqualValues(t, xproto.EventMaskPropertyChange|xproto.EventMaskFocusChange, req.u32(12)) | |||
| 241 | + | ||||
| 242 | + | h.EXPECT().Dissociate(s, true).Once() | |||
| 243 | + | p.destroyNotify(win) | |||
| 244 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 }) | |||
| 245 | + | require.Zero(t, x.tab.n) | |||
| 246 | + | _ = src | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | // The X server reports the XWM's own check window to it like any other | |||
| 250 | + | // child of the root; it is never adopted. | |||
| 251 | + | func TestOwnWindowNeverAdopted(t *testing.T) { | |||
| 252 | + | x, p, _, _, _ := attached(t) // the mock fails the test on any host call | |||
| 253 | + | p.createNotify(x.wmWin, false) | |||
| 254 | + | const marker = 0x500003 | |||
| 255 | + | p.createNotify(marker, true) | |||
| 256 | + | readUntil(t, x, func() bool { return x.tab.lookup(marker) != 0 }) | |||
| 257 | + | require.Zero(t, x.tab.lookup(x.wmWin)) | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | func TestOverrideRedirectNeverReachesHost(t *testing.T) { | |||
| 261 | + | x, p, _, _, _ := attached(t) // the mock fails the test on any host call | |||
| 262 | + | p.createNotify(0x500002, true) | |||
| 263 | + | readUntil(t, x, func() bool { return x.tab.lookup(0x500002) != 0 }) | |||
| 264 | + | require.Equal(t, kindUnmanaged, x.tab.rec(x.tab.lookup(0x500002)).kind) | |||
| 265 | + | require.EqualValues(t, 1, x.Stats().UnmanagedSeen) | |||
| 266 | + | p.destroyNotify(0x500002) | |||
| 267 | + | readUntil(t, x, func() bool { return x.tab.lookup(0x500002) == 0 }) | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | func TestCloseDissociatesEverything(t *testing.T) { | |||
| 271 | + | x, p, h, src, _ := attached(t) | |||
| 272 | + | for _, win := range []uint32{0x500001, 0x500002} { | |||
| 273 | + | h.EXPECT().Created(mock.Anything, win).Return(ports.WindowID(win)).Once() | |||
| 274 | + | p.createNotify(win, false) | |||
| 275 | + | } | |||
| 276 | + | p.createNotify(0x500003, true) | |||
| 277 | + | readUntil(t, x, func() bool { return x.tab.lookup(0x500003) != 0 }) | |||
| 278 | + | h.EXPECT().Dissociate(x.tab.lookup(0x500001), true).Once() | |||
| 279 | + | h.EXPECT().Dissociate(x.tab.lookup(0x500002), true).Once() | |||
| 280 | + | src.EXPECT().Remove().Once() | |||
| 281 | + | x.Close() | |||
| 282 | + | x.Close() // idempotent | |||
| 283 | + | require.True(t, x.closed) | |||
| 284 | + | require.Equal(t, -1, x.conn.Fd()) | |||
| 285 | + | } | |||
| 286 | + | ||||
| 287 | + | func TestEOFClosesXWM(t *testing.T) { | |||
| 288 | + | x, p, h, src, _ := attached(t) | |||
| 289 | + | h.EXPECT().Created(mock.Anything, uint32(0x500001)).Return(ports.WindowID(1)).Once() | |||
| 290 | + | p.createNotify(0x500001, false) | |||
| 291 | + | readUntil(t, x, func() bool { return x.tab.lookup(0x500001) != 0 }) | |||
| 292 | + | h.EXPECT().Dissociate(x.tab.lookup(0x500001), true).Once() | |||
| 293 | + | src.EXPECT().Remove().Once() | |||
| 294 | + | p.hangup() | |||
| 295 | + | readUntil(t, x, func() bool { return x.closed }) | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | func TestWakeCoalescesFlush(t *testing.T) { | |||
| 299 | + | x, _, _, _, runs := attached(t) | |||
| 300 | + | x.wake() | |||
| 301 | + | x.wake() | |||
| 302 | + | x.wake() | |||
| 303 | + | require.Len(t, *runs, 1) | |||
| 304 | + | (*runs)[0]() | |||
| 305 | + | x.wake() | |||
| 306 | + | require.Len(t, *runs, 2) | |||
| 307 | + | } | |||
| 308 | + | ||||
| 309 | + | func TestWriteBackpressure(t *testing.T) { | |||
| 310 | + | x, p, _, src, _ := attached(t) | |||
| 311 | + | p.pause() | |||
| 312 | + | b := x.conn.Buf() | |||
| 313 | + | big := make([]byte, 64<<10) | |||
| 314 | + | for i := 0; i < 64; i++ { // 4 MiB: more than any socket buffer | |||
| 315 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.root), xproto.AtomWmName, xproto.AtomString, 8, uint32(len(big)), big) | |||
| 316 | + | } | |||
| 317 | + | src.EXPECT().SetWritable(true).Return(nil).Once() | |||
| 318 | + | x.flush() | |||
| 319 | + | require.True(t, x.writable) | |||
| 320 | + | x.flush() // still blocked: SetWritable is not called again | |||
| 321 | + | x.onWritable() | |||
| 322 | + | require.True(t, x.writable) | |||
| 323 | + | ||||
| 324 | + | drained := false | |||
| 325 | + | src.EXPECT().SetWritable(false).RunAndReturn(func(bool) error { drained = true; return nil }).Once() | |||
| 326 | + | p.resume() | |||
| 327 | + | deadline := time.Now().Add(10 * time.Second) | |||
| 328 | + | for !drained { | |||
| 329 | + | require.True(t, time.Now().Before(deadline), "never drained") | |||
| 330 | + | x.onWritable() | |||
| 331 | + | time.Sleep(time.Millisecond) | |||
| 332 | + | } | |||
| 333 | + | require.False(t, x.writable) | |||
| 334 | + | } | |||
| 335 | + | ||||
| 336 | + | func TestFlushWithNothingQueuedSendsNothing(t *testing.T) { | |||
| 337 | + | x, p, _, _, _ := attached(t) | |||
| 338 | + | n := len(p.requests()) | |||
| 339 | + | x.flush() | |||
| 340 | + | time.Sleep(20 * time.Millisecond) | |||
| 341 | + | require.Len(t, p.requests(), n, "nothing queued, nothing sent") | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | // The XWM never blocks after Boot: NewXID, Sync and InternAtoms are startup | |||
| 345 | + | // only, so they may appear in production code only inside boot(). | |||
| 346 | + | func TestNoBlockingCallsOutsideBoot(t *testing.T) { | |||
| 347 | + | files, err := os.ReadDir(".") | |||
| 348 | + | require.NoError(t, err) | |||
| 349 | + | for _, f := range files { | |||
| 350 | + | name := f.Name() | |||
| 351 | + | if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { | |||
| 352 | + | continue | |||
| 353 | + | } | |||
| 354 | + | src, err := os.ReadFile(name) | |||
| 355 | + | require.NoError(t, err) | |||
| 356 | + | text := string(src) | |||
| 357 | + | lo, hi := 0, 0 | |||
| 358 | + | if name == "xwm.go" { | |||
| 359 | + | lo = strings.Index(text, "func (x *XWM) boot() error") | |||
| 360 | + | hi = strings.Index(text, "// Attach registers") | |||
| 361 | + | require.Greater(t, hi, lo) | |||
| 362 | + | } | |||
| 363 | + | for _, call := range []string{".NewXID(", ".Sync(", ".InternAtoms("} { | |||
| 364 | + | for _, at := range allIndex(text, call) { | |||
| 365 | + | require.True(t, name == "xwm.go" && at > lo && at < hi, "%s: %s outside boot", name, call) | |||
| 366 | + | } | |||
| 367 | + | } | |||
| 368 | + | } | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | func allIndex(s, sub string) []int { | |||
| 372 | + | var out []int | |||
| 373 | + | for off := 0; ; { | |||
| 374 | + | i := strings.Index(s[off:], sub) | |||
| 375 | + | if i < 0 { | |||
| 376 | + | return out | |||
| 377 | + | } | |||
| 378 | + | out = append(out, off+i) | |||
| 379 | + | off += i + len(sub) | |||
| 380 | + | } | |||
| 381 | + | } | |||
A internal/adapters/xwayland/builtin.go +216 -0
| @@ -0,0 +1,216 @@ | |||||
| 1 | + | package xwayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "fmt" | |||
| 7 | + | "os" | |||
| 8 | + | "os/exec" | |||
| 9 | + | "strconv" | |||
| 10 | + | "strings" | |||
| 11 | + | "syscall" | |||
| 12 | + | "time" | |||
| 13 | + | ||||
| 14 | + | "golang.org/x/sys/unix" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | type builtinStarter struct{} | |||
| 18 | + | ||||
| 19 | + | func (builtinStarter) start(ctx context.Context, d *Display, stderr *logWriter) error { | |||
| 20 | + | // Files own all setup descriptors until Start succeeds and Attach takes | |||
| 21 | + | // the Wayland socket. This also unwinds partial setup failures. | |||
| 22 | + | var files []*os.File | |||
| 23 | + | defer func() { | |||
| 24 | + | for _, f := range files { | |||
| 25 | + | if f != nil { | |||
| 26 | + | f.Close() | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | }() | |||
| 30 | + | pair := func(name string) (int, error) { | |||
| 31 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 32 | + | if err != nil { | |||
| 33 | + | return 0, err | |||
| 34 | + | } | |||
| 35 | + | i := len(files) | |||
| 36 | + | files = append(files, os.NewFile(uintptr(fds[0]), name+" parent"), os.NewFile(uintptr(fds[1]), name+" child")) | |||
| 37 | + | return i, nil | |||
| 38 | + | } | |||
| 39 | + | wl, err := pair("wayland") | |||
| 40 | + | if err != nil { | |||
| 41 | + | return err | |||
| 42 | + | } | |||
| 43 | + | wm, err := pair("wm") | |||
| 44 | + | if err != nil { | |||
| 45 | + | return err | |||
| 46 | + | } | |||
| 47 | + | var pipe [2]int | |||
| 48 | + | if err := unix.Pipe2(pipe[:], unix.O_CLOEXEC); err != nil { | |||
| 49 | + | return err | |||
| 50 | + | } | |||
| 51 | + | rd := os.NewFile(uintptr(pipe[0]), "displayfd read") | |||
| 52 | + | wr := os.NewFile(uintptr(pipe[1]), "displayfd write") | |||
| 53 | + | files = append(files, rd, wr) | |||
| 54 | + | args := []string{d.Name(), "-rootless"} | |||
| 55 | + | for i := range d.sockets { | |||
| 56 | + | args = append(args, "-listenfd", strconv.Itoa(3+i)) | |||
| 57 | + | } | |||
| 58 | + | wlChild := 3 + len(d.sockets) | |||
| 59 | + | args = append(args, "-wm", strconv.Itoa(wlChild+1), "-displayfd", strconv.Itoa(wlChild+2)) | |||
| 60 | + | cmd := exec.Command(d.opts.Binary, args...) | |||
| 61 | + | for _, e := range d.opts.Env { | |||
| 62 | + | if !strings.HasPrefix(e, "WAYLAND_DISPLAY=") && !strings.HasPrefix(e, "WAYLAND_SOCKET=") { | |||
| 63 | + | cmd.Env = append(cmd.Env, e) | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | cmd.Env = append(cmd.Env, "WAYLAND_SOCKET="+strconv.Itoa(wlChild)) | |||
| 67 | + | cmd.ExtraFiles = append(append([]*os.File(nil), d.sockets...), files[wl+1], files[wm+1], wr) | |||
| 68 | + | cmd.Stderr = stderr | |||
| 69 | + | cmd.WaitDelay = 2 * time.Second | |||
| 70 | + | cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} | |||
| 71 | + | if err := cmd.Start(); err != nil { | |||
| 72 | + | return err | |||
| 73 | + | } | |||
| 74 | + | files[wl+1].Close() | |||
| 75 | + | files[wm+1].Close() | |||
| 76 | + | wr.Close() | |||
| 77 | + | d.opts.Log.Info().Str("display", d.Name()).Int("pid", cmd.Process.Pid).Msg("Xwayland started") | |||
| 78 | + | // Detach the Wayland descriptor from os.File ownership before handing it | |||
| 79 | + | // to the display (including its finalizers). | |||
| 80 | + | if d.opts.Attach != nil { | |||
| 81 | + | // Dup transfers an independent descriptor; closing the File wrapper below | |||
| 82 | + | // cannot race with the display's ownership of the transferred descriptor. | |||
| 83 | + | var wlFD int | |||
| 84 | + | wlFD, err = unix.FcntlInt(uintptr(files[wl].Fd()), unix.F_DUPFD_CLOEXEC, 0) | |||
| 85 | + | if err == nil { | |||
| 86 | + | d.opts.Attach(wlFD) | |||
| 87 | + | } | |||
| 88 | + | } | |||
| 89 | + | files[wl].Close() | |||
| 90 | + | done := make(chan error, 1) | |||
| 91 | + | procGone := make(chan struct{}) | |||
| 92 | + | go func() { done <- cmd.Wait(); close(procGone) }() | |||
| 93 | + | if err != nil { | |||
| 94 | + | stopGroup(cmd, done) | |||
| 95 | + | return err | |||
| 96 | + | } | |||
| 97 | + | exited, readyErr := waitReady(ctx, pipe[0], done) | |||
| 98 | + | if readyErr != nil { | |||
| 99 | + | err := readyErr | |||
| 100 | + | if errors.Is(err, errReadinessTimeout) { | |||
| 101 | + | d.opts.Log.Warn().Str("display", d.Name()).Msg("Xwayland readiness timed out") | |||
| 102 | + | } | |||
| 103 | + | // Join Wait unless readiness already observed process exit. | |||
| 104 | + | if !exited { | |||
| 105 | + | stopGroup(cmd, done) | |||
| 106 | + | } | |||
| 107 | + | return err | |||
| 108 | + | } | |||
| 109 | + | rd.Close() | |||
| 110 | + | d.opts.Log.Info().Str("display", d.Name()).Msg("Xwayland ready") | |||
| 111 | + | if d.opts.AttachWM != nil { | |||
| 112 | + | // The WM socket is bootstrapped only now: Xwayland serves it after its | |||
| 113 | + | // Wayland round trips, which the display goroutine must be free to answer. | |||
| 114 | + | wmFD, err := unix.FcntlInt(uintptr(files[wm].Fd()), unix.F_DUPFD_CLOEXEC, 0) | |||
| 115 | + | if err != nil { | |||
| 116 | + | stopGroup(cmd, done) | |||
| 117 | + | return err | |||
| 118 | + | } | |||
| 119 | + | // The context ends with the process or the caller's context, so a | |||
| 120 | + | // bootstrap against a dead or silent Xwayland aborts at once. | |||
| 121 | + | wmCtx, cancel := context.WithCancel(ctx) | |||
| 122 | + | watched := make(chan struct{}) | |||
| 123 | + | go func() { | |||
| 124 | + | defer close(watched) | |||
| 125 | + | select { | |||
| 126 | + | case <-procGone: | |||
| 127 | + | cancel() | |||
| 128 | + | case <-wmCtx.Done(): | |||
| 129 | + | } | |||
| 130 | + | }() | |||
| 131 | + | stop := d.opts.AttachWM(wmCtx, wmFD) | |||
| 132 | + | cancel() | |||
| 133 | + | <-watched | |||
| 134 | + | if stop != nil { | |||
| 135 | + | // Runs before start returns so the restart loop never overlaps two XWMs. | |||
| 136 | + | defer stop() | |||
| 137 | + | } | |||
| 138 | + | } | |||
| 139 | + | select { | |||
| 140 | + | case err := <-done: | |||
| 141 | + | return err | |||
| 142 | + | case <-ctx.Done(): | |||
| 143 | + | stopGroup(cmd, done) | |||
| 144 | + | return ctx.Err() | |||
| 145 | + | } | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | var errReadinessTimeout = errors.New("xwayland readiness timed out") | |||
| 149 | + | ||||
| 150 | + | // waitReady uses bounded poll waits rather than a pipe reader goroutine. | |||
| 151 | + | // The result reports whether cmd.Wait was already joined. | |||
| 152 | + | func waitReady(ctx context.Context, fd int, done <-chan error) (bool, error) { | |||
| 153 | + | deadline := time.Now().Add(10 * time.Second) | |||
| 154 | + | var buf [32]byte | |||
| 155 | + | n := 0 | |||
| 156 | + | for { | |||
| 157 | + | select { | |||
| 158 | + | case err := <-done: | |||
| 159 | + | if err == nil { | |||
| 160 | + | err = errors.New("xwayland exited before readiness") | |||
| 161 | + | } | |||
| 162 | + | return true, err | |||
| 163 | + | default: | |||
| 164 | + | } | |||
| 165 | + | if ctx.Err() != nil { | |||
| 166 | + | return false, ctx.Err() | |||
| 167 | + | } | |||
| 168 | + | if time.Now().After(deadline) { | |||
| 169 | + | return false, errReadinessTimeout | |||
| 170 | + | } | |||
| 171 | + | // A closed pipe also covers process exit, including exec's stderr delay. | |||
| 172 | + | p := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}} | |||
| 173 | + | _, err := unix.Poll(p, 50) | |||
| 174 | + | if errors.Is(err, unix.EINTR) { | |||
| 175 | + | continue | |||
| 176 | + | } | |||
| 177 | + | if err != nil { | |||
| 178 | + | return false, err | |||
| 179 | + | } | |||
| 180 | + | if p[0].Revents == 0 { | |||
| 181 | + | continue | |||
| 182 | + | } | |||
| 183 | + | if p[0].Revents&unix.POLLIN != 0 { | |||
| 184 | + | count, err := unix.Read(fd, buf[n:]) | |||
| 185 | + | if errors.Is(err, unix.EINTR) { | |||
| 186 | + | continue | |||
| 187 | + | } | |||
| 188 | + | if err != nil { | |||
| 189 | + | return false, err | |||
| 190 | + | } | |||
| 191 | + | n += count | |||
| 192 | + | if n > 0 && buf[n-1] == '\n' { | |||
| 193 | + | if _, err := strconv.Atoi(strings.TrimSpace(string(buf[:n]))); err != nil { | |||
| 194 | + | return false, fmt.Errorf("xwayland displayfd: %w", err) | |||
| 195 | + | } | |||
| 196 | + | return false, nil | |||
| 197 | + | } | |||
| 198 | + | if n == len(buf) { | |||
| 199 | + | return false, errors.New("invalid Xwayland displayfd response") | |||
| 200 | + | } | |||
| 201 | + | } | |||
| 202 | + | if p[0].Revents&(unix.POLLHUP|unix.POLLERR|unix.POLLNVAL) != 0 { | |||
| 203 | + | return false, errors.New("xwayland exited before readiness") | |||
| 204 | + | } | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | func stopGroup(cmd *exec.Cmd, done <-chan error) { | |||
| 209 | + | _ = unix.Kill(-cmd.Process.Pid, unix.SIGTERM) | |||
| 210 | + | select { | |||
| 211 | + | case <-done: | |||
| 212 | + | case <-time.After(2 * time.Second): | |||
| 213 | + | _ = unix.Kill(-cmd.Process.Pid, unix.SIGKILL) | |||
| 214 | + | <-done | |||
| 215 | + | } | |||
| 216 | + | } | |||
A internal/adapters/xwayland/starter_mock_test.go +110 -0
| @@ -0,0 +1,110 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package xwayland | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "context" | |||
| 9 | + | ||||
| 10 | + | mock "github.com/stretchr/testify/mock" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // newMockstarter creates a new instance of mockstarter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 14 | + | // The first argument is typically a *testing.T value. | |||
| 15 | + | func newMockstarter(t interface { | |||
| 16 | + | mock.TestingT | |||
| 17 | + | Cleanup(func()) | |||
| 18 | + | }) *mockstarter { | |||
| 19 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 20 | + | helper.Helper() | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | mock := &mockstarter{} | |||
| 24 | + | mock.Mock.Test(t) | |||
| 25 | + | ||||
| 26 | + | t.Cleanup(func() { | |||
| 27 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 28 | + | helper.Helper() | |||
| 29 | + | } | |||
| 30 | + | mock.AssertExpectations(t) | |||
| 31 | + | }) | |||
| 32 | + | ||||
| 33 | + | return mock | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // mockstarter is an autogenerated mock type for the starter type | |||
| 37 | + | type mockstarter struct { | |||
| 38 | + | mock.Mock | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | type mockstarter_Expecter struct { | |||
| 42 | + | mock *mock.Mock | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (_m *mockstarter) EXPECT() *mockstarter_Expecter { | |||
| 46 | + | return &mockstarter_Expecter{mock: &_m.Mock} | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // start provides a mock function for the type mockstarter | |||
| 50 | + | func (_mock *mockstarter) start(ctx context.Context, d *Display, stderr *logWriter) error { | |||
| 51 | + | ret := _mock.Called(ctx, d, stderr) | |||
| 52 | + | ||||
| 53 | + | if len(ret) == 0 { | |||
| 54 | + | panic("no return value specified for start") | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | var r0 error | |||
| 58 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, *Display, *logWriter) error); ok { | |||
| 59 | + | r0 = returnFunc(ctx, d, stderr) | |||
| 60 | + | } else { | |||
| 61 | + | r0 = ret.Error(0) | |||
| 62 | + | } | |||
| 63 | + | return r0 | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // mockstarter_start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'start' | |||
| 67 | + | type mockstarter_start_Call struct { | |||
| 68 | + | *mock.Call | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // start is a helper method to define mock.On call | |||
| 72 | + | // - ctx context.Context | |||
| 73 | + | // - d *Display | |||
| 74 | + | // - stderr *logWriter | |||
| 75 | + | func (_e *mockstarter_Expecter) start(ctx any, d any, stderr any) *mockstarter_start_Call { | |||
| 76 | + | return &mockstarter_start_Call{Call: _e.mock.On("start", ctx, d, stderr)} | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | func (_c *mockstarter_start_Call) Run(run func(ctx context.Context, d *Display, stderr *logWriter)) *mockstarter_start_Call { | |||
| 80 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 81 | + | var arg0 context.Context | |||
| 82 | + | if args[0] != nil { | |||
| 83 | + | arg0 = args[0].(context.Context) | |||
| 84 | + | } | |||
| 85 | + | var arg1 *Display | |||
| 86 | + | if args[1] != nil { | |||
| 87 | + | arg1 = args[1].(*Display) | |||
| 88 | + | } | |||
| 89 | + | var arg2 *logWriter | |||
| 90 | + | if args[2] != nil { | |||
| 91 | + | arg2 = args[2].(*logWriter) | |||
| 92 | + | } | |||
| 93 | + | run( | |||
| 94 | + | arg0, | |||
| 95 | + | arg1, | |||
| 96 | + | arg2, | |||
| 97 | + | ) | |||
| 98 | + | }) | |||
| 99 | + | return _c | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func (_c *mockstarter_start_Call) Return(err error) *mockstarter_start_Call { | |||
| 103 | + | _c.Call.Return(err) | |||
| 104 | + | return _c | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | func (_c *mockstarter_start_Call) RunAndReturn(run func(ctx context.Context, d *Display, stderr *logWriter) error) *mockstarter_start_Call { | |||
| 108 | + | _c.Call.Return(run) | |||
| 109 | + | return _c | |||
| 110 | + | } | |||
M internal/adapters/xwayland/xwayland.go +46 -18
| @@ -1,6 +1,5 @@ | |||||
| 1 | - | // Package xwayland runs X11 clients through xwayland-satellite, started on | 1 | + | // Package xwayland starts an X11 backend on demand. NeferWL owns the display |
| 2 | - | // demand: neferwl owns the X11 display sockets and starts the satellite | 2 | + | // sockets and starts the selected backend when the first client connects. |
| 3 | - | // when the first X11 client connects, and again after it exits. | |||
| 4 | package xwayland | 3 | package xwayland | ||
| 5 | 4 | ||||
| 6 | import ( | 5 | import ( | ||
| @@ -17,15 +16,26 @@ import ( | |||||
| 17 | "syscall" | 16 | "syscall" | ||
| 18 | "time" | 17 | "time" | ||
| 19 | 18 | ||||
| 19 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 20 | "github.com/bnema/zerowrap" | 20 | "github.com/bnema/zerowrap" | ||
| 21 | "golang.org/x/sys/unix" | 21 | "golang.org/x/sys/unix" | ||
| 22 | ) | 22 | ) | ||
| 23 | 23 | ||||
| 24 | // Options configures the X11 display. | 24 | // Options configures the X11 display. | ||
| 25 | type Options struct { | 25 | type Options struct { | ||
| 26 | - | // Binary is the xwayland-satellite executable. | 26 | + | Backend ports.XwaylandBackend |
| 27 | + | // Attach takes ownership of the Wayland descriptor right after the backend | |||
| 28 | + | // starts. | |||
| 29 | + | Attach func(wlFD int) | |||
| 30 | + | // AttachWM takes ownership of the window manager descriptor once the | |||
| 31 | + | // backend is ready. It may block to bootstrap the X11 connection; ctx ends | |||
| 32 | + | // when the backend exits or the run is cancelled, and the bootstrap must | |||
| 33 | + | // then give up. The returned stop (nil for none) is called before the run | |||
| 34 | + | // ends, on backend exit or context cancellation. | |||
| 35 | + | AttachWM func(ctx context.Context, wmFD int) (stop func()) | |||
| 36 | + | // Binary is the selected backend executable. | |||
| 27 | Binary string | 37 | Binary string | ||
| 28 | - | // Env is the satellite's environment; it must hold WAYLAND_DISPLAY. | 38 | + | // Env is the backend environment; satellite requires WAYLAND_DISPLAY. |
| 29 | Env []string | 39 | Env []string | ||
| 30 | // Dir holds the X11 sockets and TmpDir the lock files: /tmp/.X11-unix | 40 | // Dir holds the X11 sockets and TmpDir the lock files: /tmp/.X11-unix | ||
| 31 | // and /tmp outside tests. | 41 | // and /tmp outside tests. | ||
| @@ -35,8 +45,9 @@ type Options struct { | |||||
| 35 | Log zerowrap.Logger | 45 | Log zerowrap.Logger | ||
| 36 | } | 46 | } | ||
| 37 | 47 | ||||
| 38 | - | // Display is an X11 display served by xwayland-satellite. | 48 | + | // Display is an on-demand X11 display. |
| 39 | type Display struct { | 49 | type Display struct { | ||
| 50 | + | starter starter | |||
| 40 | opts Options | 51 | opts Options | ||
| 41 | n int | 52 | n int | ||
| 42 | sockets []*os.File // unix socket, then the abstract one | 53 | sockets []*os.File // unix socket, then the abstract one | ||
| @@ -81,7 +92,10 @@ func Open(opts Options) (*Display, error) { | |||||
| 81 | _ = os.Remove(lock) | 92 | _ = os.Remove(lock) | ||
| 82 | return nil, fmt.Errorf("write X11 lock: %w", err) | 93 | return nil, fmt.Errorf("write X11 lock: %w", err) | ||
| 83 | } | 94 | } | ||
| 84 | - | d := &Display{opts: opts, n: n, lock: lock, socket: filepath.Join(opts.Dir, fmt.Sprintf("X%d", n))} | 95 | + | d := &Display{starter: satelliteStarter{}, opts: opts, n: n, lock: lock, socket: filepath.Join(opts.Dir, fmt.Sprintf("X%d", n))} |
| 96 | + | if opts.Backend == ports.XwaylandBuiltin { | |||
| 97 | + | d.starter = builtinStarter{} | |||
| 98 | + | } | |||
| 85 | if err := d.listen(); err != nil { | 99 | if err := d.listen(); err != nil { | ||
| 86 | last = err | 100 | last = err | ||
| 87 | d.Close() | 101 | d.Close() | ||
| @@ -212,15 +226,15 @@ func (d *Display) Run(ctx context.Context) error { | |||||
| 212 | return err | 226 | return err | ||
| 213 | } | 227 | } | ||
| 214 | start := time.Now() | 228 | start := time.Now() | ||
| 215 | - | stderr := &logWriter{log: d.opts.Log} | 229 | + | stderr := &logWriter{log: d.opts.Log, backend: d.backendName()} |
| 216 | - | err := d.serve(ctx, stderr) | 230 | + | err := d.starter.start(ctx, d, stderr) |
| 217 | if ctx.Err() != nil { | 231 | if ctx.Err() != nil { | ||
| 218 | return nil | 232 | return nil | ||
| 219 | } | 233 | } | ||
| 220 | if err != nil { | 234 | if err != nil { | ||
| 221 | - | d.opts.Log.Warn().Err(err).Strs("stderr", stderr.tail()).Str("display", d.Name()).Msg("xwayland-satellite exited") | 235 | + | d.opts.Log.Warn().Err(err).Strs("stderr", stderr.tail()).Str("display", d.Name()).Msg(d.backendName() + " exited") |
| 222 | } else { | 236 | } else { | ||
| 223 | - | d.opts.Log.Info().Str("display", d.Name()).Msg("xwayland-satellite exited") | 237 | + | d.opts.Log.Info().Str("display", d.Name()).Msg(d.backendName() + " exited") |
| 224 | } | 238 | } | ||
| 225 | // Clients the satellite never accepted would wake us at once: a | 239 | // Clients the satellite never accepted would wake us at once: a | ||
| 226 | // satellite that cannot start must not respawn in a loop. | 240 | // satellite that cannot start must not respawn in a loop. | ||
| @@ -261,8 +275,14 @@ func (d *Display) waitClient(ctx context.Context, wake int) error { | |||||
| 261 | return ctx.Err() | 275 | return ctx.Err() | ||
| 262 | } | 276 | } | ||
| 263 | 277 | ||||
| 264 | - | // serve runs the satellite on the sockets until it exits or ctx ends. | 278 | + | // starter runs a backend on the reserved sockets until it exits or ctx ends. |
| 265 | - | func (d *Display) serve(ctx context.Context, stderr *logWriter) error { | 279 | + | type starter interface { |
| 280 | + | start(ctx context.Context, d *Display, stderr *logWriter) error | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | type satelliteStarter struct{} | |||
| 284 | + | ||||
| 285 | + | func (satelliteStarter) start(ctx context.Context, d *Display, stderr *logWriter) error { | |||
| 266 | args := []string{d.Name()} | 286 | args := []string{d.Name()} | ||
| 267 | for i := range d.sockets { | 287 | for i := range d.sockets { | ||
| 268 | args = append(args, "-listenfd", strconv.Itoa(3+i)) | 288 | args = append(args, "-listenfd", strconv.Itoa(3+i)) | ||
| @@ -318,10 +338,11 @@ func (d *Display) dropPending() { | |||||
| 318 | // logWriter logs the satellite's stderr line by line and keeps the last | 338 | // logWriter logs the satellite's stderr line by line and keeps the last | ||
| 319 | // lines for the exit log; exec copies it from a pipe on its own goroutine. | 339 | // lines for the exit log; exec copies it from a pipe on its own goroutine. | ||
| 320 | type logWriter struct { | 340 | type logWriter struct { | ||
| 321 | - | mu sync.Mutex | 341 | + | backend string |
| 322 | - | log zerowrap.Logger | 342 | + | mu sync.Mutex |
| 323 | - | line []byte | 343 | + | log zerowrap.Logger |
| 324 | - | last []string | 344 | + | line []byte |
| 345 | + | last []string | |||
| 325 | } | 346 | } | ||
| 326 | 347 | ||||
| 327 | // tail is the last lines written. | 348 | // tail is the last lines written. | ||
| @@ -342,7 +363,7 @@ func (w *logWriter) Write(p []byte) (int, error) { | |||||
| 342 | } | 363 | } | ||
| 343 | if i > 0 { | 364 | if i > 0 { | ||
| 344 | line := string(w.line[:i]) | 365 | line := string(w.line[:i]) | ||
| 345 | - | w.log.Debug().Str("line", line).Msg("xwayland-satellite") | 366 | + | w.log.Debug().Str("line", line).Msg(w.backend) |
| 346 | if w.last = append(w.last, line); len(w.last) > 5 { | 367 | if w.last = append(w.last, line); len(w.last) > 5 { | ||
| 347 | w.last = w.last[1:] | 368 | w.last = w.last[1:] | ||
| 348 | } | 369 | } | ||
| @@ -354,3 +375,10 @@ func (w *logWriter) Write(p []byte) (int, error) { | |||||
| 354 | } | 375 | } | ||
| 355 | return len(p), nil | 376 | return len(p), nil | ||
| 356 | } | 377 | } | ||
| 378 | + | ||||
| 379 | + | func (d *Display) backendName() string { | |||
| 380 | + | if d.opts.Backend == ports.XwaylandBuiltin { | |||
| 381 | + | return "Xwayland" | |||
| 382 | + | } | |||
| 383 | + | return "xwayland-satellite" | |||
| 384 | + | } | |||
M internal/adapters/xwayland/xwayland_test.go +207 -0
| @@ -11,6 +11,8 @@ import ( | |||||
| 11 | "time" | 11 | "time" | ||
| 12 | 12 | ||||
| 13 | "github.com/bnema/neferwl/internal/adapters/logging" | 13 | "github.com/bnema/neferwl/internal/adapters/logging" | ||
| 14 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 15 | + | "github.com/stretchr/testify/mock" | |||
| 14 | "golang.org/x/sys/unix" | 16 | "golang.org/x/sys/unix" | ||
| 15 | ) | 17 | ) | ||
| 16 | 18 | ||||
| @@ -270,3 +272,208 @@ func TestRunKillsSatelliteGroup(t *testing.T) { | |||||
| 270 | } | 272 | } | ||
| 271 | } | 273 | } | ||
| 272 | } | 274 | } | ||
| 275 | + | ||||
| 276 | + | func TestStarterSelection(t *testing.T) { | |||
| 277 | + | for _, backend := range []ports.XwaylandBackend{ports.XwaylandSatellite, ports.XwaylandBuiltin} { | |||
| 278 | + | opts := testOptions(t, "true") | |||
| 279 | + | opts.Backend = backend | |||
| 280 | + | d, err := Open(opts) | |||
| 281 | + | if err != nil { | |||
| 282 | + | t.Fatal(err) | |||
| 283 | + | } | |||
| 284 | + | defer d.Close() | |||
| 285 | + | switch d.starter.(type) { | |||
| 286 | + | case satelliteStarter: | |||
| 287 | + | if backend != ports.XwaylandSatellite { | |||
| 288 | + | t.Fatal("satellite selected") | |||
| 289 | + | } | |||
| 290 | + | case builtinStarter: | |||
| 291 | + | if backend != ports.XwaylandBuiltin { | |||
| 292 | + | t.Fatal("builtin selected") | |||
| 293 | + | } | |||
| 294 | + | default: | |||
| 295 | + | t.Fatalf("unexpected starter %T", d.starter) | |||
| 296 | + | } | |||
| 297 | + | } | |||
| 298 | + | } | |||
| 299 | + | ||||
| 300 | + | func TestRunCallsStarter(t *testing.T) { | |||
| 301 | + | d, err := Open(testOptions(t, "unused")) | |||
| 302 | + | if err != nil { | |||
| 303 | + | t.Fatal(err) | |||
| 304 | + | } | |||
| 305 | + | defer d.Close() | |||
| 306 | + | ctx, cancel := context.WithCancel(t.Context()) | |||
| 307 | + | defer cancel() | |||
| 308 | + | m := newMockstarter(t) | |||
| 309 | + | d.starter = m | |||
| 310 | + | m.EXPECT().start(ctx, d, mock.Anything).RunAndReturn(func(_ context.Context, got *Display, stderr *logWriter) error { | |||
| 311 | + | if got != d || stderr == nil { | |||
| 312 | + | t.Error("starter arguments") | |||
| 313 | + | } | |||
| 314 | + | cancel() | |||
| 315 | + | return ctx.Err() | |||
| 316 | + | }).Once() | |||
| 317 | + | done := make(chan error, 1) | |||
| 318 | + | go func() { done <- d.Run(ctx) }() | |||
| 319 | + | unix.Close(dial(t, d.socket)) | |||
| 320 | + | select { | |||
| 321 | + | case err := <-done: | |||
| 322 | + | if err != nil { | |||
| 323 | + | t.Fatal(err) | |||
| 324 | + | } | |||
| 325 | + | case <-time.After(3 * time.Second): | |||
| 326 | + | t.Fatal("Run did not stop") | |||
| 327 | + | } | |||
| 328 | + | } | |||
| 329 | + | ||||
| 330 | + | func TestWaitReady(t *testing.T) { | |||
| 331 | + | for _, tt := range []struct { | |||
| 332 | + | name, data string | |||
| 333 | + | wantErr bool | |||
| 334 | + | }{ | |||
| 335 | + | {"ready", "12\n", false}, {"invalid", "bad\n", true}, {"closed", "", true}, | |||
| 336 | + | } { | |||
| 337 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 338 | + | var p [2]int | |||
| 339 | + | if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil { | |||
| 340 | + | t.Fatal(err) | |||
| 341 | + | } | |||
| 342 | + | defer unix.Close(p[0]) | |||
| 343 | + | if tt.data != "" { | |||
| 344 | + | if _, err := unix.Write(p[1], []byte(tt.data)); err != nil { | |||
| 345 | + | t.Fatal(err) | |||
| 346 | + | } | |||
| 347 | + | } | |||
| 348 | + | unix.Close(p[1]) | |||
| 349 | + | joined, err := waitReady(t.Context(), p[0], make(chan error)) | |||
| 350 | + | if joined || (err != nil) != tt.wantErr { | |||
| 351 | + | t.Fatalf("joined=%v err=%v", joined, err) | |||
| 352 | + | } | |||
| 353 | + | }) | |||
| 354 | + | } | |||
| 355 | + | t.Run("exit", func(t *testing.T) { | |||
| 356 | + | done := make(chan error, 1) | |||
| 357 | + | done <- errors.New("exit") | |||
| 358 | + | joined, err := waitReady(t.Context(), -1, done) | |||
| 359 | + | if !joined || err == nil { | |||
| 360 | + | t.Fatal(joined, err) | |||
| 361 | + | } | |||
| 362 | + | }) | |||
| 363 | + | t.Run("cancel", func(t *testing.T) { | |||
| 364 | + | ctx, cancel := context.WithCancel(t.Context()) | |||
| 365 | + | cancel() | |||
| 366 | + | joined, err := waitReady(ctx, -1, make(chan error)) | |||
| 367 | + | if joined || !errors.Is(err, context.Canceled) { | |||
| 368 | + | t.Fatal(joined, err) | |||
| 369 | + | } | |||
| 370 | + | }) | |||
| 371 | + | } | |||
| 372 | + | ||||
| 373 | + | // readyScript stands in for Xwayland: after a short delay it creates a marker | |||
| 374 | + | // file and reports readiness on the -displayfd descriptor when ready is true, | |||
| 375 | + | // then runs tail. | |||
| 376 | + | func readyScript(t *testing.T, dir string, ready bool, tail string) (bin, marker string) { | |||
| 377 | + | t.Helper() | |||
| 378 | + | bin, marker = filepath.Join(dir, "xwayland"), filepath.Join(dir, "ready") | |||
| 379 | + | script := "#!/bin/sh\nwhile [ $# -gt 0 ]; do [ \"$1\" = -displayfd ] && fd=$2; shift; done\n" + | |||
| 380 | + | "sleep 0.3\n" | |||
| 381 | + | if ready { | |||
| 382 | + | script += ": > " + marker + "\neval \"echo 1 >&$fd\"\n" | |||
| 383 | + | } | |||
| 384 | + | script += tail + "\n" | |||
| 385 | + | if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { | |||
| 386 | + | t.Fatal(err) | |||
| 387 | + | } | |||
| 388 | + | return bin, marker | |||
| 389 | + | } | |||
| 390 | + | ||||
| 391 | + | func runBuiltin(t *testing.T, ctx context.Context, ready bool, tail string, attachWM func(ctx context.Context, wmFD int)) (events []string) { | |||
| 392 | + | t.Helper() | |||
| 393 | + | opts := testOptions(t, "") | |||
| 394 | + | opts.Backend = ports.XwaylandBuiltin | |||
| 395 | + | var marker string | |||
| 396 | + | opts.Binary, marker = readyScript(t, opts.TmpDir, ready, tail) | |||
| 397 | + | exists := func() bool { _, err := os.Stat(marker); return err == nil } | |||
| 398 | + | opts.Attach = func(wlFD int) { | |||
| 399 | + | if exists() { | |||
| 400 | + | t.Error("Attach ran after readiness") | |||
| 401 | + | } | |||
| 402 | + | events = append(events, "attach") | |||
| 403 | + | unix.Close(wlFD) | |||
| 404 | + | } | |||
| 405 | + | opts.AttachWM = func(ctx context.Context, wmFD int) func() { | |||
| 406 | + | if !exists() { | |||
| 407 | + | t.Error("AttachWM ran before readiness") | |||
| 408 | + | } | |||
| 409 | + | events = append(events, "attachWM") | |||
| 410 | + | attachWM(ctx, wmFD) | |||
| 411 | + | return func() { events = append(events, "stop") } | |||
| 412 | + | } | |||
| 413 | + | d, err := Open(opts) | |||
| 414 | + | if err != nil { | |||
| 415 | + | t.Fatal(err) | |||
| 416 | + | } | |||
| 417 | + | defer d.Close() | |||
| 418 | + | _ = d.starter.start(ctx, d, &logWriter{log: opts.Log, backend: d.backendName()}) | |||
| 419 | + | return append(events, "returned") | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | // AttachWM runs only after -displayfd readiness; stop runs before start | |||
| 423 | + | // returns, whether the process exits or the context ends. | |||
| 424 | + | func TestBuiltinAttachWMAfterReadiness(t *testing.T) { | |||
| 425 | + | for name, tc := range map[string]struct { | |||
| 426 | + | tail string | |||
| 427 | + | cancel bool | |||
| 428 | + | }{"process exit": {tail: "exit 0"}, "context end": {tail: "exec sleep 30", cancel: true}} { | |||
| 429 | + | t.Run(name, func(t *testing.T) { | |||
| 430 | + | ctx, cancel := context.WithCancel(t.Context()) | |||
| 431 | + | defer cancel() | |||
| 432 | + | events := runBuiltin(t, ctx, true, tc.tail, func(_ context.Context, wmFD int) { | |||
| 433 | + | if _, err := unix.FcntlInt(uintptr(wmFD), unix.F_GETFD, 0); err != nil { | |||
| 434 | + | t.Errorf("wmFD not open: %v", err) | |||
| 435 | + | } | |||
| 436 | + | unix.Close(wmFD) | |||
| 437 | + | if tc.cancel { | |||
| 438 | + | go func() { time.Sleep(200 * time.Millisecond); cancel() }() | |||
| 439 | + | } | |||
| 440 | + | }) | |||
| 441 | + | if got := strings.Join(events, ","); got != "attach,attachWM,stop,returned" { | |||
| 442 | + | t.Fatalf("events %s", got) | |||
| 443 | + | } | |||
| 444 | + | }) | |||
| 445 | + | } | |||
| 446 | + | } | |||
| 447 | + | ||||
| 448 | + | // A process that exits before reporting readiness never reaches AttachWM. | |||
| 449 | + | func TestBuiltinNoAttachWMWithoutReadiness(t *testing.T) { | |||
| 450 | + | events := runBuiltin(t, t.Context(), false, "exit 1", func(context.Context, int) {}) | |||
| 451 | + | if got := strings.Join(events, ","); got != "attach,returned" { | |||
| 452 | + | t.Fatalf("events %s", got) | |||
| 453 | + | } | |||
| 454 | + | } | |||
| 455 | + | ||||
| 456 | + | // The AttachWM context ends when the process dies, so a bootstrap against a | |||
| 457 | + | // dead Xwayland gives up at once instead of waiting out its handshake. | |||
| 458 | + | func TestBuiltinAttachWMContextEndsWithProcess(t *testing.T) { | |||
| 459 | + | ended := make(chan error, 1) | |||
| 460 | + | start := time.Now() | |||
| 461 | + | events := runBuiltin(t, t.Context(), true, "sleep 0.3", func(ctx context.Context, wmFD int) { | |||
| 462 | + | defer unix.Close(wmFD) | |||
| 463 | + | select { | |||
| 464 | + | case <-ctx.Done(): | |||
| 465 | + | ended <- ctx.Err() | |||
| 466 | + | case <-time.After(5 * time.Second): | |||
| 467 | + | ended <- nil | |||
| 468 | + | } | |||
| 469 | + | }) | |||
| 470 | + | if err := <-ended; err == nil { | |||
| 471 | + | t.Fatal("AttachWM context did not end with the process") | |||
| 472 | + | } | |||
| 473 | + | if time.Since(start) > 3*time.Second { | |||
| 474 | + | t.Fatalf("took %v", time.Since(start)) | |||
| 475 | + | } | |||
| 476 | + | if got := strings.Join(events, ","); got != "attach,attachWM,stop,returned" { | |||
| 477 | + | t.Fatalf("events %s", got) | |||
| 478 | + | } | |||
| 479 | + | } | |||
M internal/app/app.go +13 -6
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "context" | 5 | "context" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "io" | 7 | "io" | ||
| 8 | + | "os/exec" | |||
| 8 | "time" | 9 | "time" | ||
| 9 | 10 | ||||
| 10 | "github.com/bnema/neferwl/internal/adapters/logging" | 11 | "github.com/bnema/neferwl/internal/adapters/logging" | ||
| @@ -188,18 +189,24 @@ func logScenes(log zerowrap.Logger, set []ports.Scene) { | |||||
| 188 | } | 189 | } | ||
| 189 | } | 190 | } | ||
| 190 | 191 | ||||
| 191 | - | // openXwayland reserves an X11 display for xwayland-satellite, or returns | 192 | + | // openXwayland reserves an X11 display for the selected backend, or returns |
| 192 | // nil when X11 is off or unavailable: the session runs without it. | 193 | // nil when X11 is off or unavailable: the session runs without it. | ||
| 193 | - | func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger) *xwayland.Display { | 194 | + | func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger, attach func(int), attachWM func(context.Context, int) func()) *xwayland.Display { |
| 194 | - | bin := opts.Config.Xwayland | 195 | + | cfg := opts.Config.Xwayland |
| 195 | - | if bin == "" || opts.NoXwayland { | 196 | + | bin := cfg.Binary |
| 197 | + | if cfg.Backend == ports.XwaylandOff || opts.NoXwayland { | |||
| 196 | return nil | 198 | return nil | ||
| 197 | } | 199 | } | ||
| 198 | - | if !xwayland.Supported(ctx, bin) { | 200 | + | if cfg.Backend == ports.XwaylandBuiltin { |
| 201 | + | if _, err := exec.LookPath(bin); err != nil { | |||
| 202 | + | log.Warn().Err(err).Str("binary", bin).Msg("Xwayland not found: X11 apps disabled") | |||
| 203 | + | return nil | |||
| 204 | + | } | |||
| 205 | + | } else if !xwayland.Supported(ctx, bin) { | |||
| 199 | log.Warn().Str("binary", bin).Msg("xwayland-satellite 0.7 or later not found: X11 apps disabled") | 206 | log.Warn().Str("binary", bin).Msg("xwayland-satellite 0.7 or later not found: X11 apps disabled") | ||
| 200 | return nil | 207 | return nil | ||
| 201 | } | 208 | } | ||
| 202 | - | d, err := xwayland.Open(xwayland.Options{Binary: bin, Env: env, Dir: "/tmp/.X11-unix", TmpDir: "/tmp", Abstract: true, Log: log}) | 209 | + | d, err := xwayland.Open(xwayland.Options{Backend: cfg.Backend, Attach: attach, AttachWM: attachWM, Binary: bin, Env: env, Dir: "/tmp/.X11-unix", TmpDir: "/tmp", Abstract: true, Log: log}) |
| 203 | if err != nil { | 210 | if err != nil { | ||
| 204 | log.Warn().Err(err).Msg("X11 display disabled") | 211 | log.Warn().Err(err).Msg("X11 display disabled") | ||
| 205 | return nil | 212 | return nil | ||
M internal/app/session.go +1 -1
| @@ -152,7 +152,7 @@ func assembleSession(ctx context.Context, opts Options) (s *session, err error) | |||||
| 152 | s.statePath = statePath | 152 | s.statePath = statePath | ||
| 153 | s.childEnv = append(s.childEnv, statefile.Env+"="+statePath) | 153 | s.childEnv = append(s.childEnv, statefile.Env+"="+statePath) | ||
| 154 | } | 154 | } | ||
| 155 | - | if s.xdisplay = openXwayland(ctx, opts, s.childEnv, logging.For(ctx, "xwayland")); s.xdisplay != nil { | 155 | + | if s.xdisplay = openXwayland(ctx, opts, s.childEnv, logging.For(ctx, "xwayland"), s.server.AttachXwayland, s.server.AttachXWM); s.xdisplay != nil { |
| 156 | s.childEnv = append(s.childEnv, "DISPLAY="+s.xdisplay.Name()) | 156 | s.childEnv = append(s.childEnv, "DISPLAY="+s.xdisplay.Name()) | ||
| 157 | } | 157 | } | ||
| 158 | s.child = launcher.New(s.childEnv, s.security, logging.For(ctx, "launcher")) | 158 | s.child = launcher.New(s.childEnv, s.security, logging.For(ctx, "launcher")) | ||
A internal/app/testdata/x11probe/main.go +232 -0
| @@ -0,0 +1,232 @@ | |||||
| 1 | + | // Command x11probe is a minimal X11 client for the Xwayland end-to-end test. | |||
| 2 | + | // | |||
| 3 | + | // It opens one window filled with solid green, prints "mapped WxH" after the | |||
| 4 | + | // first ConfigureNotify and "focus" on each FocusIn, and exits 0 when the | |||
| 5 | + | // window manager sends WM_DELETE_WINDOW. | |||
| 6 | + | // | |||
| 7 | + | // With -override it also maps a magenta override-redirect window after the | |||
| 8 | + | // main one is mapped (prints "override mapped") and unmaps it after | |||
| 9 | + | // -override-for (prints "override unmapped"). With -override-dismiss-on-focusout | |||
| 10 | + | // it unmaps it instead when the main window loses focus (prints "dismissed"). | |||
| 11 | + | package main | |||
| 12 | + | ||||
| 13 | + | import ( | |||
| 14 | + | "encoding/binary" | |||
| 15 | + | "flag" | |||
| 16 | + | "fmt" | |||
| 17 | + | "os" | |||
| 18 | + | "strconv" | |||
| 19 | + | "strings" | |||
| 20 | + | "time" | |||
| 21 | + | ||||
| 22 | + | "golang.org/x/sys/unix" | |||
| 23 | + | ||||
| 24 | + | "bnema.dev/xturbo" | |||
| 25 | + | "bnema.dev/xturbo/shape" | |||
| 26 | + | "bnema.dev/xturbo/xproto" | |||
| 27 | + | ) | |||
| 28 | + | ||||
| 29 | + | const ( | |||
| 30 | + | green = 0x00ff00 | |||
| 31 | + | magenta = 0xff00ff | |||
| 32 | + | ) | |||
| 33 | + | ||||
| 34 | + | func main() { | |||
| 35 | + | if err := run(); err != nil { | |||
| 36 | + | fmt.Fprintln(os.Stderr, "x11probe:", err) | |||
| 37 | + | os.Exit(1) | |||
| 38 | + | } | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func run() error { | |||
| 42 | + | class := flag.String("class", "X11Probe", "WM_CLASS class (the instance is x11probe)") | |||
| 43 | + | title := flag.String("title", "probe", "_NET_WM_NAME") | |||
| 44 | + | size := flag.String("size", "300x200", "window size WxH") | |||
| 45 | + | sleep := flag.Duration("sleep", 0, "wait before mapping the window") | |||
| 46 | + | override := flag.Bool("override", false, "map a magenta override-redirect window after the main one") | |||
| 47 | + | overrideAt := flag.String("override-at", "100,50", "override window position X,Y in root coordinates") | |||
| 48 | + | overrideSize := flag.String("override-size", "120x80", "override window size WxH") | |||
| 49 | + | overrideFor := flag.Duration("override-for", 0, "unmap the override window after this delay (0: never)") | |||
| 50 | + | overrideType := flag.String("override-type", "menu", "override window type: menu or tooltip") | |||
| 51 | + | overrideShape := flag.Bool("override-shape", false, "give the override window an empty input shape") | |||
| 52 | + | dismiss := flag.Bool("override-dismiss-on-focusout", false, "unmap the override window when the main window loses focus") | |||
| 53 | + | flag.Parse() | |||
| 54 | + | w, h, ok := parseSize(*size) | |||
| 55 | + | if !ok { | |||
| 56 | + | return fmt.Errorf("bad -size %q", *size) | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | c, err := xturbo.Dial("") // $DISPLAY | |||
| 60 | + | if err != nil { | |||
| 61 | + | return err | |||
| 62 | + | } | |||
| 63 | + | defer c.Close() | |||
| 64 | + | ||||
| 65 | + | var atoms [7]uint32 | |||
| 66 | + | if err := c.InternAtoms([]string{"WM_PROTOCOLS", "WM_DELETE_WINDOW", "_NET_WM_NAME", "UTF8_STRING", | |||
| 67 | + | "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", "_NET_WM_WINDOW_TYPE_TOOLTIP"}, false, atoms[:]); err != nil { | |||
| 68 | + | return err | |||
| 69 | + | } | |||
| 70 | + | protocols, deleteWin, netWMName, utf8 := xproto.Atom(atoms[0]), xproto.Atom(atoms[1]), xproto.Atom(atoms[2]), xproto.Atom(atoms[3]) | |||
| 71 | + | typeAtom, typeMenu, typeTooltip := xproto.Atom(atoms[4]), atoms[5], atoms[6] | |||
| 72 | + | ||||
| 73 | + | b := c.Buf() | |||
| 74 | + | xwin, err := c.NewXID() | |||
| 75 | + | if err != nil { | |||
| 76 | + | return err | |||
| 77 | + | } | |||
| 78 | + | xgc, err := c.NewXID() | |||
| 79 | + | if err != nil { | |||
| 80 | + | return err | |||
| 81 | + | } | |||
| 82 | + | win, gc := xproto.Window(xwin), xproto.Gcontext(xgc) | |||
| 83 | + | ||||
| 84 | + | var v xproto.CreateWindowValues | |||
| 85 | + | v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify | xproto.EventMaskFocusChange) | |||
| 86 | + | xproto.CreateWindow(b, 0, win, xproto.Window(c.Root()), 0, 0, uint16(w), uint16(h), 0, xproto.WindowClassInputOutput, 0, &v) | |||
| 87 | + | ||||
| 88 | + | wmClass := []byte("x11probe\x00" + *class + "\x00") | |||
| 89 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, win, xproto.AtomWmClass, xproto.AtomString, 8, uint32(len(wmClass)), wmClass) | |||
| 90 | + | name := []byte(*title) | |||
| 91 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, win, netWMName, utf8, 8, uint32(len(name)), name) | |||
| 92 | + | var del [4]byte | |||
| 93 | + | binary.LittleEndian.PutUint32(del[:], uint32(deleteWin)) | |||
| 94 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, win, protocols, xproto.AtomAtom, 32, 1, del[:]) | |||
| 95 | + | ||||
| 96 | + | var gv xproto.CreateGCValues | |||
| 97 | + | gv.SetForeground(green) | |||
| 98 | + | xproto.CreateGC(b, gc, xproto.Drawable(win), &gv) | |||
| 99 | + | if err := c.Flush(); err != nil { | |||
| 100 | + | return err | |||
| 101 | + | } | |||
| 102 | + | time.Sleep(*sleep) | |||
| 103 | + | xproto.MapWindow(b, win) | |||
| 104 | + | ||||
| 105 | + | // The override-redirect window is created once the main window is mapped. | |||
| 106 | + | var ovWin xproto.Window | |||
| 107 | + | var ovAt, ovSz [2]int | |||
| 108 | + | if *override { | |||
| 109 | + | var ok1, ok2 bool | |||
| 110 | + | x, y, found := strings.Cut(*overrideAt, ",") | |||
| 111 | + | ovAt[0], ovAt[1] = atoi(x), atoi(y) | |||
| 112 | + | ok1 = found | |||
| 113 | + | ovSz[0], ovSz[1], ok2 = parseSize(*overrideSize) | |||
| 114 | + | if !ok1 || !ok2 || (*overrideType != "menu" && *overrideType != "tooltip") { | |||
| 115 | + | return fmt.Errorf("bad -override-at/-override-size/-override-type") | |||
| 116 | + | } | |||
| 117 | + | } | |||
| 118 | + | var ovDeadline time.Time // zero: no timed unmap pending | |||
| 119 | + | unmapOverride := func(msg string) { | |||
| 120 | + | if ovWin == 0 { | |||
| 121 | + | return | |||
| 122 | + | } | |||
| 123 | + | xproto.UnmapWindow(b, ovWin) | |||
| 124 | + | ovWin = 0 | |||
| 125 | + | ovDeadline = time.Time{} | |||
| 126 | + | fmt.Println(msg) | |||
| 127 | + | } | |||
| 128 | + | mapOverride := func() error { | |||
| 129 | + | id, err := c.NewXID() | |||
| 130 | + | if err != nil { | |||
| 131 | + | return err | |||
| 132 | + | } | |||
| 133 | + | ovWin = xproto.Window(id) | |||
| 134 | + | var ov xproto.CreateWindowValues | |||
| 135 | + | ov.SetBackgroundPixel(magenta).SetOverrideRedirect(true) | |||
| 136 | + | xproto.CreateWindow(b, 0, ovWin, xproto.Window(c.Root()), int16(ovAt[0]), int16(ovAt[1]), uint16(ovSz[0]), uint16(ovSz[1]), 0, xproto.WindowClassInputOutput, 0, &ov) | |||
| 137 | + | typ := typeMenu | |||
| 138 | + | if *overrideType == "tooltip" { | |||
| 139 | + | typ = typeTooltip | |||
| 140 | + | } | |||
| 141 | + | var tb [4]byte | |||
| 142 | + | binary.LittleEndian.PutUint32(tb[:], typ) | |||
| 143 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, ovWin, typeAtom, xproto.AtomAtom, 32, 1, tb[:]) | |||
| 144 | + | if *overrideShape { | |||
| 145 | + | shape.Rectangles(b, shape.SoSet, shape.SkInput, 0, ovWin, 0, 0, nil) | |||
| 146 | + | } | |||
| 147 | + | xproto.MapWindow(b, ovWin) | |||
| 148 | + | fmt.Println("override mapped") | |||
| 149 | + | if *overrideFor > 0 { | |||
| 150 | + | ovDeadline = time.Now().Add(*overrideFor) | |||
| 151 | + | } | |||
| 152 | + | return nil | |||
| 153 | + | } | |||
| 154 | + | var ovErr error | |||
| 155 | + | ||||
| 156 | + | mapped, done := false, false | |||
| 157 | + | cw, ch := uint16(w), uint16(h) // current size, tracked from ConfigureNotify | |||
| 158 | + | hd := &xturbo.Handler{ | |||
| 159 | + | OnEvent: func(ev *xturbo.Event) { | |||
| 160 | + | switch ev.Kind { | |||
| 161 | + | case xproto.KindExpose: | |||
| 162 | + | xproto.PolyFillRectangle(b, xproto.Drawable(win), gc, []xproto.Rectangle{{Width: cw, Height: ch}}) | |||
| 163 | + | case xproto.KindConfigureNotify: | |||
| 164 | + | e := xproto.ConfigureNotifyOf(ev) | |||
| 165 | + | if e.Window() != win { | |||
| 166 | + | break | |||
| 167 | + | } | |||
| 168 | + | cw, ch = e.Width(), e.Height() | |||
| 169 | + | if !mapped { | |||
| 170 | + | mapped = true | |||
| 171 | + | fmt.Printf("mapped %dx%d\n", cw, ch) | |||
| 172 | + | if *override { | |||
| 173 | + | ovErr = mapOverride() | |||
| 174 | + | } | |||
| 175 | + | } | |||
| 176 | + | case xproto.KindFocusIn: | |||
| 177 | + | fmt.Println("focus") | |||
| 178 | + | case xproto.KindFocusOut: | |||
| 179 | + | if *dismiss && xproto.FocusOutOf(ev).Event() == win { | |||
| 180 | + | unmapOverride("dismissed") | |||
| 181 | + | } | |||
| 182 | + | case xproto.KindClientMessage: | |||
| 183 | + | e := xproto.ClientMessageOf(ev) | |||
| 184 | + | if e.Type() == protocols && e.Format() == 32 && e.Data().Data32()[0] == uint32(deleteWin) { | |||
| 185 | + | done = true | |||
| 186 | + | } | |||
| 187 | + | } | |||
| 188 | + | }, | |||
| 189 | + | OnError: func(e *xturbo.XError) { fmt.Fprintln(os.Stderr, "x11probe:", e) }, | |||
| 190 | + | } | |||
| 191 | + | for !done { | |||
| 192 | + | events := int16(unix.POLLIN) | |||
| 193 | + | if err := c.Flush(); err == xturbo.ErrWouldBlock { | |||
| 194 | + | events |= unix.POLLOUT | |||
| 195 | + | } else if err != nil { | |||
| 196 | + | return err | |||
| 197 | + | } | |||
| 198 | + | timeout := -1 | |||
| 199 | + | if !ovDeadline.IsZero() { | |||
| 200 | + | timeout = max(int(time.Until(ovDeadline).Milliseconds()), 0) | |||
| 201 | + | } | |||
| 202 | + | fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}} | |||
| 203 | + | if _, err := unix.Poll(fds, timeout); err != nil && err != unix.EINTR { | |||
| 204 | + | return err | |||
| 205 | + | } | |||
| 206 | + | if !ovDeadline.IsZero() && !time.Now().Before(ovDeadline) { | |||
| 207 | + | unmapOverride("override unmapped") | |||
| 208 | + | } | |||
| 209 | + | if err := c.Dispatch(hd); err != nil { | |||
| 210 | + | return err | |||
| 211 | + | } | |||
| 212 | + | if ovErr != nil { | |||
| 213 | + | return ovErr | |||
| 214 | + | } | |||
| 215 | + | } | |||
| 216 | + | return nil | |||
| 217 | + | } | |||
| 218 | + | ||||
| 219 | + | func parseSize(s string) (w, h int, ok bool) { | |||
| 220 | + | ws, hs, found := strings.Cut(s, "x") | |||
| 221 | + | if !found { | |||
| 222 | + | return 0, 0, false | |||
| 223 | + | } | |||
| 224 | + | w, err1 := strconv.Atoi(ws) | |||
| 225 | + | h, err2 := strconv.Atoi(hs) | |||
| 226 | + | return w, h, err1 == nil && err2 == nil && w > 0 && h > 0 && w < 0xffff && h < 0xffff | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | func atoi(s string) int { | |||
| 230 | + | n, _ := strconv.Atoi(s) | |||
| 231 | + | return n | |||
| 232 | + | } | |||
A internal/app/xwayland_test.go +394 -0
| @@ -0,0 +1,394 @@ | |||||
| 1 | + | //go:build xwayland | |||
| 2 | + | ||||
| 3 | + | // End-to-end test of the built-in XWM: a real Xwayland and a real X11 client | |||
| 4 | + | // (testdata/x11probe) run against a headless NeferWL. It needs Xwayland on | |||
| 5 | + | // PATH and a GPU, so it is excluded from `go test ./...` and CI. Run it with | |||
| 6 | + | // `make e2e-xwayland`. | |||
| 7 | + | package app | |||
| 8 | + | ||||
| 9 | + | import ( | |||
| 10 | + | "context" | |||
| 11 | + | "image" | |||
| 12 | + | "image/png" | |||
| 13 | + | "io" | |||
| 14 | + | "os" | |||
| 15 | + | "os/exec" | |||
| 16 | + | "path/filepath" | |||
| 17 | + | "regexp" | |||
| 18 | + | "strings" | |||
| 19 | + | "sync" | |||
| 20 | + | "testing" | |||
| 21 | + | "time" | |||
| 22 | + | ||||
| 23 | + | "github.com/bnema/zerowrap" | |||
| 24 | + | ||||
| 25 | + | "github.com/bnema/neferwl/internal/adapters/config" | |||
| 26 | + | "github.com/bnema/neferwl/internal/adapters/statefile" | |||
| 27 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 28 | + | ) | |||
| 29 | + | ||||
| 30 | + | const ( | |||
| 31 | + | xwaylandTimeout = 10 * time.Second | |||
| 32 | + | xwaylandW, xwaylandH = 640, 360 | |||
| 33 | + | ) | |||
| 34 | + | ||||
| 35 | + | var displayRe = regexp.MustCompile(`"DISPLAY":"(:\d+)"`) | |||
| 36 | + | ||||
| 37 | + | // probe is a running x11probe. | |||
| 38 | + | type probe struct { | |||
| 39 | + | cmd *exec.Cmd | |||
| 40 | + | out *lockedBuffer | |||
| 41 | + | waited chan error | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | // xwaylandEnv is a running headless compositor with the built-in XWM. | |||
| 45 | + | type xwaylandEnv struct { | |||
| 46 | + | bin, display, statePath, shots string | |||
| 47 | + | script *io.PipeWriter | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // startXwayland runs the compositor on one headless output and waits for its | |||
| 51 | + | // X11 display. It skips the test without Xwayland and stops everything when | |||
| 52 | + | // the test ends. | |||
| 53 | + | func startXwayland(t *testing.T) *xwaylandEnv { | |||
| 54 | + | t.Helper() | |||
| 55 | + | if _, err := exec.LookPath("Xwayland"); err != nil { | |||
| 56 | + | t.Skip("Xwayland not on PATH") | |||
| 57 | + | } | |||
| 58 | + | // The compositor must not read or write the live session's state. | |||
| 59 | + | for _, k := range []string{"NEFERWL_STATE", "NEFERWL_SLOT"} { | |||
| 60 | + | if v, ok := os.LookupEnv(k); ok { | |||
| 61 | + | os.Unsetenv(k) | |||
| 62 | + | t.Cleanup(func() { os.Setenv(k, v) }) | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | runtime := t.TempDir() | |||
| 66 | + | t.Setenv("XDG_RUNTIME_DIR", runtime) | |||
| 67 | + | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |||
| 68 | + | shots := t.TempDir() | |||
| 69 | + | bin := buildProbe(t) | |||
| 70 | + | ||||
| 71 | + | script, scriptW := io.Pipe() | |||
| 72 | + | logs := &lockedBuffer{} | |||
| 73 | + | ctx, cancel := context.WithCancel(zerowrap.WithCtx(context.Background(), zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: logs}))) | |||
| 74 | + | cfg := config.Defaults() | |||
| 75 | + | cfg.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland"} | |||
| 76 | + | done := make(chan error, 1) | |||
| 77 | + | go func() { | |||
| 78 | + | done <- Run(ctx, Options{ | |||
| 79 | + | Backend: "headless", NoTerminal: true, Config: cfg, Script: script, | |||
| 80 | + | ScreenshotDir: shots, Sizes: [][2]int{{xwaylandW, xwaylandH}}, | |||
| 81 | + | }) | |||
| 82 | + | }() | |||
| 83 | + | t.Cleanup(func() { | |||
| 84 | + | if t.Failed() { | |||
| 85 | + | t.Logf("compositor log:\n%s", logs.String()) | |||
| 86 | + | } | |||
| 87 | + | cancel() | |||
| 88 | + | _ = scriptW.Close() | |||
| 89 | + | select { | |||
| 90 | + | case err := <-done: | |||
| 91 | + | if err != nil { | |||
| 92 | + | t.Errorf("Run: %v", err) | |||
| 93 | + | } | |||
| 94 | + | case <-time.After(5 * time.Second): | |||
| 95 | + | t.Error("Run did not stop") | |||
| 96 | + | } | |||
| 97 | + | }) | |||
| 98 | + | ||||
| 99 | + | var display string | |||
| 100 | + | until(t, "X11 display in the log", func() bool { | |||
| 101 | + | if m := displayRe.FindStringSubmatch(logs.String()); m != nil { | |||
| 102 | + | display = m[1] | |||
| 103 | + | return true | |||
| 104 | + | } | |||
| 105 | + | return false | |||
| 106 | + | }) | |||
| 107 | + | statePath, err := statefile.Path(runtime, "wayland-1") | |||
| 108 | + | if err != nil { | |||
| 109 | + | t.Fatal(err) | |||
| 110 | + | } | |||
| 111 | + | return &xwaylandEnv{bin: bin, display: display, statePath: statePath, shots: shots, script: scriptW} | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | // TestXwaylandProbe: an X11 client is managed by the built-in XWM. It shows | |||
| 115 | + | // up in the state with its class and pid, is drawn, tiles next to a second | |||
| 116 | + | // one, receives focus and is closed by the close-window bind. | |||
| 117 | + | func TestXwaylandProbe(t *testing.T) { | |||
| 118 | + | e := startXwayland(t) | |||
| 119 | + | bin, display, statePath, shots, scriptW := e.bin, e.display, e.statePath, e.shots, e.script | |||
| 120 | + | first := startProbe(t, bin, display, "-title", "one") | |||
| 121 | + | until(t, "probe one mapped", func() bool { return strings.Contains(first.out.String(), "mapped ") }) | |||
| 122 | + | ||||
| 123 | + | var st statefile.State | |||
| 124 | + | until(t, "one visible X11Probe window", func() bool { | |||
| 125 | + | st = readState(statePath) | |||
| 126 | + | return len(st.Windows) == 1 && st.Windows[0].AppID == "X11Probe" && st.Windows[0].Visible | |||
| 127 | + | }) | |||
| 128 | + | if w := st.Windows[0]; w.PID != first.cmd.Process.Pid { | |||
| 129 | + | t.Errorf("window pid %d, want %d", w.PID, first.cmd.Process.Pid) | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // The client draws green and the compositor shows it. | |||
| 133 | + | id := st.Windows[0].ID | |||
| 134 | + | until(t, "green window in latest.png", func() bool { | |||
| 135 | + | r, ok := tileRect(readState(statePath), id) | |||
| 136 | + | return ok && greenShare(filepath.Join(shots, "latest.png"), r) >= 0.5 | |||
| 137 | + | }) | |||
| 138 | + | ||||
| 139 | + | // Focus round trip: the compositor focused the window, the client saw it. | |||
| 140 | + | until(t, "probe one focus", func() bool { return strings.Contains(first.out.String(), "focus") }) | |||
| 141 | + | ||||
| 142 | + | // A second probe tiles beside the first. | |||
| 143 | + | second := startProbe(t, bin, display, "-title", "two") | |||
| 144 | + | until(t, "two tiled windows", func() bool { | |||
| 145 | + | st = readState(statePath) | |||
| 146 | + | if len(st.Windows) != 2 { | |||
| 147 | + | return false | |||
| 148 | + | } | |||
| 149 | + | for _, w := range st.Windows { | |||
| 150 | + | if w.AppID != "X11Probe" || !w.Visible || w.Floating { | |||
| 151 | + | return false | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | return true | |||
| 155 | + | }) | |||
| 156 | + | until(t, "probe two focus", func() bool { return strings.Contains(second.out.String(), "focus") }) | |||
| 157 | + | ||||
| 158 | + | // The close-window bind asks the focused (second) probe to close. | |||
| 159 | + | if _, err := io.WriteString(scriptW, "key Super+q\n"); err != nil { | |||
| 160 | + | t.Fatal(err) | |||
| 161 | + | } | |||
| 162 | + | select { | |||
| 163 | + | case err := <-second.waited: | |||
| 164 | + | if err != nil { | |||
| 165 | + | t.Errorf("probe two exited with %v, want 0", err) | |||
| 166 | + | } | |||
| 167 | + | case <-time.After(2 * time.Second): | |||
| 168 | + | t.Fatal("probe two did not exit within 2s of Super+q") | |||
| 169 | + | } | |||
| 170 | + | until(t, "one window left", func() bool { | |||
| 171 | + | st = readState(statePath) | |||
| 172 | + | return len(st.Windows) == 1 && st.Windows[0].PID == first.cmd.Process.Pid | |||
| 173 | + | }) | |||
| 174 | + | select { | |||
| 175 | + | case err := <-first.waited: | |||
| 176 | + | t.Fatalf("probe one exited (%v) though only the focused probe was closed", err) | |||
| 177 | + | default: | |||
| 178 | + | } | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | // buildProbe builds testdata/x11probe. | |||
| 182 | + | func buildProbe(t *testing.T) string { | |||
| 183 | + | t.Helper() | |||
| 184 | + | bin := filepath.Join(t.TempDir(), "x11probe") | |||
| 185 | + | build := exec.Command("go", "build", "-o", bin, "./testdata/x11probe") | |||
| 186 | + | build.Env = append(os.Environ(), "CGO_ENABLED=0") | |||
| 187 | + | if out, err := build.CombinedOutput(); err != nil { | |||
| 188 | + | t.Fatalf("build x11probe: %v: %s", err, out) | |||
| 189 | + | } | |||
| 190 | + | return bin | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | // startProbe starts x11probe on display, with X11 as its only display | |||
| 194 | + | // server, and stops it when the test ends. | |||
| 195 | + | func startProbe(t *testing.T, bin, display string, args ...string) *probe { | |||
| 196 | + | t.Helper() | |||
| 197 | + | p := &probe{out: &lockedBuffer{}, waited: make(chan error, 1)} | |||
| 198 | + | p.cmd = exec.Command(bin, args...) | |||
| 199 | + | p.cmd.Env = append(envWithout("DISPLAY", "WAYLAND_DISPLAY", "NEFERWL_STATE", "NEFERWL_SLOT"), "DISPLAY="+display) | |||
| 200 | + | p.cmd.Stdout = p.out | |||
| 201 | + | stderr := &lockedBuffer{} | |||
| 202 | + | p.cmd.Stderr = stderr | |||
| 203 | + | if err := p.cmd.Start(); err != nil { | |||
| 204 | + | t.Fatal(err) | |||
| 205 | + | } | |||
| 206 | + | var exited sync.Once | |||
| 207 | + | go func() { p.waited <- p.cmd.Wait() }() | |||
| 208 | + | t.Cleanup(func() { | |||
| 209 | + | exited.Do(func() { | |||
| 210 | + | _ = p.cmd.Process.Kill() | |||
| 211 | + | select { | |||
| 212 | + | case <-p.waited: | |||
| 213 | + | case <-time.After(2 * time.Second): | |||
| 214 | + | } | |||
| 215 | + | }) | |||
| 216 | + | if s := stderr.String(); s != "" { | |||
| 217 | + | t.Logf("x11probe stderr: %s", s) | |||
| 218 | + | } | |||
| 219 | + | }) | |||
| 220 | + | return p | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | func envWithout(keys ...string) []string { | |||
| 224 | + | var env []string | |||
| 225 | + | next: | |||
| 226 | + | for _, kv := range os.Environ() { | |||
| 227 | + | for _, k := range keys { | |||
| 228 | + | if strings.HasPrefix(kv, k+"=") { | |||
| 229 | + | continue next | |||
| 230 | + | } | |||
| 231 | + | } | |||
| 232 | + | env = append(env, kv) | |||
| 233 | + | } | |||
| 234 | + | return env | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // until polls cond every 20 ms and fails the test after xwaylandTimeout. | |||
| 238 | + | func until(t *testing.T, what string, cond func() bool) { | |||
| 239 | + | t.Helper() | |||
| 240 | + | for deadline := time.Now().Add(xwaylandTimeout); !cond(); time.Sleep(20 * time.Millisecond) { | |||
| 241 | + | if time.Now().After(deadline) { | |||
| 242 | + | t.Fatalf("timeout waiting for %s", what) | |||
| 243 | + | } | |||
| 244 | + | } | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | // readState returns the current state file, or an empty state while it does | |||
| 248 | + | // not exist or is being replaced. | |||
| 249 | + | func readState(path string) statefile.State { | |||
| 250 | + | st, err := statefile.Read(path) | |||
| 251 | + | if err != nil { | |||
| 252 | + | return statefile.State{} | |||
| 253 | + | } | |||
| 254 | + | return st | |||
| 255 | + | } | |||
| 256 | + | ||||
| 257 | + | // tileRect finds the rectangle of window id in the tiled columns. | |||
| 258 | + | func tileRect(st statefile.State, id uint64) (image.Rectangle, bool) { | |||
| 259 | + | for _, ws := range st.Workspaces { | |||
| 260 | + | for _, c := range ws.Columns { | |||
| 261 | + | for _, w := range c.Windows { | |||
| 262 | + | if w.ID == id { | |||
| 263 | + | return image.Rect(w.X, w.Y, w.X+w.Width, w.Y+w.Height), true | |||
| 264 | + | } | |||
| 265 | + | } | |||
| 266 | + | } | |||
| 267 | + | } | |||
| 268 | + | return image.Rectangle{}, false | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | // greenShare is the fraction of pixels of r in the PNG at path that are | |||
| 272 | + | // clearly green; 0 when the image cannot be read yet. | |||
| 273 | + | func greenShare(path string, r image.Rectangle) float64 { | |||
| 274 | + | f, err := os.Open(path) | |||
| 275 | + | if err != nil { | |||
| 276 | + | return 0 | |||
| 277 | + | } | |||
| 278 | + | defer f.Close() | |||
| 279 | + | img, err := png.Decode(f) | |||
| 280 | + | if err != nil { | |||
| 281 | + | return 0 | |||
| 282 | + | } | |||
| 283 | + | r = r.Intersect(img.Bounds()) | |||
| 284 | + | if r.Empty() { | |||
| 285 | + | return 0 | |||
| 286 | + | } | |||
| 287 | + | n := 0 | |||
| 288 | + | for y := r.Min.Y; y < r.Max.Y; y++ { | |||
| 289 | + | for x := r.Min.X; x < r.Max.X; x++ { | |||
| 290 | + | cr, cg, cb, _ := img.At(x, y).RGBA() | |||
| 291 | + | if cg>>8 > 200 && cr>>8 < 60 && cb>>8 < 60 { | |||
| 292 | + | n++ | |||
| 293 | + | } | |||
| 294 | + | } | |||
| 295 | + | } | |||
| 296 | + | return float64(n) / float64(r.Dx()*r.Dy()) | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | // magentaShare is the fraction of pixels of r in the PNG at path that are | |||
| 300 | + | // clearly magenta; 0 when the image cannot be read yet. | |||
| 301 | + | func magentaShare(path string, r image.Rectangle) float64 { | |||
| 302 | + | f, err := os.Open(path) | |||
| 303 | + | if err != nil { | |||
| 304 | + | return 0 | |||
| 305 | + | } | |||
| 306 | + | defer f.Close() | |||
| 307 | + | img, err := png.Decode(f) | |||
| 308 | + | if err != nil { | |||
| 309 | + | return 0 | |||
| 310 | + | } | |||
| 311 | + | r = r.Intersect(img.Bounds()) | |||
| 312 | + | if r.Empty() { | |||
| 313 | + | return 0 | |||
| 314 | + | } | |||
| 315 | + | n := 0 | |||
| 316 | + | for y := r.Min.Y; y < r.Max.Y; y++ { | |||
| 317 | + | for x := r.Min.X; x < r.Max.X; x++ { | |||
| 318 | + | cr, cg, cb, _ := img.At(x, y).RGBA() | |||
| 319 | + | if cr>>8 > 200 && cg>>8 < 60 && cb>>8 > 200 { | |||
| 320 | + | n++ | |||
| 321 | + | } | |||
| 322 | + | } | |||
| 323 | + | } | |||
| 324 | + | return float64(n) / float64(r.Dx()*r.Dy()) | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | // TestXwaylandOverride: an override-redirect window (a menu) of an X11 client | |||
| 328 | + | // is drawn above the toplevel at its X position, is not a window in the | |||
| 329 | + | // state, leaves keyboard focus on the toplevel and vanishes when unmapped. | |||
| 330 | + | func TestXwaylandOverride(t *testing.T) { | |||
| 331 | + | e := startXwayland(t) | |||
| 332 | + | // Probe unmaps the menu 3 s after mapping it; the first checks run well | |||
| 333 | + | // inside that window. | |||
| 334 | + | p := startProbe(t, e.bin, e.display, "-title", "menu", "-override", "-override-at", "100,50", | |||
| 335 | + | "-override-size", "120x80", "-override-for", "3s") | |||
| 336 | + | until(t, "override mapped", func() bool { return strings.Contains(p.out.String(), "override mapped") }) | |||
| 337 | + | shot := filepath.Join(e.shots, "latest.png") | |||
| 338 | + | box := image.Rect(100, 50, 220, 130) // output-local: one output at the X origin | |||
| 339 | + | until(t, "magenta menu in latest.png", func() bool { return magentaShare(shot, box) >= 0.9 }) | |||
| 340 | + | ||||
| 341 | + | var st statefile.State | |||
| 342 | + | until(t, "focused X11Probe toplevel", func() bool { | |||
| 343 | + | st = readState(e.statePath) | |||
| 344 | + | return len(st.Windows) >= 1 && st.Window != nil | |||
| 345 | + | }) | |||
| 346 | + | if len(st.Windows) != 1 || st.Windows[0].AppID != "X11Probe" { | |||
| 347 | + | t.Fatalf("state windows = %+v, want only the X11Probe toplevel", st.Windows) | |||
| 348 | + | } | |||
| 349 | + | id := st.Windows[0].ID | |||
| 350 | + | if st.Window.ID != id { | |||
| 351 | + | t.Errorf("focused window = %+v, want the toplevel %d", st.Window, id) | |||
| 352 | + | } | |||
| 353 | + | until(t, "probe focus", func() bool { return strings.Contains(p.out.String(), "focus") }) | |||
| 354 | + | ||||
| 355 | + | // The menu is above the toplevel: its box is magenta, not green. | |||
| 356 | + | if g := greenShare(shot, box); g > 0.1 { | |||
| 357 | + | t.Errorf("green share in menu box = %.2f, menu is not above the toplevel", g) | |||
| 358 | + | } | |||
| 359 | + | until(t, "override unmapped", func() bool { return strings.Contains(p.out.String(), "override unmapped") }) | |||
| 360 | + | until(t, "menu gone from latest.png", func() bool { | |||
| 361 | + | return magentaShare(shot, box) == 0 && greenShare(shot, box) >= 0.9 | |||
| 362 | + | }) | |||
| 363 | + | if st := readState(e.statePath); st.Window == nil || st.Window.ID != id { | |||
| 364 | + | t.Errorf("focus after the menu closed = %+v, want %d", st.Window, id) | |||
| 365 | + | } | |||
| 366 | + | } | |||
| 367 | + | ||||
| 368 | + | // TestXwaylandOverrideDismiss: the menu owner sees FocusOut on the toplevel | |||
| 369 | + | // when core focuses another window, which is how toolkits close their menus. | |||
| 370 | + | func TestXwaylandOverrideDismiss(t *testing.T) { | |||
| 371 | + | e := startXwayland(t) | |||
| 372 | + | p := startProbe(t, e.bin, e.display, "-title", "menu", "-override", "-override-dismiss-on-focusout") | |||
| 373 | + | until(t, "override mapped", func() bool { return strings.Contains(p.out.String(), "override mapped") }) | |||
| 374 | + | until(t, "probe focus", func() bool { return strings.Contains(p.out.String(), "focus") }) | |||
| 375 | + | // A second X11 window takes the focus away from the menu owner. | |||
| 376 | + | startProbe(t, e.bin, e.display, "-title", "other") | |||
| 377 | + | until(t, "menu dismissed on FocusOut", func() bool { return strings.Contains(p.out.String(), "dismissed") }) | |||
| 378 | + | box := image.Rect(100, 50, 220, 130) | |||
| 379 | + | until(t, "menu gone from latest.png", func() bool { | |||
| 380 | + | return magentaShare(filepath.Join(e.shots, "latest.png"), box) == 0 | |||
| 381 | + | }) | |||
| 382 | + | } | |||
| 383 | + | ||||
| 384 | + | // TestXwaylandOverrideShape: a menu with an empty input shape is still drawn | |||
| 385 | + | // and still not a window. | |||
| 386 | + | func TestXwaylandOverrideShape(t *testing.T) { | |||
| 387 | + | e := startXwayland(t) | |||
| 388 | + | p := startProbe(t, e.bin, e.display, "-title", "menu", "-override", "-override-shape", "-override-type", "tooltip") | |||
| 389 | + | until(t, "override mapped", func() bool { return strings.Contains(p.out.String(), "override mapped") }) | |||
| 390 | + | until(t, "magenta tooltip in latest.png", func() bool { | |||
| 391 | + | return magentaShare(filepath.Join(e.shots, "latest.png"), image.Rect(100, 50, 220, 130)) >= 0.9 | |||
| 392 | + | }) | |||
| 393 | + | until(t, "only the toplevel in state", func() bool { return len(readState(e.statePath).Windows) == 1 }) | |||
| 394 | + | } | |||
M internal/core/client.go +8 -0
| @@ -70,6 +70,14 @@ func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish, | |||||
| 70 | if _, w := c.screenOf(v.ID); w != nil { | 70 | if _, w := c.screenOf(v.ID); w != nil { | ||
| 71 | w.SetModal(v.ID, v.Modal) | 71 | w.SetModal(v.ID, v.Modal) | ||
| 72 | } | 72 | } | ||
| 73 | + | case ports.UnmanagedMapped, ports.UnmanagedMoved, ports.UnmanagedUnmapped: | |||
| 74 | + | changed, err := c.unmanagedEvent(ctx, v) | |||
| 75 | + | if err != nil { | |||
| 76 | + | return false, true | |||
| 77 | + | } | |||
| 78 | + | if !changed { | |||
| 79 | + | return false, false | |||
| 80 | + | } | |||
| 73 | case ports.WindowUnmapped: | 81 | case ports.WindowUnmapped: | ||
| 74 | if c.unmapClient(ctx, v) != nil { | 82 | if c.unmapClient(ctx, v) != nil { | ||
| 75 | return false, true | 83 | return false, true | ||
M internal/core/core.go +16 -5
| @@ -103,9 +103,11 @@ type Core struct { | |||||
| 103 | binds map[binding]Action | 103 | binds map[binding]Action | ||
| 104 | pressed map[string]bool | 104 | pressed map[string]bool | ||
| 105 | configures configures | 105 | configures configures | ||
| 106 | - | pointer WindowID | 106 | + | // placed is the last position (X, Y of the Rect) sent in a PlaceWindow, per window. |
| 107 | - | grab WindowID | 107 | + | placed map[WindowID]Rect |
| 108 | - | buttons map[uint32]bool | 108 | + | pointer WindowID |
| 109 | + | grab WindowID | |||
| 110 | + | buttons map[uint32]bool | |||
| 109 | // drag is the pointer drag in progress (drag.go); swallow holds the | 111 | // drag is the pointer drag in progress (drag.go); swallow holds the | ||
| 110 | // buttons whose release no client must see. mods are the modifiers | 112 | // buttons whose release no client must see. mods are the modifiers | ||
| 111 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | 113 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | ||
| @@ -144,7 +146,11 @@ type Core struct { | |||||
| 144 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | 146 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | ||
| 145 | popups map[WindowID]*popupState | 147 | popups map[WindowID]*popupState | ||
| 146 | popupOrder []WindowID | 148 | popupOrder []WindowID | ||
| 147 | - | keyboard keyboard | 149 | + | // unmanaged are the mapped override-redirect windows, unmanagedOrder |
| 150 | + | // stacks them, bottom to top (unmanaged.go). | |||
| 151 | + | unmanaged map[WindowID]*unmanagedState | |||
| 152 | + | unmanagedOrder []WindowID | |||
| 153 | + | keyboard keyboard | |||
| 148 | // activity is when core last told wayland about user input | 154 | // activity is when core last told wayland about user input | ||
| 149 | // (ports.UserActivity), sent at most once per ActivityInterval. | 155 | // (ports.UserActivity), sent at most once per ActivityInterval. | ||
| 150 | activity time.Time | 156 | activity time.Time | ||
| @@ -417,7 +423,7 @@ func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) { | |||||
| 417 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | 423 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | ||
| 418 | } | 424 | } | ||
| 419 | // A placeholder screen holds windows until the first output arrives. | 425 | // A placeholder screen holds windows until the first output arrives. | ||
| 420 | - | c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), ping: make(chan chan struct{}), out: newOutbox()} | 426 | + | c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, unmanaged: map[WindowID]*unmanagedState{}, unmanagedOrder: make([]WindowID, 0, 32), ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), placed: map[WindowID]Rect{}, ping: make(chan chan struct{}), out: newOutbox()} |
| 421 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | 427 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | ||
| 422 | if err := c.apply(cfg); err != nil { | 428 | if err := c.apply(cfg); err != nil { | ||
| 423 | return nil, err | 429 | return nil, err | ||
| @@ -994,6 +1000,11 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 994 | } | 1000 | } | ||
| 995 | sc := c.screens[i] | 1001 | sc := c.screens[i] | ||
| 996 | lx, ly := x-float64(sc.x), y-float64(sc.y) | 1002 | lx, ly := x-float64(sc.x), y-float64(sc.y) | ||
| 1003 | + | // Unmanaged windows are over everything, popups included; a locked | |||
| 1004 | + | // pointer keeps its focus (a mouse-look game). | |||
| 1005 | + | if id, px, py := c.unmanagedAt(sc, lx, ly); id != 0 { | |||
| 1006 | + | return id, px, py | |||
| 1007 | + | } | |||
| 997 | // Layer popups are over everything; window popups are over the windows | 1008 | // Layer popups are over everything; window popups are over the windows | ||
| 998 | // only, under the top and overlay layers (as drawn). | 1009 | // only, under the top and overlay layers (as drawn). | ||
| 999 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | 1010 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | ||
M internal/core/core_test.go +4 -0
| @@ -38,6 +38,10 @@ func command(t *testing.T, ch <-chan ports.ClientCommand) ports.ClientCommand { | |||||
| 38 | for { | 38 | for { | ||
| 39 | switch v := receive(t, ch); v.(type) { | 39 | switch v := receive(t, ch); v.(type) { | ||
| 40 | case nil, ports.SetOutputs, ports.UserActivity: | 40 | case nil, ports.SetOutputs, ports.UserActivity: | ||
| 41 | + | case ports.PlaceWindow: | |||
| 42 | + | // Only Positioned (X11) windows are placed; every scenario | |||
| 43 | + | // using this helper maps xdg windows. | |||
| 44 | + | t.Fatalf("unexpected %+v", v) | |||
| 41 | default: | 45 | default: | ||
| 42 | return v | 46 | return v | ||
| 43 | } | 47 | } | ||
M internal/core/datadrag.go +1 -1
| @@ -101,7 +101,7 @@ func dataDragHitChanged(ev ports.ClientEvent) bool { | |||||
| 101 | case ports.LayerChanged, ports.InputRegionChanged, ports.WindowMapped, | 101 | case ports.LayerChanged, ports.InputRegionChanged, ports.WindowMapped, | ||
| 102 | ports.WindowResized, ports.PopupRequest, ports.PopupMapped, | 102 | ports.WindowResized, ports.PopupRequest, ports.PopupMapped, | ||
| 103 | ports.OutputPower, ports.WindowParent, ports.WindowModal, | 103 | ports.OutputPower, ports.WindowParent, ports.WindowModal, | ||
| 104 | - | ports.WindowUnmapped, ports.PointerWarp, ports.WindowMoveRequest, | 104 | + | ports.WindowUnmapped, ports.UnmanagedMapped, ports.UnmanagedMoved, ports.UnmanagedUnmapped, ports.PointerWarp, ports.WindowMoveRequest, |
| 105 | ports.WindowFullscreenRequest, ports.WorkspaceActivate, ports.WindowActivate: | 105 | ports.WindowFullscreenRequest, ports.WorkspaceActivate, ports.WindowActivate: | ||
| 106 | return true | 106 | return true | ||
| 107 | default: | 107 | default: | ||
A internal/core/place_test.go +80 -0
| @@ -0,0 +1,80 @@ | |||||
| 1 | + | package core_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/core" | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // places collects the PlaceWindow commands queued so far. | |||
| 12 | + | func places(commands chan ports.ClientCommand) (out []ports.PlaceWindow) { | |||
| 13 | + | for len(commands) > 0 { | |||
| 14 | + | if v, ok := (<-commands).(ports.PlaceWindow); ok { | |||
| 15 | + | out = append(out, v) | |||
| 16 | + | } | |||
| 17 | + | } | |||
| 18 | + | return out | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func TestPlaceWindow(t *testing.T) { | |||
| 22 | + | cfg := altCmdDefaults() | |||
| 23 | + | cfg.Border.Width = 0 | |||
| 24 | + | cfg.Layout.Gaps = 8 | |||
| 25 | + | client := make(chan ports.ClientEvent, 16) | |||
| 26 | + | input := make(chan ports.InputEvent, 16) | |||
| 27 | + | output := make(chan ports.OutputEvent, 8) | |||
| 28 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 29 | + | scenes := make(chan []ports.Scene, 1) | |||
| 30 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |||
| 31 | + | if err != nil { | |||
| 32 | + | t.Fatal(err) | |||
| 33 | + | } | |||
| 34 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 35 | + | defer cancel() | |||
| 36 | + | go func() { _ = c.Run(ctx) }() | |||
| 37 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 38 | + | scene(t, scenes) | |||
| 39 | + | ||||
| 40 | + | // An xdg window is never placed. | |||
| 41 | + | client <- ports.WindowMapped{ID: 1} | |||
| 42 | + | scene(t, scenes) | |||
| 43 | + | if got := places(commands); len(got) != 0 { | |||
| 44 | + | t.Fatal(got) | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // A positioned window is placed once on map. | |||
| 48 | + | client <- ports.WindowMapped{ID: 2, Positioned: true} | |||
| 49 | + | scene(t, scenes) | |||
| 50 | + | first := places(commands) | |||
| 51 | + | if len(first) != 1 || first[0].ID != 2 { | |||
| 52 | + | t.Fatal(first) | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // The same layout sends nothing more. | |||
| 56 | + | client <- ports.WindowMapped{ID: 1} | |||
| 57 | + | scene(t, scenes) | |||
| 58 | + | if got := places(commands); len(got) != 0 { | |||
| 59 | + | t.Fatal(got) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // A position change sends it again, for the positioned window only. | |||
| 63 | + | client <- ports.WindowMapped{ID: 3} | |||
| 64 | + | scene(t, scenes) | |||
| 65 | + | moved := places(commands) | |||
| 66 | + | if len(moved) != 1 || moved[0].ID != 2 || (moved[0].X == first[0].X && moved[0].Y == first[0].Y) { | |||
| 67 | + | t.Fatal(first, moved) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // Unmap prunes it: mapping again places from nothing. | |||
| 71 | + | client <- ports.WindowUnmapped{ID: 2} | |||
| 72 | + | scene(t, scenes) | |||
| 73 | + | places(commands) | |||
| 74 | + | client <- ports.WindowMapped{ID: 2, Positioned: true} | |||
| 75 | + | scene(t, scenes) | |||
| 76 | + | again := places(commands) | |||
| 77 | + | if len(again) != 1 || again[0].ID != 2 { | |||
| 78 | + | t.Fatal(again) | |||
| 79 | + | } | |||
| 80 | + | } | |||
M internal/core/scene.go +12 -1
| @@ -20,7 +20,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 20 | // layout is what is drawn; settled (same indexes) is where the | 20 | // layout is what is drawn; settled (same indexes) is where the | ||
| 21 | // windows are going, and alone sizes the configures. | 21 | // windows are going, and alone sizes the configures. | ||
| 22 | layout, settled := sc.shown, sc.settledLayout | 22 | layout, settled := sc.shown, sc.settledLayout | ||
| 23 | - | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Radius: c.cfg.Border.Radius, Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)} | 23 | + | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Radius: c.cfg.Border.Radius, Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+len(c.unmanagedOrder)+1), Layers: shownLayers(sc)} |
| 24 | if !sc.mon.previewing() { | 24 | if !sc.mon.previewing() { | ||
| 25 | // Tiles stop at the panels; the overview lays out previews over | 25 | // Tiles stop at the panels; the overview lays out previews over | ||
| 26 | // the whole output. | 26 | // the whole output. | ||
| @@ -116,8 +116,19 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 116 | } | 116 | } | ||
| 117 | c.configures.mark(v) | 117 | c.configures.mark(v) | ||
| 118 | } | 118 | } | ||
| 119 | + | if !p.Hidden && p.Preview == 0 && c.windows.lookup(p.ID).client.Positioned { | |||
| 120 | + | cr := c.clientRect(ps) | |||
| 121 | + | at := Rect{X: sc.x + cr.X, Y: sc.y + cr.Y} | |||
| 122 | + | if old, ok := c.placed[p.ID]; !ok || old != at { | |||
| 123 | + | if err := c.command(ctx, ports.PlaceWindow{ID: p.ID, X: at.X, Y: at.Y}); err != nil { | |||
| 124 | + | return ports.Scene{}, err | |||
| 125 | + | } | |||
| 126 | + | c.placed[p.ID] = at | |||
| 127 | + | } | |||
| 128 | + | } | |||
| 119 | } | 129 | } | ||
| 120 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | 130 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | ||
| 131 | + | scene.Windows = c.sceneUnmanaged(scene.Windows, sc) | |||
| 121 | if icon, ok := c.dataDragIconOn(i, sc); ok { | 132 | if icon, ok := c.dataDragIconOn(i, sc); ok { | ||
| 122 | // The drag icon is over everything, popups and layers included. | 133 | // The drag icon is over everything, popups and layers included. | ||
| 123 | scene.Windows = append(scene.Windows, icon) | 134 | scene.Windows = append(scene.Windows, icon) | ||
M internal/core/transition.go +1 -0
| @@ -453,6 +453,7 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) { | |||||
| 453 | // The window is unmapped: a toplevel that maps again keeps its ID and | 453 | // The window is unmapped: a toplevel that maps again keeps its ID and | ||
| 454 | // must be configured from nothing (a leaving entry keeps no configure). | 454 | // must be configured from nothing (a leaving entry keeps no configure). | ||
| 455 | c.configures.forget(v.ID) | 455 | c.configures.forget(v.ID) | ||
| 456 | + | delete(c.placed, v.ID) | |||
| 456 | sc, w := c.screenOf(v.ID) | 457 | sc, w := c.screenOf(v.ID) | ||
| 457 | if sc == nil { | 458 | if sc == nil { | ||
| 458 | return | 459 | return | ||
A internal/core/unmanaged.go +117 -0
| @@ -0,0 +1,117 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "slices" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // unmanagedState is a mapped override-redirect X11 window: an absolute layout | |||
| 11 | + | // rect chosen by the client, no parent, never keyboard-focused. rect is in | |||
| 12 | + | // global logical coordinates. | |||
| 13 | + | type unmanagedState struct { | |||
| 14 | + | id WindowID | |||
| 15 | + | rect Rect | |||
| 16 | + | noInput bool | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // unmanagedEvent applies an unmanaged window event. changed is true when the | |||
| 20 | + | // scene or the pointer target may differ. | |||
| 21 | + | func (c *Core) unmanagedEvent(ctx context.Context, ev ports.ClientEvent) (changed bool, err error) { | |||
| 22 | + | switch v := ev.(type) { | |||
| 23 | + | case ports.UnmanagedMapped: | |||
| 24 | + | r := Rect{X: v.X, Y: v.Y, W: v.Width, H: v.Height} | |||
| 25 | + | if u := c.unmanaged[v.ID]; u != nil { | |||
| 26 | + | if u.rect == r && u.noInput == v.NoInput { | |||
| 27 | + | return false, nil | |||
| 28 | + | } | |||
| 29 | + | u.rect, u.noInput = r, v.NoInput | |||
| 30 | + | } else { | |||
| 31 | + | c.unmanaged[v.ID] = &unmanagedState{id: v.ID, rect: r, noInput: v.NoInput} | |||
| 32 | + | c.unmanagedOrder = append(c.unmanagedOrder, v.ID) | |||
| 33 | + | } | |||
| 34 | + | case ports.UnmanagedMoved: | |||
| 35 | + | u := c.unmanaged[v.ID] | |||
| 36 | + | r := Rect{X: v.X, Y: v.Y, W: v.Width, H: v.Height} | |||
| 37 | + | if u == nil || u.rect == r { | |||
| 38 | + | return false, nil | |||
| 39 | + | } | |||
| 40 | + | u.rect = r | |||
| 41 | + | case ports.UnmanagedUnmapped: | |||
| 42 | + | if c.unmanaged[v.ID] == nil { | |||
| 43 | + | return false, nil | |||
| 44 | + | } | |||
| 45 | + | delete(c.unmanaged, v.ID) | |||
| 46 | + | c.unmanagedOrder = slices.DeleteFunc(c.unmanagedOrder, func(id WindowID) bool { return id == v.ID }) | |||
| 47 | + | c.windows.drop(v.ID) | |||
| 48 | + | if err := c.dataDragGone(ctx, v.ID); err != nil { | |||
| 49 | + | return false, err | |||
| 50 | + | } | |||
| 51 | + | // rehit does nothing while a button grab or drag is held: do not | |||
| 52 | + | // leave the pointer and grab on the dead window. | |||
| 53 | + | if c.grab == v.ID { | |||
| 54 | + | c.grab = 0 | |||
| 55 | + | } | |||
| 56 | + | if c.pointer == v.ID { | |||
| 57 | + | c.pointer = 0 | |||
| 58 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |||
| 59 | + | return false, err | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | // The window under a still pointer may have changed. | |||
| 64 | + | return true, c.rehit(ctx) | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // unmanagedScreen is the screen showing u: the one containing its origin, or | |||
| 68 | + | // the one the pointer clamp would pick when the origin is in a hole. | |||
| 69 | + | func (c *Core) unmanagedScreen(u *unmanagedState) *screen { | |||
| 70 | + | x, y := float64(u.rect.X), float64(u.rect.Y) | |||
| 71 | + | i := c.screenAt(x, y) | |||
| 72 | + | if i < 0 { | |||
| 73 | + | cx, cy := c.clampPointer(x, y, x, y) | |||
| 74 | + | i = c.screenAt(cx, cy) | |||
| 75 | + | } | |||
| 76 | + | if i < 0 { | |||
| 77 | + | return nil | |||
| 78 | + | } | |||
| 79 | + | return c.screens[i] | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // sceneUnmanaged appends the unmanaged windows shown on sc, bottom to top, | |||
| 83 | + | // drawn as popups over the layers. They are hidden while sc previews. | |||
| 84 | + | func (c *Core) sceneUnmanaged(out []ports.SceneWindow, sc *screen) []ports.SceneWindow { | |||
| 85 | + | if len(c.unmanagedOrder) == 0 || sc.mon.previewing() { | |||
| 86 | + | return out | |||
| 87 | + | } | |||
| 88 | + | for _, id := range c.unmanagedOrder { | |||
| 89 | + | u := c.unmanaged[id] | |||
| 90 | + | if u == nil || c.unmanagedScreen(u) != sc { | |||
| 91 | + | continue | |||
| 92 | + | } | |||
| 93 | + | r := u.rect | |||
| 94 | + | r.X, r.Y = r.X-sc.x, r.Y-sc.y | |||
| 95 | + | out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: true}) | |||
| 96 | + | } | |||
| 97 | + | return out | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // unmanagedAt is the topmost unmanaged window taking the pointer at the | |||
| 101 | + | // screen-local point. None while the pointer is locked or the screen previews. | |||
| 102 | + | func (c *Core) unmanagedAt(sc *screen, lx, ly float64) (WindowID, float64, float64) { | |||
| 103 | + | if len(c.unmanagedOrder) == 0 || c.constraint.Mode == ports.ConstraintLock || sc.mon.previewing() { | |||
| 104 | + | return 0, 0, 0 | |||
| 105 | + | } | |||
| 106 | + | for i := len(c.unmanagedOrder) - 1; i >= 0; i-- { | |||
| 107 | + | u := c.unmanaged[c.unmanagedOrder[i]] | |||
| 108 | + | if u == nil || u.noInput || c.unmanagedScreen(u) != sc { | |||
| 109 | + | continue | |||
| 110 | + | } | |||
| 111 | + | x, y := lx-float64(u.rect.X-sc.x), ly-float64(u.rect.Y-sc.y) | |||
| 112 | + | if x >= 0 && x < float64(u.rect.W) && y >= 0 && y < float64(u.rect.H) && c.acceptsInput(u.id, x, y) { | |||
| 113 | + | return u.id, x, y | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | return 0, 0, 0 | |||
| 117 | + | } | |||
A internal/core/unmanaged_test.go +145 -0
| @@ -0,0 +1,145 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func unmanagedScene(t *testing.T, c *Core, i int) ports.Scene { | |||
| 11 | + | t.Helper() | |||
| 12 | + | s, err := c.sceneFor(context.Background(), i, c.screens[i], nil, nil) | |||
| 13 | + | if err != nil { | |||
| 14 | + | t.Fatal(err) | |||
| 15 | + | } | |||
| 16 | + | return s | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | func mapUnmanaged(t *testing.T, c *Core, id WindowID, x, y, w, h int, noInput bool) { | |||
| 20 | + | t.Helper() | |||
| 21 | + | c.clientEvent(context.Background(), ports.UnmanagedMapped{ID: id, X: x, Y: y, Width: w, Height: h, NoInput: noInput}) | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func TestUnmanagedScene(t *testing.T) { | |||
| 25 | + | c := publishRig(t, 2) | |||
| 26 | + | mapUnmanaged(t, c, 100, 310, 20, 50, 40, false) | |||
| 27 | + | mapUnmanaged(t, c, 101, 5, 5, 10, 10, true) | |||
| 28 | + | b := unmanagedScene(t, c, 1) | |||
| 29 | + | last := b.Windows[len(b.Windows)-1] | |||
| 30 | + | if last.ID != 100 || last.Rect != (Rect{X: 10, Y: 20, W: 50, H: 40}) || !last.Popup || !last.OverLayers { | |||
| 31 | + | t.Fatalf("screen B top window %+v", last) | |||
| 32 | + | } | |||
| 33 | + | a := unmanagedScene(t, c, 0) | |||
| 34 | + | if got := a.Windows[len(a.Windows)-1]; got.ID != 101 || got.Rect.X != 5 { | |||
| 35 | + | t.Fatalf("screen A top window %+v", got) | |||
| 36 | + | } | |||
| 37 | + | c.clientEvent(context.Background(), ports.UnmanagedMoved{ID: 100, X: 20, Y: 20, Width: 50, Height: 40}) | |||
| 38 | + | if got := unmanagedScene(t, c, 0).Windows; got[len(got)-2].ID != 100 || got[len(got)-2].Rect.X != 20 { | |||
| 39 | + | t.Fatalf("moved window not on screen A: %+v", got) | |||
| 40 | + | } | |||
| 41 | + | c.clientEvent(context.Background(), ports.UnmanagedUnmapped{ID: 100}) | |||
| 42 | + | c.clientEvent(context.Background(), ports.UnmanagedUnmapped{ID: 101}) | |||
| 43 | + | if len(c.unmanagedOrder) != 0 || len(c.unmanaged) != 0 { | |||
| 44 | + | t.Fatal("state left after unmap") | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func TestUnmanagedHiddenWhilePreviewing(t *testing.T) { | |||
| 49 | + | c := publishRig(t, 1) | |||
| 50 | + | mapUnmanaged(t, c, 100, 10, 10, 50, 40, false) | |||
| 51 | + | c.cur().mon.ov.open = true | |||
| 52 | + | if !c.cur().mon.previewing() { | |||
| 53 | + | t.Fatal("setup: not previewing") | |||
| 54 | + | } | |||
| 55 | + | for _, w := range unmanagedScene(t, c, 0).Windows { | |||
| 56 | + | if w.ID == 100 { | |||
| 57 | + | t.Fatal("unmanaged window drawn in the overview") | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | if id, _, _ := c.hit(20, 20); id == 100 { | |||
| 61 | + | t.Fatal("unmanaged window hit in the overview") | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func TestUnmanagedHit(t *testing.T) { | |||
| 66 | + | c := publishRig(t, 2) | |||
| 67 | + | mapUnmanaged(t, c, 100, 310, 20, 50, 40, false) | |||
| 68 | + | mapUnmanaged(t, c, 101, 320, 30, 10, 10, false) | |||
| 69 | + | mapUnmanaged(t, c, 102, 340, 30, 10, 10, true) | |||
| 70 | + | if id, x, y := c.hit(325, 35); id != 101 || x != 5 || y != 5 { | |||
| 71 | + | t.Fatalf("topmost: %d %v %v", id, x, y) | |||
| 72 | + | } | |||
| 73 | + | if id, _, _ := c.hit(345, 35); id != 100 { | |||
| 74 | + | t.Fatalf("noInput must be skipped: %d", id) | |||
| 75 | + | } | |||
| 76 | + | c.windows.setRegion(ports.InputRegionChanged{ID: 100, Rects: []Rect{{X: 0, Y: 0, W: 5, H: 5}}}) | |||
| 77 | + | if id, _, _ := c.hit(345, 35); id == 100 { | |||
| 78 | + | t.Fatal("outside the input region") | |||
| 79 | + | } | |||
| 80 | + | c.constraint.Mode = ports.ConstraintLock | |||
| 81 | + | if id, _, _ := c.hit(325, 35); id == 101 { | |||
| 82 | + | t.Fatal("locked pointer must not move to an unmanaged window") | |||
| 83 | + | } | |||
| 84 | + | c.constraint.Mode = 0 | |||
| 85 | + | c.security.Protected = true | |||
| 86 | + | if id, _, _ := c.hit(325, 35); id == 101 { | |||
| 87 | + | t.Fatal("protected session hit an unmanaged window") | |||
| 88 | + | } | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | func unmanagedRig(t testing.TB, n int) *Core { | |||
| 92 | + | c := publishRig(t, 1) | |||
| 93 | + | for i := range n { | |||
| 94 | + | c.clientEvent(context.Background(), ports.UnmanagedMapped{ID: WindowID(100 + i), X: 10 * i, Y: 10, Width: 30, Height: 30}) | |||
| 95 | + | } | |||
| 96 | + | return c | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func publishOnce(t testing.TB, c *Core) { | |||
| 100 | + | if err := c.publish(context.Background()); err != nil { | |||
| 101 | + | t.Fatal(err) | |||
| 102 | + | } | |||
| 103 | + | <-c.ch.Scenes | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // TestUnmanagedPublishAllocations: three unmanaged windows cost no more than | |||
| 107 | + | // none (the scene window slice is presized for them). | |||
| 108 | + | func TestUnmanagedPublishAllocations(t *testing.T) { | |||
| 109 | + | base, with := unmanagedRig(t, 0), unmanagedRig(t, 3) | |||
| 110 | + | publishOnce(t, base) | |||
| 111 | + | publishOnce(t, with) | |||
| 112 | + | a := testing.AllocsPerRun(50, func() { publishOnce(t, base) }) | |||
| 113 | + | b := testing.AllocsPerRun(50, func() { publishOnce(t, with) }) | |||
| 114 | + | if b > a { | |||
| 115 | + | t.Fatalf("publish allocs with unmanaged = %v, without = %v", b, a) | |||
| 116 | + | } | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | func TestUnmanagedHitAllocations(t *testing.T) { | |||
| 120 | + | c := unmanagedRig(t, 3) | |||
| 121 | + | if n := testing.AllocsPerRun(100, func() { c.hit(25, 20); c.hit(250, 150) }); n != 0 { | |||
| 122 | + | t.Fatalf("hit allocs = %v", n) | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func TestUnmanagedUnmapDuringGrabClearsPointer(t *testing.T) { | |||
| 127 | + | c := publishRig(t, 1) | |||
| 128 | + | mapUnmanaged(t, c, 100, 10, 10, 50, 40, false) | |||
| 129 | + | cmds := make(chan ports.ClientCommand, 64) | |||
| 130 | + | c.ch.Commands = cmds | |||
| 131 | + | c.pointer, c.grab = 100, 100 | |||
| 132 | + | c.clientEvent(context.Background(), ports.UnmanagedUnmapped{ID: 100}) | |||
| 133 | + | if c.pointer == 100 || c.grab != 0 { | |||
| 134 | + | t.Fatalf("pointer %d grab %d left on the dead window", c.pointer, c.grab) | |||
| 135 | + | } | |||
| 136 | + | var focus bool | |||
| 137 | + | for len(cmds) > 0 { | |||
| 138 | + | if f, ok := (<-cmds).(ports.PointerFocus); ok && f == (ports.PointerFocus{}) { | |||
| 139 | + | focus = true | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | if !focus { | |||
| 143 | + | t.Fatal("pointer focus not reset") | |||
| 144 | + | } | |||
| 145 | + | } | |||
M internal/ports/client_commands.go +10 -0
| @@ -47,6 +47,16 @@ type ConfigureWindow struct { | |||||
| 47 | 47 | ||||
| 48 | func (ConfigureWindow) clientCommand() {} | 48 | func (ConfigureWindow) clientCommand() {} | ||
| 49 | 49 | ||||
| 50 | + | // PlaceWindow carries core → wayland the position of a Positioned window's | |||
| 51 | + | // client rect, in layout logical coordinates (output origin included). It | |||
| 52 | + | // follows the ConfigureWindow of a frame and is sent once per change. | |||
| 53 | + | type PlaceWindow struct { | |||
| 54 | + | ID WindowID | |||
| 55 | + | X, Y int | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func (PlaceWindow) clientCommand() {} | |||
| 59 | + | ||||
| 50 | // OutputPlacement is one output in the global layout. X, Y, Width and | 60 | // OutputPlacement is one output in the global layout. X, Y, Width and | ||
| 51 | // Height are logical (after Transform: they swap for 90/270); | 61 | // Height are logical (after Transform: they swap for 90/270); | ||
| 52 | // physical = logical × Scale. Info.Width and Info.Height are the physical | 62 | // physical = logical × Scale. Info.Width and Info.Height are the physical | ||
M internal/ports/client_events.go +28 -0
| @@ -20,6 +20,9 @@ type WindowMapped struct { | |||||
| 20 | // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over | 20 | // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over | ||
| 21 | // it. | 21 | // it. | ||
| 22 | Parent WindowID | 22 | Parent WindowID | ||
| 23 | + | // Positioned windows are placed by core: it sends PlaceWindow with the | |||
| 24 | + | // client rect origin. Set only by the X11 host, false for xdg. | |||
| 25 | + | Positioned bool | |||
| 23 | // Remap is set when the toplevel mapped before and was unmapped (a tray | 26 | // Remap is set when the toplevel mapped before and was unmapped (a tray | ||
| 24 | // app restored): window rules only place its first map. | 27 | // app restored): window rules only place its first map. | ||
| 25 | Remap bool | 28 | Remap bool | ||
| @@ -49,6 +52,31 @@ type WindowUnmapped struct{ ID WindowID } | |||||
| 49 | 52 | ||||
| 50 | func (WindowUnmapped) clientEvent() {} | 53 | func (WindowUnmapped) clientEvent() {} | ||
| 51 | 54 | ||||
| 55 | + | // UnmanagedMapped carries wayland → core that an override-redirect X11 window | |||
| 56 | + | // shows. The rect is its place in the layout (logical coordinates). NoInput | |||
| 57 | + | // windows (tooltips, drag icons, notifications) never take the pointer. | |||
| 58 | + | type UnmanagedMapped struct { | |||
| 59 | + | ID WindowID | |||
| 60 | + | X, Y, Width, Height int | |||
| 61 | + | NoInput bool | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | func (UnmanagedMapped) clientEvent() {} | |||
| 65 | + | ||||
| 66 | + | // UnmanagedMoved carries wayland → core a move and/or resize of a mapped | |||
| 67 | + | // unmanaged window. | |||
| 68 | + | type UnmanagedMoved struct { | |||
| 69 | + | ID WindowID | |||
| 70 | + | X, Y, Width, Height int | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func (UnmanagedMoved) clientEvent() {} | |||
| 74 | + | ||||
| 75 | + | // UnmanagedUnmapped carries wayland → core that an unmanaged window is gone. | |||
| 76 | + | type UnmanagedUnmapped struct{ ID WindowID } | |||
| 77 | + | ||||
| 78 | + | func (UnmanagedUnmapped) clientEvent() {} | |||
| 79 | + | ||||
| 52 | // ShortcutsInhibit carries wayland → core that a window asks for (Active) | 80 | // ShortcutsInhibit carries wayland → core that a window asks for (Active) | ||
| 53 | // or stops asking for all keys, compositor binds included, while it has | 81 | // or stops asking for all keys, compositor binds included, while it has | ||
| 54 | // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1). | 82 | // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1). | ||
M internal/ports/config.go +17 -3
| @@ -101,9 +101,8 @@ type Config struct { | |||||
| 101 | Command []string | 101 | Command []string | ||
| 102 | AutoOpen string | 102 | AutoOpen string | ||
| 103 | } | 103 | } | ||
| 104 | - | // Xwayland is the xwayland-satellite binary serving X11 clients; empty | 104 | + | // Xwayland selects the X11 launcher. |
| 105 | - | // disables X11. | 105 | + | Xwayland XwaylandConfig |
| 106 | - | Xwayland string | |||
| 107 | // Startup are commands run once when the session starts, in order. | 106 | // Startup are commands run once when the session starts, in order. | ||
| 108 | // One startup line may hold several, split by SplitCommands. | 107 | // One startup line may hold several, split by SplitCommands. | ||
| 109 | Startup [][]string | 108 | Startup [][]string | ||
| @@ -298,3 +297,18 @@ type WindowRule struct { | |||||
| 298 | // (1 to 16; 0 leaves it automatic). | 297 | // (1 to 16; 0 leaves it automatic). | ||
| 299 | Cells int | 298 | Cells int | ||
| 300 | } | 299 | } | ||
| 300 | + | ||||
| 301 | + | // XwaylandBackend selects how X11 clients are served. | |||
| 302 | + | type XwaylandBackend uint8 | |||
| 303 | + | ||||
| 304 | + | const ( | |||
| 305 | + | XwaylandOff XwaylandBackend = iota | |||
| 306 | + | XwaylandSatellite | |||
| 307 | + | XwaylandBuiltin | |||
| 308 | + | ) | |||
| 309 | + | ||||
| 310 | + | // XwaylandConfig selects the launcher and its executable. | |||
| 311 | + | type XwaylandConfig struct { | |||
| 312 | + | Backend XwaylandBackend | |||
| 313 | + | Binary string | |||
| 314 | + | } | |||