fix(layer): on-demand focus ends on any focus change, window popups stay under top layers
- commit
- e2fd4a06f00cf0e10dfe49f02727eaf63ed78377
- author
- brice · · 2026-09-26 05:59 UTC
- committer
- brice · · 2026-09-26 06:05 UTC
- parent
- 2273fad
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +127 -23
M internal/adapters/vulkan/renderer.go +11 -8
| @@ -548,16 +548,19 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 548 | 548 | } | |
| 549 | 549 | } | |
| 550 | 550 | } | |
| 551 | - | addLayers(true) | |
| 552 | - | // Popups go over everything, layers included: a bar's menu shows over | |
| 553 | - | // the bar and the windows. | |
| 554 | - | for _, w := range s.Windows { | |
| 555 | - | if w.Popup && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | |
| 556 | - | // Menus draw only what the client drew, shadows clipped. | |
| 557 | - | content := contents[w.ID] | |
| 558 | - | place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | |
| 551 | + | // Popups draw only what the client drew, shadows clipped. Window popups | |
| 552 | + | // stay with the windows; a layer's go over every layer. | |
| 553 | + | addPopups := func(overLayers bool) { | |
| 554 | + | for _, w := range s.Windows { | |
| 555 | + | if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | |
| 556 | + | content := contents[w.ID] | |
| 557 | + | place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | |
| 558 | + | } | |
| 559 | 559 | } | |
| 560 | 560 | } | |
| 561 | + | addPopups(false) | |
| 562 | + | addLayers(true) | |
| 563 | + | addPopups(true) | |
| 561 | 564 | if err := r.ensureStaging(used); err != nil { | |
| 562 | 565 | return err | |
| 563 | 566 | } | |
| @@ -548,16 +548,19 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 548 | } | 548 | } | ||
| 549 | } | 549 | } | ||
| 550 | } | 550 | } | ||
| 551 | - | addLayers(true) | 551 | + | // Popups draw only what the client drew, shadows clipped. Window popups |
| 552 | - | // Popups go over everything, layers included: a bar's menu shows over | 552 | + | // stay with the windows; a layer's go over every layer. |
| 553 | - | // the bar and the windows. | 553 | + | addPopups := func(overLayers bool) { |
| 554 | - | for _, w := range s.Windows { | 554 | + | for _, w := range s.Windows { |
| 555 | - | if w.Popup && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | 555 | + | if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { |
| 556 | - | // Menus draw only what the client drew, shadows clipped. | 556 | + | content := contents[w.ID] |
| 557 | - | content := contents[w.ID] | 557 | + | place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) |
| 558 | - | place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | 558 | + | } |
| 559 | } | 559 | } | ||
| 560 | } | 560 | } | ||
| 561 | + | addPopups(false) | |||
| 562 | + | addLayers(true) | |||
| 563 | + | addPopups(true) | |||
| 561 | if err := r.ensureStaging(used); err != nil { | 564 | if err := r.ensureStaging(used); err != nil { | ||
| 562 | return err | 565 | return err | ||
| 563 | } | 566 | } | ||
M internal/core/core.go +23 -10
| @@ -82,8 +82,11 @@ type Core struct { | |||
| 82 | 82 | popups map[WindowID]*popupState | |
| 83 | 83 | popupOrder []WindowID | |
| 84 | 84 | // layerFocus is the on-demand layer surface the user clicked; it keeps | |
| 85 | - | // the keyboard until a window is clicked or the layer unmaps. | |
| 85 | + | // the keyboard while it stays mapped on-demand and the window focus | |
| 86 | + | // (layerOver) does not change: any click elsewhere, bind, activation or | |
| 87 | + | // new window takes the keyboard back. | |
| 86 | 88 | layerFocus WindowID | |
| 89 | + | layerOver WindowID | |
| 87 | 90 | } | |
| 88 | 91 | ||
| 89 | 92 | func keyName(s string) string { | |
| @@ -319,10 +322,13 @@ func (c *Core) keyboardFocus() WindowID { | |||
| 319 | 322 | if g := c.grabFocus(); g != 0 { | |
| 320 | 323 | return g | |
| 321 | 324 | } | |
| 322 | - | if c.layerFocus != 0 && c.onDemand(c.layerFocus) { | |
| 325 | + | id, _ := c.cur().mon.Focused() | |
| 326 | + | if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || id != c.layerOver) { | |
| 327 | + | c.layerFocus = 0 | |
| 328 | + | } | |
| 329 | + | if c.layerFocus != 0 { | |
| 323 | 330 | return c.layerFocus | |
| 324 | 331 | } | |
| 325 | - | id, _ := c.cur().mon.Focused() | |
| 326 | 332 | return id | |
| 327 | 333 | } | |
| 328 | 334 | ||
| @@ -515,13 +521,17 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 515 | 521 | } | |
| 516 | 522 | lx, ly := x-float64(o.X), y-float64(o.Y) | |
| 517 | 523 | sc := c.screens[c.screenIndex(o.Info.Name)] | |
| 518 | - | // Popups are above everything but fullscreen covers nothing of them. | |
| 519 | - | if id, px, py := c.popupAt(sc, lx, ly); id != 0 { | |
| 524 | + | // Layer popups are over everything; window popups are over the windows | |
| 525 | + | // only, under the top and overlay layers (as drawn). | |
| 526 | + | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | |
| 520 | 527 | return id, px, py | |
| 521 | 528 | } | |
| 522 | 529 | if id, px, py := layerAt(sc, lx, ly, true); id != 0 { | |
| 523 | 530 | return id, px, py | |
| 524 | 531 | } | |
| 532 | + | if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 { | |
| 533 | + | return id, px, py | |
| 534 | + | } | |
| 525 | 535 | var id WindowID | |
| 526 | 536 | var sx, sy float64 | |
| 527 | 537 | full := false | |
| @@ -737,16 +747,19 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 737 | 747 | // A click on an on-demand layer gives it the keyboard. | |
| 738 | 748 | if v.Pressed && c.onDemand(id) && c.layerFocus != id { | |
| 739 | 749 | c.layerFocus = id | |
| 750 | + | c.layerOver, _ = c.cur().mon.Focused() | |
| 740 | 751 | if err := c.publish(ctx); err != nil { | |
| 741 | 752 | return nil | |
| 742 | 753 | } | |
| 743 | - | } | |
| 744 | - | // A click focuses the window and its output, taking the | |
| 745 | - | // keyboard back from a layer. | |
| 746 | - | s, w := c.screenOf(id) | |
| 747 | - | if v.Pressed && s != nil { | |
| 754 | + | } else if v.Pressed && c.layerFocus != 0 && id != c.layerFocus && c.popupRoot(id) != c.layerFocus { | |
| 755 | + | // A click anywhere else takes the keyboard back. | |
| 748 | 756 | c.layerFocus = 0 | |
| 757 | + | if err := c.publish(ctx); err != nil { | |
| 758 | + | return nil | |
| 759 | + | } | |
| 749 | 760 | } | |
| 761 | + | // A click focuses the window and its output. | |
| 762 | + | s, w := c.screenOf(id) | |
| 750 | 763 | if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) { | |
| 751 | 764 | w.FocusID(id) | |
| 752 | 765 | c.focusScreen = c.screenIndex(s.name()) | |
| @@ -82,8 +82,11 @@ type Core struct { | |||||
| 82 | popups map[WindowID]*popupState | 82 | popups map[WindowID]*popupState | ||
| 83 | popupOrder []WindowID | 83 | popupOrder []WindowID | ||
| 84 | // layerFocus is the on-demand layer surface the user clicked; it keeps | 84 | // layerFocus is the on-demand layer surface the user clicked; it keeps | ||
| 85 | - | // the keyboard until a window is clicked or the layer unmaps. | 85 | + | // the keyboard while it stays mapped on-demand and the window focus |
| 86 | + | // (layerOver) does not change: any click elsewhere, bind, activation or | |||
| 87 | + | // new window takes the keyboard back. | |||
| 86 | layerFocus WindowID | 88 | layerFocus WindowID | ||
| 89 | + | layerOver WindowID | |||
| 87 | } | 90 | } | ||
| 88 | 91 | ||||
| 89 | func keyName(s string) string { | 92 | func keyName(s string) string { | ||
| @@ -319,10 +322,13 @@ func (c *Core) keyboardFocus() WindowID { | |||||
| 319 | if g := c.grabFocus(); g != 0 { | 322 | if g := c.grabFocus(); g != 0 { | ||
| 320 | return g | 323 | return g | ||
| 321 | } | 324 | } | ||
| 322 | - | if c.layerFocus != 0 && c.onDemand(c.layerFocus) { | 325 | + | id, _ := c.cur().mon.Focused() |
| 326 | + | if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || id != c.layerOver) { | |||
| 327 | + | c.layerFocus = 0 | |||
| 328 | + | } | |||
| 329 | + | if c.layerFocus != 0 { | |||
| 323 | return c.layerFocus | 330 | return c.layerFocus | ||
| 324 | } | 331 | } | ||
| 325 | - | id, _ := c.cur().mon.Focused() | |||
| 326 | return id | 332 | return id | ||
| 327 | } | 333 | } | ||
| 328 | 334 | ||||
| @@ -515,13 +521,17 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 515 | } | 521 | } | ||
| 516 | lx, ly := x-float64(o.X), y-float64(o.Y) | 522 | lx, ly := x-float64(o.X), y-float64(o.Y) | ||
| 517 | sc := c.screens[c.screenIndex(o.Info.Name)] | 523 | sc := c.screens[c.screenIndex(o.Info.Name)] | ||
| 518 | - | // Popups are above everything but fullscreen covers nothing of them. | 524 | + | // Layer popups are over everything; window popups are over the windows |
| 519 | - | if id, px, py := c.popupAt(sc, lx, ly); id != 0 { | 525 | + | // only, under the top and overlay layers (as drawn). |
| 526 | + | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | |||
| 520 | return id, px, py | 527 | return id, px, py | ||
| 521 | } | 528 | } | ||
| 522 | if id, px, py := layerAt(sc, lx, ly, true); id != 0 { | 529 | if id, px, py := layerAt(sc, lx, ly, true); id != 0 { | ||
| 523 | return id, px, py | 530 | return id, px, py | ||
| 524 | } | 531 | } | ||
| 532 | + | if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 { | |||
| 533 | + | return id, px, py | |||
| 534 | + | } | |||
| 525 | var id WindowID | 535 | var id WindowID | ||
| 526 | var sx, sy float64 | 536 | var sx, sy float64 | ||
| 527 | full := false | 537 | full := false | ||
| @@ -737,16 +747,19 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 737 | // A click on an on-demand layer gives it the keyboard. | 747 | // A click on an on-demand layer gives it the keyboard. | ||
| 738 | if v.Pressed && c.onDemand(id) && c.layerFocus != id { | 748 | if v.Pressed && c.onDemand(id) && c.layerFocus != id { | ||
| 739 | c.layerFocus = id | 749 | c.layerFocus = id | ||
| 750 | + | c.layerOver, _ = c.cur().mon.Focused() | |||
| 740 | if err := c.publish(ctx); err != nil { | 751 | if err := c.publish(ctx); err != nil { | ||
| 741 | return nil | 752 | return nil | ||
| 742 | } | 753 | } | ||
| 743 | - | } | 754 | + | } else if v.Pressed && c.layerFocus != 0 && id != c.layerFocus && c.popupRoot(id) != c.layerFocus { |
| 744 | - | // A click focuses the window and its output, taking the | 755 | + | // A click anywhere else takes the keyboard back. |
| 745 | - | // keyboard back from a layer. | |||
| 746 | - | s, w := c.screenOf(id) | |||
| 747 | - | if v.Pressed && s != nil { | |||
| 748 | c.layerFocus = 0 | 756 | c.layerFocus = 0 | ||
| 757 | + | if err := c.publish(ctx); err != nil { | |||
| 758 | + | return nil | |||
| 759 | + | } | |||
| 749 | } | 760 | } | ||
| 761 | + | // A click focuses the window and its output. | |||
| 762 | + | s, w := c.screenOf(id) | |||
| 750 | if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) { | 763 | if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) { | ||
| 751 | w.FocusID(id) | 764 | w.FocusID(id) | ||
| 752 | c.focusScreen = c.screenIndex(s.name()) | 765 | c.focusScreen = c.screenIndex(s.name()) | ||
M internal/core/layer_input_test.go +62 -0
| @@ -112,6 +112,68 @@ func TestLayerPointerPopupAndFocus(t *testing.T) { | |||
| 112 | 112 | if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 { | |
| 113 | 113 | t.Fatalf("focus %d, want the bar back", f.ID) | |
| 114 | 114 | } | |
| 115 | + | ||
| 116 | + | // Unmapped then mapped again, the bar does not take the keyboard back. | |
| 117 | + | client <- ports.LayerChanged{} | |
| 118 | + | next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 1 }) | |
| 119 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |
| 120 | + | scene(t, scenes) | |
| 121 | + | input <- ports.PointerMotion{X: 20, Y: 50} // publishes | |
| 122 | + | next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 1 }) | |
| 123 | + | select { | |
| 124 | + | case v := <-commands: | |
| 125 | + | if f, ok := v.(ports.FocusWindow); ok { | |
| 126 | + | t.Fatalf("focus moved to %d", f.ID) | |
| 127 | + | } | |
| 128 | + | default: | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // A window focus change by keyboard or a new window takes it back too. | |
| 132 | + | input <- ports.PointerMotion{X: 50, Y: 5} | |
| 133 | + | next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 }) | |
| 134 | + | input <- ports.PointerButton{Button: 0x110, Pressed: true} | |
| 135 | + | next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 }) | |
| 136 | + | input <- ports.PointerButton{Button: 0x110} | |
| 137 | + | client <- ports.WindowMapped{ID: 2} | |
| 138 | + | if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 2 { | |
| 139 | + | t.Fatalf("focus %d, want the new window", f.ID) | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | // A window's menu stays with the windows: an overlay layer covers it. | |
| 144 | + | func TestWindowPopupUnderOverlay(t *testing.T) { | |
| 145 | + | cfg := config.Defaults() | |
| 146 | + | cfg.Border.Width = 0 | |
| 147 | + | client := make(chan ports.ClientEvent, 8) | |
| 148 | + | input := make(chan ports.InputEvent, 8) | |
| 149 | + | output := make(chan ports.OutputEvent, 8) | |
| 150 | + | commands := make(chan ports.ClientCommand, 256) | |
| 151 | + | scenes := make(chan []ports.Scene, 1) | |
| 152 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 153 | + | if err != nil { | |
| 154 | + | t.Fatal(err) | |
| 155 | + | } | |
| 156 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 157 | + | defer cancel() | |
| 158 | + | go c.Run(ctx) | |
| 159 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 160 | + | scene(t, scenes) | |
| 161 | + | client <- ports.WindowMapped{ID: 1} | |
| 162 | + | scene(t, scenes) | |
| 163 | + | client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 0, Y: 0, W: 10, H: 10}}} | |
| 164 | + | next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 }) | |
| 165 | + | client <- ports.PopupMapped{ID: 9} | |
| 166 | + | sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 }) | |
| 167 | + | if w := sc.Windows[1]; !w.Popup || w.OverLayers { | |
| 168 | + | t.Fatalf("window popup %+v", w) | |
| 169 | + | } | |
| 170 | + | osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: 15} | |
| 171 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{osd}} | |
| 172 | + | scene(t, scenes) | |
| 173 | + | input <- ports.PointerMotion{X: 5, Y: 5} | |
| 174 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 7 { | |
| 175 | + | t.Fatalf("hit %d, want the overlay", v.ID) | |
| 176 | + | } | |
| 115 | 177 | } | |
| 116 | 178 | ||
| 117 | 179 | // A fullscreen window hides top and bottom layers from the pointer, not | |
| @@ -112,6 +112,68 @@ func TestLayerPointerPopupAndFocus(t *testing.T) { | |||||
| 112 | if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 { | 112 | if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 { | ||
| 113 | t.Fatalf("focus %d, want the bar back", f.ID) | 113 | t.Fatalf("focus %d, want the bar back", f.ID) | ||
| 114 | } | 114 | } | ||
| 115 | + | ||||
| 116 | + | // Unmapped then mapped again, the bar does not take the keyboard back. | |||
| 117 | + | client <- ports.LayerChanged{} | |||
| 118 | + | next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 1 }) | |||
| 119 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |||
| 120 | + | scene(t, scenes) | |||
| 121 | + | input <- ports.PointerMotion{X: 20, Y: 50} // publishes | |||
| 122 | + | next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 1 }) | |||
| 123 | + | select { | |||
| 124 | + | case v := <-commands: | |||
| 125 | + | if f, ok := v.(ports.FocusWindow); ok { | |||
| 126 | + | t.Fatalf("focus moved to %d", f.ID) | |||
| 127 | + | } | |||
| 128 | + | default: | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // A window focus change by keyboard or a new window takes it back too. | |||
| 132 | + | input <- ports.PointerMotion{X: 50, Y: 5} | |||
| 133 | + | next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 }) | |||
| 134 | + | input <- ports.PointerButton{Button: 0x110, Pressed: true} | |||
| 135 | + | next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 }) | |||
| 136 | + | input <- ports.PointerButton{Button: 0x110} | |||
| 137 | + | client <- ports.WindowMapped{ID: 2} | |||
| 138 | + | if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 2 { | |||
| 139 | + | t.Fatalf("focus %d, want the new window", f.ID) | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | // A window's menu stays with the windows: an overlay layer covers it. | |||
| 144 | + | func TestWindowPopupUnderOverlay(t *testing.T) { | |||
| 145 | + | cfg := config.Defaults() | |||
| 146 | + | cfg.Border.Width = 0 | |||
| 147 | + | client := make(chan ports.ClientEvent, 8) | |||
| 148 | + | input := make(chan ports.InputEvent, 8) | |||
| 149 | + | output := make(chan ports.OutputEvent, 8) | |||
| 150 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 151 | + | scenes := make(chan []ports.Scene, 1) | |||
| 152 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |||
| 153 | + | if err != nil { | |||
| 154 | + | t.Fatal(err) | |||
| 155 | + | } | |||
| 156 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 157 | + | defer cancel() | |||
| 158 | + | go c.Run(ctx) | |||
| 159 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 160 | + | scene(t, scenes) | |||
| 161 | + | client <- ports.WindowMapped{ID: 1} | |||
| 162 | + | scene(t, scenes) | |||
| 163 | + | client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 0, Y: 0, W: 10, H: 10}}} | |||
| 164 | + | next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 }) | |||
| 165 | + | client <- ports.PopupMapped{ID: 9} | |||
| 166 | + | sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 }) | |||
| 167 | + | if w := sc.Windows[1]; !w.Popup || w.OverLayers { | |||
| 168 | + | t.Fatalf("window popup %+v", w) | |||
| 169 | + | } | |||
| 170 | + | osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: 15} | |||
| 171 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{osd}} | |||
| 172 | + | scene(t, scenes) | |||
| 173 | + | input <- ports.PointerMotion{X: 5, Y: 5} | |||
| 174 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 7 { | |||
| 175 | + | t.Fatalf("hit %d, want the overlay", v.ID) | |||
| 176 | + | } | |||
| 115 | } | 177 | } | ||
| 116 | 178 | ||||
| 117 | // A fullscreen window hides top and bottom layers from the pointer, not | 179 | // A fullscreen window hides top and bottom layers from the pointer, not | ||
M internal/core/popup.go +26 -4
| @@ -278,11 +278,12 @@ func (c *Core) grabFocus() WindowID { | |||
| 278 | 278 | return 0 | |
| 279 | 279 | } | |
| 280 | 280 | ||
| 281 | - | // popupAt returns the topmost mapped popup under the output-local point. | |
| 282 | - | func (c *Core) popupAt(sc *screen, lx, ly float64) (WindowID, float64, float64) { | |
| 281 | + | // popupAt returns the topmost mapped popup under the output-local point, | |
| 282 | + | // among layer popups (overLayers) or window popups. | |
| 283 | + | func (c *Core) popupAt(sc *screen, lx, ly float64, overLayers bool) (WindowID, float64, float64) { | |
| 283 | 284 | for i := len(c.popupOrder) - 1; i >= 0; i-- { | |
| 284 | 285 | p := c.popups[c.popupOrder[i]] | |
| 285 | - | if p == nil || !p.mapped { | |
| 286 | + | if p == nil || !p.mapped || c.onLayer(c.popupRoot(p.id)) != overLayers { | |
| 286 | 287 | continue | |
| 287 | 288 | } | |
| 288 | 289 | s, r, ok := c.windowRect(p.id) | |
| @@ -296,6 +297,27 @@ func (c *Core) popupAt(sc *screen, lx, ly float64) (WindowID, float64, float64) | |||
| 296 | 297 | return 0, 0, 0 | |
| 297 | 298 | } | |
| 298 | 299 | ||
| 300 | + | // popupRoot is the window or layer a popup chain hangs from; id itself | |
| 301 | + | // when it is not a popup. | |
| 302 | + | func (c *Core) popupRoot(id WindowID) WindowID { | |
| 303 | + | for p := c.popups[id]; p != nil; p = c.popups[id] { | |
| 304 | + | id = p.parent | |
| 305 | + | } | |
| 306 | + | return id | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | // onLayer reports whether id is a mapped layer surface. | |
| 310 | + | func (c *Core) onLayer(id WindowID) bool { | |
| 311 | + | for _, sc := range c.screens { | |
| 312 | + | for _, l := range sc.layers { | |
| 313 | + | if l.ID == id { | |
| 314 | + | return true | |
| 315 | + | } | |
| 316 | + | } | |
| 317 | + | } | |
| 318 | + | return false | |
| 319 | + | } | |
| 320 | + | ||
| 299 | 321 | // scenePopups are the mapped popups on a screen, bottom to top. | |
| 300 | 322 | func (c *Core) scenePopups(sc *screen) []ports.SceneWindow { | |
| 301 | 323 | var out []ports.SceneWindow | |
| @@ -305,7 +327,7 @@ func (c *Core) scenePopups(sc *screen) []ports.SceneWindow { | |||
| 305 | 327 | continue | |
| 306 | 328 | } | |
| 307 | 329 | if s, r, ok := c.windowRect(id); ok && s == sc { | |
| 308 | - | out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true}) | |
| 330 | + | out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: c.onLayer(c.popupRoot(id))}) | |
| 309 | 331 | } | |
| 310 | 332 | } | |
| 311 | 333 | return out | |
| @@ -278,11 +278,12 @@ func (c *Core) grabFocus() WindowID { | |||||
| 278 | return 0 | 278 | return 0 | ||
| 279 | } | 279 | } | ||
| 280 | 280 | ||||
| 281 | - | // popupAt returns the topmost mapped popup under the output-local point. | 281 | + | // popupAt returns the topmost mapped popup under the output-local point, |
| 282 | - | func (c *Core) popupAt(sc *screen, lx, ly float64) (WindowID, float64, float64) { | 282 | + | // among layer popups (overLayers) or window popups. |
| 283 | + | func (c *Core) popupAt(sc *screen, lx, ly float64, overLayers bool) (WindowID, float64, float64) { | |||
| 283 | for i := len(c.popupOrder) - 1; i >= 0; i-- { | 284 | for i := len(c.popupOrder) - 1; i >= 0; i-- { | ||
| 284 | p := c.popups[c.popupOrder[i]] | 285 | p := c.popups[c.popupOrder[i]] | ||
| 285 | - | if p == nil || !p.mapped { | 286 | + | if p == nil || !p.mapped || c.onLayer(c.popupRoot(p.id)) != overLayers { |
| 286 | continue | 287 | continue | ||
| 287 | } | 288 | } | ||
| 288 | s, r, ok := c.windowRect(p.id) | 289 | s, r, ok := c.windowRect(p.id) | ||
| @@ -296,6 +297,27 @@ func (c *Core) popupAt(sc *screen, lx, ly float64) (WindowID, float64, float64) | |||||
| 296 | return 0, 0, 0 | 297 | return 0, 0, 0 | ||
| 297 | } | 298 | } | ||
| 298 | 299 | ||||
| 300 | + | // popupRoot is the window or layer a popup chain hangs from; id itself | |||
| 301 | + | // when it is not a popup. | |||
| 302 | + | func (c *Core) popupRoot(id WindowID) WindowID { | |||
| 303 | + | for p := c.popups[id]; p != nil; p = c.popups[id] { | |||
| 304 | + | id = p.parent | |||
| 305 | + | } | |||
| 306 | + | return id | |||
| 307 | + | } | |||
| 308 | + | ||||
| 309 | + | // onLayer reports whether id is a mapped layer surface. | |||
| 310 | + | func (c *Core) onLayer(id WindowID) bool { | |||
| 311 | + | for _, sc := range c.screens { | |||
| 312 | + | for _, l := range sc.layers { | |||
| 313 | + | if l.ID == id { | |||
| 314 | + | return true | |||
| 315 | + | } | |||
| 316 | + | } | |||
| 317 | + | } | |||
| 318 | + | return false | |||
| 319 | + | } | |||
| 320 | + | ||||
| 299 | // scenePopups are the mapped popups on a screen, bottom to top. | 321 | // scenePopups are the mapped popups on a screen, bottom to top. | ||
| 300 | func (c *Core) scenePopups(sc *screen) []ports.SceneWindow { | 322 | func (c *Core) scenePopups(sc *screen) []ports.SceneWindow { | ||
| 301 | var out []ports.SceneWindow | 323 | var out []ports.SceneWindow | ||
| @@ -305,7 +327,7 @@ func (c *Core) scenePopups(sc *screen) []ports.SceneWindow { | |||||
| 305 | continue | 327 | continue | ||
| 306 | } | 328 | } | ||
| 307 | if s, r, ok := c.windowRect(id); ok && s == sc { | 329 | if s, r, ok := c.windowRect(id); ok && s == sc { | ||
| 308 | - | out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true}) | 330 | + | out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: c.onLayer(c.popupRoot(id))}) |
| 309 | } | 331 | } | ||
| 310 | } | 332 | } | ||
| 311 | return out | 333 | return out | ||
M internal/ports/messages.go +5 -1
| @@ -532,7 +532,8 @@ type Scene struct { | |||
| 532 | 532 | Windows []SceneWindow | |
| 533 | 533 | // Layers are drawn in slice order: background and bottom before windows, | |
| 534 | 534 | // top and overlay after. A fullscreen window covers bottom and top. | |
| 535 | - | // Popups (SceneWindow.Popup) are drawn last, over every layer. | |
| 535 | + | // Window popups are drawn after the windows, layer popups | |
| 536 | + | // (SceneWindow.OverLayers) last, over every layer. | |
| 536 | 537 | Layers []SceneLayer | |
| 537 | 538 | } | |
| 538 | 539 | ||
| @@ -658,6 +659,9 @@ type SceneWindow struct { | |||
| 658 | 659 | Borderless bool | |
| 659 | 660 | // Popups are drawn from their content only: no border, no background. | |
| 660 | 661 | Popup bool | |
| 662 | + | // OverLayers popups hang from a layer surface: drawn over the top and | |
| 663 | + | // overlay layers. Window popups stay with the windows, under them. | |
| 664 | + | OverLayers bool | |
| 661 | 665 | } | |
| 662 | 666 | ||
| 663 | 667 | // State carries core → state publisher a snapshot of what is on screen, for | |
| @@ -532,7 +532,8 @@ type Scene struct { | |||||
| 532 | Windows []SceneWindow | 532 | Windows []SceneWindow | ||
| 533 | // Layers are drawn in slice order: background and bottom before windows, | 533 | // Layers are drawn in slice order: background and bottom before windows, | ||
| 534 | // top and overlay after. A fullscreen window covers bottom and top. | 534 | // top and overlay after. A fullscreen window covers bottom and top. | ||
| 535 | - | // Popups (SceneWindow.Popup) are drawn last, over every layer. | 535 | + | // Window popups are drawn after the windows, layer popups |
| 536 | + | // (SceneWindow.OverLayers) last, over every layer. | |||
| 536 | Layers []SceneLayer | 537 | Layers []SceneLayer | ||
| 537 | } | 538 | } | ||
| 538 | 539 | ||||
| @@ -658,6 +659,9 @@ type SceneWindow struct { | |||||
| 658 | Borderless bool | 659 | Borderless bool | ||
| 659 | // Popups are drawn from their content only: no border, no background. | 660 | // Popups are drawn from their content only: no border, no background. | ||
| 660 | Popup bool | 661 | Popup bool | ||
| 662 | + | // OverLayers popups hang from a layer surface: drawn over the top and | |||
| 663 | + | // overlay layers. Window popups stay with the windows, under them. | |||
| 664 | + | OverLayers bool | |||
| 661 | } | 665 | } | ||
| 662 | 666 | ||||
| 663 | // State carries core → state publisher a snapshot of what is on screen, for | 667 | // State carries core → state publisher a snapshot of what is on screen, for | ||