Merge branch 'feat/x7-core' into feat/x7
8 files · +278 -5
M Makefile +1 -1
| @@ -70,7 +70,7 @@ adapter-imports: | |||
| 70 | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 71 | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 72 | 72 | perf-check: | |
| 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)$$' -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)$$' -count=1 | |
| 74 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |
| 75 | 75 | check: vet test arch fakes-check log-check perf-check | |
| 76 | 76 | ||
| @@ -70,7 +70,7 @@ adapter-imports: | |||||
| 70 | [ -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; } | ||
| 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 72 | perf-check: | 72 | perf-check: | ||
| 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)$$' -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)$$' -count=1 |
| 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | ||
| 75 | check: vet test arch fakes-check log-check perf-check | 75 | check: vet test arch fakes-check log-check perf-check | ||
| 76 | 76 | ||||
M internal/core/client.go +8 -0
| @@ -70,6 +70,14 @@ func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish, | |||
| 70 | 70 | if _, w := c.screenOf(v.ID); w != nil { | |
| 71 | 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 | 81 | case ports.WindowUnmapped: | |
| 74 | 82 | if c.unmapClient(ctx, v) != nil { | |
| 75 | 83 | return false, true | |
| @@ -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 +11 -2
| @@ -146,7 +146,11 @@ type Core struct { | |||
| 146 | 146 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | |
| 147 | 147 | popups map[WindowID]*popupState | |
| 148 | 148 | popupOrder []WindowID | |
| 149 | - | 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 | |
| 150 | 154 | // activity is when core last told wayland about user input | |
| 151 | 155 | // (ports.UserActivity), sent at most once per ActivityInterval. | |
| 152 | 156 | activity time.Time | |
| @@ -419,7 +423,7 @@ func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) { | |||
| 419 | 423 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | |
| 420 | 424 | } | |
| 421 | 425 | // A placeholder screen holds windows until the first output arrives. | |
| 422 | - | 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(), placed: map[WindowID]Rect{}, 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()} | |
| 423 | 427 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | |
| 424 | 428 | if err := c.apply(cfg); err != nil { | |
| 425 | 429 | return nil, err | |
| @@ -996,6 +1000,11 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 996 | 1000 | } | |
| 997 | 1001 | sc := c.screens[i] | |
| 998 | 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 | + | } | |
| 999 | 1008 | // Layer popups are over everything; window popups are over the windows | |
| 1000 | 1009 | // only, under the top and overlay layers (as drawn). | |
| 1001 | 1010 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | |
| @@ -146,7 +146,11 @@ type Core struct { | |||||
| 146 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | 146 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | ||
| 147 | popups map[WindowID]*popupState | 147 | popups map[WindowID]*popupState | ||
| 148 | popupOrder []WindowID | 148 | popupOrder []WindowID | ||
| 149 | - | 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 | |||
| 150 | // activity is when core last told wayland about user input | 154 | // activity is when core last told wayland about user input | ||
| 151 | // (ports.UserActivity), sent at most once per ActivityInterval. | 155 | // (ports.UserActivity), sent at most once per ActivityInterval. | ||
| 152 | activity time.Time | 156 | activity time.Time | ||
| @@ -419,7 +423,7 @@ func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) { | |||||
| 419 | 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") | ||
| 420 | } | 424 | } | ||
| 421 | // A placeholder screen holds windows until the first output arrives. | 425 | // A placeholder screen holds windows until the first output arrives. | ||
| 422 | - | 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(), placed: map[WindowID]Rect{}, 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()} |
| 423 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | 427 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | ||
| 424 | if err := c.apply(cfg); err != nil { | 428 | if err := c.apply(cfg); err != nil { | ||
| 425 | return nil, err | 429 | return nil, err | ||
| @@ -996,6 +1000,11 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 996 | } | 1000 | } | ||
| 997 | sc := c.screens[i] | 1001 | sc := c.screens[i] | ||
| 998 | 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 | + | } | |||
| 999 | // Layer popups are over everything; window popups are over the windows | 1008 | // Layer popups are over everything; window popups are over the windows | ||
| 1000 | // only, under the top and overlay layers (as drawn). | 1009 | // only, under the top and overlay layers (as drawn). | ||
| 1001 | 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/datadrag.go +1 -1
| @@ -101,7 +101,7 @@ func dataDragHitChanged(ev ports.ClientEvent) bool { | |||
| 101 | 101 | case ports.LayerChanged, ports.InputRegionChanged, ports.WindowMapped, | |
| 102 | 102 | ports.WindowResized, ports.PopupRequest, ports.PopupMapped, | |
| 103 | 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 | 105 | ports.WindowFullscreenRequest, ports.WorkspaceActivate, ports.WindowActivate: | |
| 106 | 106 | return true | |
| 107 | 107 | default: | |
| @@ -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: | ||
M internal/core/scene.go +2 -1
| @@ -20,7 +20,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 20 | 20 | // layout is what is drawn; settled (same indexes) is where the | |
| 21 | 21 | // windows are going, and alone sizes the configures. | |
| 22 | 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 | 24 | if !sc.mon.previewing() { | |
| 25 | 25 | // Tiles stop at the panels; the overview lays out previews over | |
| 26 | 26 | // the whole output. | |
| @@ -128,6 +128,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 128 | 128 | } | |
| 129 | 129 | } | |
| 130 | 130 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | |
| 131 | + | scene.Windows = c.sceneUnmanaged(scene.Windows, sc) | |
| 131 | 132 | if icon, ok := c.dataDragIconOn(i, sc); ok { | |
| 132 | 133 | // The drag icon is over everything, popups and layers included. | |
| 133 | 134 | scene.Windows = append(scene.Windows, icon) | |
| @@ -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. | ||
| @@ -128,6 +128,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 128 | } | 128 | } | ||
| 129 | } | 129 | } | ||
| 130 | 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) | |||
| 131 | if icon, ok := c.dataDragIconOn(i, sc); ok { | 132 | if icon, ok := c.dataDragIconOn(i, sc); ok { | ||
| 132 | // The drag icon is over everything, popups and layers included. | 133 | // The drag icon is over everything, popups and layers included. | ||
| 133 | scene.Windows = append(scene.Windows, icon) | 134 | scene.Windows = append(scene.Windows, icon) | ||
A internal/core/unmanaged.go +106 -0
| @@ -0,0 +1,106 @@ | |||
| 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 | + | } | |
| 52 | + | // The window under a still pointer may have changed. | |
| 53 | + | return true, c.rehit(ctx) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // unmanagedScreen is the screen showing u: the one containing its origin, or | |
| 57 | + | // the one the pointer clamp would pick when the origin is in a hole. | |
| 58 | + | func (c *Core) unmanagedScreen(u *unmanagedState) *screen { | |
| 59 | + | x, y := float64(u.rect.X), float64(u.rect.Y) | |
| 60 | + | i := c.screenAt(x, y) | |
| 61 | + | if i < 0 { | |
| 62 | + | cx, cy := c.clampPointer(x, y, x, y) | |
| 63 | + | i = c.screenAt(cx, cy) | |
| 64 | + | } | |
| 65 | + | if i < 0 { | |
| 66 | + | return nil | |
| 67 | + | } | |
| 68 | + | return c.screens[i] | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // sceneUnmanaged appends the unmanaged windows shown on sc, bottom to top, | |
| 72 | + | // drawn as popups over the layers. They are hidden while sc previews. | |
| 73 | + | func (c *Core) sceneUnmanaged(out []ports.SceneWindow, sc *screen) []ports.SceneWindow { | |
| 74 | + | if len(c.unmanagedOrder) == 0 || sc.mon.previewing() { | |
| 75 | + | return out | |
| 76 | + | } | |
| 77 | + | for _, id := range c.unmanagedOrder { | |
| 78 | + | u := c.unmanaged[id] | |
| 79 | + | if u == nil || c.unmanagedScreen(u) != sc { | |
| 80 | + | continue | |
| 81 | + | } | |
| 82 | + | r := u.rect | |
| 83 | + | r.X, r.Y = r.X-sc.x, r.Y-sc.y | |
| 84 | + | out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: true}) | |
| 85 | + | } | |
| 86 | + | return out | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // unmanagedAt is the topmost unmanaged window taking the pointer at the | |
| 90 | + | // screen-local point. None while the pointer is locked or the screen previews. | |
| 91 | + | func (c *Core) unmanagedAt(sc *screen, lx, ly float64) (WindowID, float64, float64) { | |
| 92 | + | if len(c.unmanagedOrder) == 0 || c.constraint.Mode == ports.ConstraintLock || sc.mon.previewing() { | |
| 93 | + | return 0, 0, 0 | |
| 94 | + | } | |
| 95 | + | for i := len(c.unmanagedOrder) - 1; i >= 0; i-- { | |
| 96 | + | u := c.unmanaged[c.unmanagedOrder[i]] | |
| 97 | + | if u == nil || u.noInput || c.unmanagedScreen(u) != sc { | |
| 98 | + | continue | |
| 99 | + | } | |
| 100 | + | x, y := lx-float64(u.rect.X-sc.x), ly-float64(u.rect.Y-sc.y) | |
| 101 | + | if x >= 0 && x < float64(u.rect.W) && y >= 0 && y < float64(u.rect.H) && c.acceptsInput(u.id, x, y) { | |
| 102 | + | return u.id, x, y | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | return 0, 0, 0 | |
| 106 | + | } | |
| @@ -0,0 +1,106 @@ | |||||
| 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 | + | } | |||
| 52 | + | // The window under a still pointer may have changed. | |||
| 53 | + | return true, c.rehit(ctx) | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // unmanagedScreen is the screen showing u: the one containing its origin, or | |||
| 57 | + | // the one the pointer clamp would pick when the origin is in a hole. | |||
| 58 | + | func (c *Core) unmanagedScreen(u *unmanagedState) *screen { | |||
| 59 | + | x, y := float64(u.rect.X), float64(u.rect.Y) | |||
| 60 | + | i := c.screenAt(x, y) | |||
| 61 | + | if i < 0 { | |||
| 62 | + | cx, cy := c.clampPointer(x, y, x, y) | |||
| 63 | + | i = c.screenAt(cx, cy) | |||
| 64 | + | } | |||
| 65 | + | if i < 0 { | |||
| 66 | + | return nil | |||
| 67 | + | } | |||
| 68 | + | return c.screens[i] | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // sceneUnmanaged appends the unmanaged windows shown on sc, bottom to top, | |||
| 72 | + | // drawn as popups over the layers. They are hidden while sc previews. | |||
| 73 | + | func (c *Core) sceneUnmanaged(out []ports.SceneWindow, sc *screen) []ports.SceneWindow { | |||
| 74 | + | if len(c.unmanagedOrder) == 0 || sc.mon.previewing() { | |||
| 75 | + | return out | |||
| 76 | + | } | |||
| 77 | + | for _, id := range c.unmanagedOrder { | |||
| 78 | + | u := c.unmanaged[id] | |||
| 79 | + | if u == nil || c.unmanagedScreen(u) != sc { | |||
| 80 | + | continue | |||
| 81 | + | } | |||
| 82 | + | r := u.rect | |||
| 83 | + | r.X, r.Y = r.X-sc.x, r.Y-sc.y | |||
| 84 | + | out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: true}) | |||
| 85 | + | } | |||
| 86 | + | return out | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // unmanagedAt is the topmost unmanaged window taking the pointer at the | |||
| 90 | + | // screen-local point. None while the pointer is locked or the screen previews. | |||
| 91 | + | func (c *Core) unmanagedAt(sc *screen, lx, ly float64) (WindowID, float64, float64) { | |||
| 92 | + | if len(c.unmanagedOrder) == 0 || c.constraint.Mode == ports.ConstraintLock || sc.mon.previewing() { | |||
| 93 | + | return 0, 0, 0 | |||
| 94 | + | } | |||
| 95 | + | for i := len(c.unmanagedOrder) - 1; i >= 0; i-- { | |||
| 96 | + | u := c.unmanaged[c.unmanagedOrder[i]] | |||
| 97 | + | if u == nil || u.noInput || c.unmanagedScreen(u) != sc { | |||
| 98 | + | continue | |||
| 99 | + | } | |||
| 100 | + | x, y := lx-float64(u.rect.X-sc.x), ly-float64(u.rect.Y-sc.y) | |||
| 101 | + | if x >= 0 && x < float64(u.rect.W) && y >= 0 && y < float64(u.rect.H) && c.acceptsInput(u.id, x, y) { | |||
| 102 | + | return u.id, x, y | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | return 0, 0, 0 | |||
| 106 | + | } | |||
A internal/core/unmanaged_test.go +124 -0
| @@ -0,0 +1,124 @@ | |||
| 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 | + | } | |
| @@ -0,0 +1,124 @@ | |||||
| 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 | + | } | |||
M internal/ports/client_events.go +25 -0
| @@ -52,6 +52,31 @@ type WindowUnmapped struct{ ID WindowID } | |||
| 52 | 52 | ||
| 53 | 53 | func (WindowUnmapped) clientEvent() {} | |
| 54 | 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 | + | ||
| 55 | 80 | // ShortcutsInhibit carries wayland → core that a window asks for (Active) | |
| 56 | 81 | // or stops asking for all keys, compositor binds included, while it has | |
| 57 | 82 | // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1). | |
| @@ -52,6 +52,31 @@ type WindowUnmapped struct{ ID WindowID } | |||||
| 52 | 52 | ||||
| 53 | func (WindowUnmapped) clientEvent() {} | 53 | func (WindowUnmapped) clientEvent() {} | ||
| 54 | 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 | + | ||||
| 55 | // ShortcutsInhibit carries wayland → core that a window asks for (Active) | 80 | // ShortcutsInhibit carries wayland → core that a window asks for (Active) | ||
| 56 | // 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 | ||
| 57 | // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1). | 82 | // keyboard focus (zwp_keyboard_shortcuts_inhibit_v1). | ||