Merge pull request 'fix(core): reach windows hidden by a fullscreen window from the overview' (#3) from fix/overview-fullscreen-reach into main
Reviewed-on: https://git.bnema.dev/bnema/neferwl/pulls/3
9 files · +589 -68
M docs/config.md +2 -2
| @@ -120,7 +120,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||
| 120 | 120 | | `spawn <command>` | Run a program | | |
| 121 | 121 | | `spawn-terminal` | Run `terminal` | | |
| 122 | 122 | | `close-window` | Close the focused window | | |
| 123 | - | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows, dialogs included, wait hidden until it leaves; a window opening meanwhile waits hidden too. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down` or an activation) leaves fullscreen. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | | |
| 123 | + | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows, dialogs included, wait hidden until it leaves; a window opening meanwhile waits hidden too. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen; the app's own fullscreen requests are then refused until it is focused again. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | | |
| 124 | 124 | | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized or expanded state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first | | |
| 125 | 125 | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | |
| 126 | 126 | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Does nothing under a fullscreen window | | |
| @@ -172,7 +172,7 @@ The drop can be on another monitor; focus follows the window. There, the middle | |||
| 172 | 172 | ||
| 173 | 173 | ### Overview | |
| 174 | 174 | ||
| 175 | - | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, up to two cards behind peek above it, and passed cards peek below. Neighbor workspaces show dimmed stacks. Each monitor has one vertical overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with horizontal rules separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear above and below; rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal workspace up/down navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's only preview; `scroll` overflow keeps its columns selectable in one card. | |
| 175 | + | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, up to two cards behind peek above it, and passed cards peek below. Neighbor workspaces show dimmed stacks. Each monitor has one vertical overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with horizontal rules separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear above and below; rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal workspace up/down navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's front card, with the windows it hides behind it; `scroll` overflow keeps its columns selectable in one card. Choosing the fullscreen window keeps it fullscreen; choosing another window leaves fullscreen. | |
| 176 | 176 | ||
| 177 | 177 | | Key | Action | | |
| 178 | 178 | | --- | --- | | |
| @@ -120,7 +120,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||||
| 120 | | `spawn <command>` | Run a program | | 120 | | `spawn <command>` | Run a program | | ||
| 121 | | `spawn-terminal` | Run `terminal` | | 121 | | `spawn-terminal` | Run `terminal` | | ||
| 122 | | `close-window` | Close the focused window | | 122 | | `close-window` | Close the focused window | | ||
| 123 | - | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows, dialogs included, wait hidden until it leaves; a window opening meanwhile waits hidden too. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down` or an activation) leaves fullscreen. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | | 123 | + | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows, dialogs included, wait hidden until it leaves; a window opening meanwhile waits hidden too. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen; the app's own fullscreen requests are then refused until it is focused again. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | |
| 124 | | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized or expanded state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first | | 124 | | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized or expanded state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first | | ||
| 125 | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | 125 | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | ||
| 126 | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Does nothing under a fullscreen window | | 126 | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Does nothing under a fullscreen window | | ||
| @@ -172,7 +172,7 @@ The drop can be on another monitor; focus follows the window. There, the middle | |||||
| 172 | 172 | ||||
| 173 | ### Overview | 173 | ### Overview | ||
| 174 | 174 | ||||
| 175 | - | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, up to two cards behind peek above it, and passed cards peek below. Neighbor workspaces show dimmed stacks. Each monitor has one vertical overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with horizontal rules separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear above and below; rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal workspace up/down navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's only preview; `scroll` overflow keeps its columns selectable in one card. | 175 | + | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, up to two cards behind peek above it, and passed cards peek below. Neighbor workspaces show dimmed stacks. Each monitor has one vertical overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with horizontal rules separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear above and below; rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal workspace up/down navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's front card, with the windows it hides behind it; `scroll` overflow keeps its columns selectable in one card. Choosing the fullscreen window keeps it fullscreen; choosing another window leaves fullscreen. |
| 176 | 176 | ||||
| 177 | | Key | Action | | 177 | | Key | Action | | ||
| 178 | | --- | --- | | 178 | | --- | --- | | ||
M internal/core/core.go +6 -0
| @@ -483,6 +483,12 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 483 | 483 | if c.security.Protected { | |
| 484 | 484 | return c.publishProtected(ctx) | |
| 485 | 485 | } | |
| 486 | + | for _, sc := range c.screens { | |
| 487 | + | // The overview's selection is provisional: it focuses nothing yet. | |
| 488 | + | if !sc.mon.ov.open { | |
| 489 | + | sc.mon.Current().settleLeft() | |
| 490 | + | } | |
| 491 | + | } | |
| 486 | 492 | c.captureExpire() | |
| 487 | 493 | capture, err := c.captureEvaluate(ctx) | |
| 488 | 494 | if err != nil { | |
| @@ -483,6 +483,12 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 483 | if c.security.Protected { | 483 | if c.security.Protected { | ||
| 484 | return c.publishProtected(ctx) | 484 | return c.publishProtected(ctx) | ||
| 485 | } | 485 | } | ||
| 486 | + | for _, sc := range c.screens { | |||
| 487 | + | // The overview's selection is provisional: it focuses nothing yet. | |||
| 488 | + | if !sc.mon.ov.open { | |||
| 489 | + | sc.mon.Current().settleLeft() | |||
| 490 | + | } | |||
| 491 | + | } | |||
| 486 | c.captureExpire() | 492 | c.captureExpire() | ||
| 487 | capture, err := c.captureEvaluate(ctx) | 493 | capture, err := c.captureEvaluate(ctx) | ||
| 488 | if err != nil { | 494 | if err != nil { | ||
A internal/core/fullscreen_left_core_test.go +80 -0
| @@ -0,0 +1,80 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Through core, scroll overflow: an app whose fullscreen the user left by | |
| 12 | + | // activating another window cannot cover it again by asking at once; once | |
| 13 | + | // activated again, it may ask and is honoured. | |
| 14 | + | func TestCoreLeftFullscreenLatch(t *testing.T) { | |
| 15 | + | client := make(chan ports.ClientEvent) | |
| 16 | + | output := make(chan ports.OutputEvent) | |
| 17 | + | commands := make(chan ports.ClientCommand, 64) | |
| 18 | + | scenes := make(chan []ports.Scene, 1) | |
| 19 | + | cfg := ports.Config{} | |
| 20 | + | cfg.Keyboard.CmdKey = "super" | |
| 21 | + | cfg.Layout.MaxColumns = 2 | |
| 22 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: laterClock(t)}) | |
| 23 | + | if err != nil { | |
| 24 | + | t.Fatal(err) | |
| 25 | + | } | |
| 26 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 27 | + | defer cancel() | |
| 28 | + | done := make(chan struct{}) | |
| 29 | + | go func() { defer close(done); _ = c.Run(ctx) }() | |
| 30 | + | go func() { | |
| 31 | + | for range commands { | |
| 32 | + | } | |
| 33 | + | }() | |
| 34 | + | full := func() ports.WindowID { | |
| 35 | + | t.Helper() | |
| 36 | + | select { | |
| 37 | + | case s := <-scenes: | |
| 38 | + | for _, w := range s[0].Windows { | |
| 39 | + | if w.Fullscreen && !w.Hidden { | |
| 40 | + | return w.ID | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | return 0 | |
| 44 | + | case <-time.After(time.Second): | |
| 45 | + | t.Fatal("scene timeout") | |
| 46 | + | return 0 | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 50 | + | full() | |
| 51 | + | client <- ports.WindowMapped{ID: 1} | |
| 52 | + | full() | |
| 53 | + | client <- ports.WindowMapped{ID: 2} | |
| 54 | + | full() | |
| 55 | + | client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true} | |
| 56 | + | if id := full(); id != 2 { | |
| 57 | + | t.Fatalf("fullscreen %d, want 2", id) | |
| 58 | + | } | |
| 59 | + | client <- ports.WindowActivate{ID: 1} | |
| 60 | + | if id := full(); id != 0 { | |
| 61 | + | t.Fatalf("after activation: fullscreen %d", id) | |
| 62 | + | } | |
| 63 | + | client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true} | |
| 64 | + | if id := full(); id != 0 { | |
| 65 | + | t.Fatalf("re-request covered: fullscreen %d", id) | |
| 66 | + | } | |
| 67 | + | client <- ports.WindowActivate{ID: 2} | |
| 68 | + | full() | |
| 69 | + | client <- ports.WindowActivate{ID: 1} | |
| 70 | + | full() | |
| 71 | + | // The latch settled when 2 had the focus: scroll overflow honours an | |
| 72 | + | // unfocused window's request again. | |
| 73 | + | client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true} | |
| 74 | + | if id := full(); id != 2 { | |
| 75 | + | t.Fatalf("after refocus: fullscreen %d, want 2", id) | |
| 76 | + | } | |
| 77 | + | cancel() | |
| 78 | + | <-done | |
| 79 | + | close(commands) | |
| 80 | + | } | |
| @@ -0,0 +1,80 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Through core, scroll overflow: an app whose fullscreen the user left by | |||
| 12 | + | // activating another window cannot cover it again by asking at once; once | |||
| 13 | + | // activated again, it may ask and is honoured. | |||
| 14 | + | func TestCoreLeftFullscreenLatch(t *testing.T) { | |||
| 15 | + | client := make(chan ports.ClientEvent) | |||
| 16 | + | output := make(chan ports.OutputEvent) | |||
| 17 | + | commands := make(chan ports.ClientCommand, 64) | |||
| 18 | + | scenes := make(chan []ports.Scene, 1) | |||
| 19 | + | cfg := ports.Config{} | |||
| 20 | + | cfg.Keyboard.CmdKey = "super" | |||
| 21 | + | cfg.Layout.MaxColumns = 2 | |||
| 22 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: laterClock(t)}) | |||
| 23 | + | if err != nil { | |||
| 24 | + | t.Fatal(err) | |||
| 25 | + | } | |||
| 26 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 27 | + | defer cancel() | |||
| 28 | + | done := make(chan struct{}) | |||
| 29 | + | go func() { defer close(done); _ = c.Run(ctx) }() | |||
| 30 | + | go func() { | |||
| 31 | + | for range commands { | |||
| 32 | + | } | |||
| 33 | + | }() | |||
| 34 | + | full := func() ports.WindowID { | |||
| 35 | + | t.Helper() | |||
| 36 | + | select { | |||
| 37 | + | case s := <-scenes: | |||
| 38 | + | for _, w := range s[0].Windows { | |||
| 39 | + | if w.Fullscreen && !w.Hidden { | |||
| 40 | + | return w.ID | |||
| 41 | + | } | |||
| 42 | + | } | |||
| 43 | + | return 0 | |||
| 44 | + | case <-time.After(time.Second): | |||
| 45 | + | t.Fatal("scene timeout") | |||
| 46 | + | return 0 | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 50 | + | full() | |||
| 51 | + | client <- ports.WindowMapped{ID: 1} | |||
| 52 | + | full() | |||
| 53 | + | client <- ports.WindowMapped{ID: 2} | |||
| 54 | + | full() | |||
| 55 | + | client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true} | |||
| 56 | + | if id := full(); id != 2 { | |||
| 57 | + | t.Fatalf("fullscreen %d, want 2", id) | |||
| 58 | + | } | |||
| 59 | + | client <- ports.WindowActivate{ID: 1} | |||
| 60 | + | if id := full(); id != 0 { | |||
| 61 | + | t.Fatalf("after activation: fullscreen %d", id) | |||
| 62 | + | } | |||
| 63 | + | client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true} | |||
| 64 | + | if id := full(); id != 0 { | |||
| 65 | + | t.Fatalf("re-request covered: fullscreen %d", id) | |||
| 66 | + | } | |||
| 67 | + | client <- ports.WindowActivate{ID: 2} | |||
| 68 | + | full() | |||
| 69 | + | client <- ports.WindowActivate{ID: 1} | |||
| 70 | + | full() | |||
| 71 | + | // The latch settled when 2 had the focus: scroll overflow honours an | |||
| 72 | + | // unfocused window's request again. | |||
| 73 | + | client <- ports.WindowFullscreenRequest{ID: 2, Fullscreen: true} | |||
| 74 | + | if id := full(); id != 2 { | |||
| 75 | + | t.Fatalf("after refocus: fullscreen %d, want 2", id) | |||
| 76 | + | } | |||
| 77 | + | cancel() | |||
| 78 | + | <-done | |||
| 79 | + | close(commands) | |||
| 80 | + | } | |||
A internal/core/fullscreen_left_test.go +133 -0
| @@ -0,0 +1,133 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | // After the user leaves a window's fullscreen for another window, the | |
| 6 | + | // client's next fullscreen request is refused until the window is focused | |
| 7 | + | // again, whichever way the user left: a focus move, an activation or the | |
| 8 | + | // overview. | |
| 9 | + | func TestLeftFullscreenRefusesRequest(t *testing.T) { | |
| 10 | + | leaves := map[string]func(m *Monitor){ | |
| 11 | + | "focus move": func(m *Monitor) { m.Current().FocusColumn(-1) }, | |
| 12 | + | "activate": func(m *Monitor) { m.Current().Activate(1) }, | |
| 13 | + | "overview": func(m *Monitor) { | |
| 14 | + | m.ToggleOverview() | |
| 15 | + | m.OverviewPick(1) | |
| 16 | + | }, | |
| 17 | + | } | |
| 18 | + | // Fixed overflow refuses an unfocused window's request anyway; scroll | |
| 19 | + | // with a float is where the latch alone refuses it. | |
| 20 | + | for _, tc := range pinnedCases { | |
| 21 | + | for name, leave := range leaves { | |
| 22 | + | t.Run(tc.name+"/"+name, func(t *testing.T) { | |
| 23 | + | m := coverMonitor(tc.overflow, tc.float) | |
| 24 | + | w := m.Current() | |
| 25 | + | leave(m) | |
| 26 | + | if f, _ := m.Focused(); f != 1 || w.fullscreen != 0 || w.left != 2 { | |
| 27 | + | t.Fatalf("leave: focused %d, fullscreen %d, left %d", f, w.fullscreen, w.left) | |
| 28 | + | } | |
| 29 | + | m.SetFullscreen(2, true) | |
| 30 | + | if f, _ := m.Focused(); f != 1 || w.fullscreen != 0 { | |
| 31 | + | t.Fatalf("re-request: focused %d, fullscreen %d", f, w.fullscreen) | |
| 32 | + | } | |
| 33 | + | w.FocusID(2) | |
| 34 | + | m.SetFullscreen(2, true) | |
| 35 | + | if w.fullscreen != 2 || w.left != 0 { | |
| 36 | + | t.Fatalf("focused again: fullscreen %d, left %d", w.fullscreen, w.left) | |
| 37 | + | } | |
| 38 | + | }) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // The bind leaves a window's own fullscreen without the latch: the app | |
| 44 | + | // may ask again at once. | |
| 45 | + | func TestToggleFullscreenOffNoLatch(t *testing.T) { | |
| 46 | + | m := coverMonitor(OverflowFixed, false) | |
| 47 | + | w := m.Current() | |
| 48 | + | m.ToggleFullscreen() | |
| 49 | + | if w.fullscreen != 0 || w.left != 0 { | |
| 50 | + | t.Fatalf("fullscreen %d, left %d", w.fullscreen, w.left) | |
| 51 | + | } | |
| 52 | + | m.SetFullscreen(2, true) | |
| 53 | + | if w.fullscreen != 2 { | |
| 54 | + | t.Fatalf("request refused: %d", w.fullscreen) | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Focusing the window again drops the latch even without a request, so a | |
| 59 | + | // later request while unfocused follows the usual rules again. | |
| 60 | + | func TestLeftFullscreenSettlesOnFocus(t *testing.T) { | |
| 61 | + | m := coverMonitor(OverflowScroll, true) | |
| 62 | + | w := m.Current() | |
| 63 | + | w.Activate(1) | |
| 64 | + | w.FocusID(2) | |
| 65 | + | w.settleLeft() | |
| 66 | + | if w.left != 0 { | |
| 67 | + | t.Fatalf("left %d", w.left) | |
| 68 | + | } | |
| 69 | + | w.FocusID(1) | |
| 70 | + | m.SetFullscreen(2, true) | |
| 71 | + | if w.fullscreen != 2 { | |
| 72 | + | t.Fatalf("request refused: %d", w.fullscreen) | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | // A taskbar fullscreen request activates the window first (core), so it | |
| 77 | + | // passes the latch. | |
| 78 | + | func TestLeftFullscreenTaskbar(t *testing.T) { | |
| 79 | + | m := coverMonitor(OverflowScroll, true) | |
| 80 | + | w := m.Current() | |
| 81 | + | w.Activate(1) | |
| 82 | + | w.Activate(2) | |
| 83 | + | m.SetFullscreen(2, true) | |
| 84 | + | if w.fullscreen != 2 || w.left != 0 { | |
| 85 | + | t.Fatalf("fullscreen %d, left %d", w.fullscreen, w.left) | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // A latched window moved away with its column, which another window of | |
| 90 | + | // it has the focus of, drops the latch. | |
| 91 | + | func TestLeftFullscreenColumnMoved(t *testing.T) { | |
| 92 | + | w := cols(OverflowScroll, 2, 0, []WindowID{2, 3}, []WindowID{1}) | |
| 93 | + | w.FocusID(2) | |
| 94 | + | w.fullscreen = 2 | |
| 95 | + | w.Activate(3) | |
| 96 | + | if w.left != 2 { | |
| 97 | + | t.Fatalf("latch %d", w.left) | |
| 98 | + | } | |
| 99 | + | if _, ok := w.takeColumn(); !ok || w.left != 0 { | |
| 100 | + | t.Fatalf("left %d", w.left) | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // A latched window that goes away drops the latch. | |
| 105 | + | func TestLeftFullscreenRemoved(t *testing.T) { | |
| 106 | + | m := coverMonitor(OverflowScroll, true) | |
| 107 | + | w := m.Current() | |
| 108 | + | w.Activate(1) | |
| 109 | + | w.RemoveWindow(2) | |
| 110 | + | if w.left != 0 { | |
| 111 | + | t.Fatalf("left %d", w.left) | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // Activating a hidden stashed window leaves another window's fullscreen | |
| 116 | + | // with the latch too. | |
| 117 | + | func TestActivateHiddenStashLatches(t *testing.T) { | |
| 118 | + | m := newMonitor("", "") | |
| 119 | + | m.SetOutput(300, 200) | |
| 120 | + | m.SetOverflow(OverflowFixed) | |
| 121 | + | w := m.Current() | |
| 122 | + | w.AddWindow(1) | |
| 123 | + | w.AddWindow(5) | |
| 124 | + | w.FocusID(5) | |
| 125 | + | w.ToggleWindowStash() | |
| 126 | + | w.ToggleStashVisible() | |
| 127 | + | w.FocusID(1) | |
| 128 | + | m.ToggleFullscreen() | |
| 129 | + | w.Activate(5) | |
| 130 | + | if f, _ := w.Focused(); f != 5 || w.fullscreen != 0 || w.left != 1 { | |
| 131 | + | t.Fatalf("focused %d, fullscreen %d, left %d", f, w.fullscreen, w.left) | |
| 132 | + | } | |
| 133 | + | } | |
| @@ -0,0 +1,133 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | // After the user leaves a window's fullscreen for another window, the | |||
| 6 | + | // client's next fullscreen request is refused until the window is focused | |||
| 7 | + | // again, whichever way the user left: a focus move, an activation or the | |||
| 8 | + | // overview. | |||
| 9 | + | func TestLeftFullscreenRefusesRequest(t *testing.T) { | |||
| 10 | + | leaves := map[string]func(m *Monitor){ | |||
| 11 | + | "focus move": func(m *Monitor) { m.Current().FocusColumn(-1) }, | |||
| 12 | + | "activate": func(m *Monitor) { m.Current().Activate(1) }, | |||
| 13 | + | "overview": func(m *Monitor) { | |||
| 14 | + | m.ToggleOverview() | |||
| 15 | + | m.OverviewPick(1) | |||
| 16 | + | }, | |||
| 17 | + | } | |||
| 18 | + | // Fixed overflow refuses an unfocused window's request anyway; scroll | |||
| 19 | + | // with a float is where the latch alone refuses it. | |||
| 20 | + | for _, tc := range pinnedCases { | |||
| 21 | + | for name, leave := range leaves { | |||
| 22 | + | t.Run(tc.name+"/"+name, func(t *testing.T) { | |||
| 23 | + | m := coverMonitor(tc.overflow, tc.float) | |||
| 24 | + | w := m.Current() | |||
| 25 | + | leave(m) | |||
| 26 | + | if f, _ := m.Focused(); f != 1 || w.fullscreen != 0 || w.left != 2 { | |||
| 27 | + | t.Fatalf("leave: focused %d, fullscreen %d, left %d", f, w.fullscreen, w.left) | |||
| 28 | + | } | |||
| 29 | + | m.SetFullscreen(2, true) | |||
| 30 | + | if f, _ := m.Focused(); f != 1 || w.fullscreen != 0 { | |||
| 31 | + | t.Fatalf("re-request: focused %d, fullscreen %d", f, w.fullscreen) | |||
| 32 | + | } | |||
| 33 | + | w.FocusID(2) | |||
| 34 | + | m.SetFullscreen(2, true) | |||
| 35 | + | if w.fullscreen != 2 || w.left != 0 { | |||
| 36 | + | t.Fatalf("focused again: fullscreen %d, left %d", w.fullscreen, w.left) | |||
| 37 | + | } | |||
| 38 | + | }) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // The bind leaves a window's own fullscreen without the latch: the app | |||
| 44 | + | // may ask again at once. | |||
| 45 | + | func TestToggleFullscreenOffNoLatch(t *testing.T) { | |||
| 46 | + | m := coverMonitor(OverflowFixed, false) | |||
| 47 | + | w := m.Current() | |||
| 48 | + | m.ToggleFullscreen() | |||
| 49 | + | if w.fullscreen != 0 || w.left != 0 { | |||
| 50 | + | t.Fatalf("fullscreen %d, left %d", w.fullscreen, w.left) | |||
| 51 | + | } | |||
| 52 | + | m.SetFullscreen(2, true) | |||
| 53 | + | if w.fullscreen != 2 { | |||
| 54 | + | t.Fatalf("request refused: %d", w.fullscreen) | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // Focusing the window again drops the latch even without a request, so a | |||
| 59 | + | // later request while unfocused follows the usual rules again. | |||
| 60 | + | func TestLeftFullscreenSettlesOnFocus(t *testing.T) { | |||
| 61 | + | m := coverMonitor(OverflowScroll, true) | |||
| 62 | + | w := m.Current() | |||
| 63 | + | w.Activate(1) | |||
| 64 | + | w.FocusID(2) | |||
| 65 | + | w.settleLeft() | |||
| 66 | + | if w.left != 0 { | |||
| 67 | + | t.Fatalf("left %d", w.left) | |||
| 68 | + | } | |||
| 69 | + | w.FocusID(1) | |||
| 70 | + | m.SetFullscreen(2, true) | |||
| 71 | + | if w.fullscreen != 2 { | |||
| 72 | + | t.Fatalf("request refused: %d", w.fullscreen) | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | // A taskbar fullscreen request activates the window first (core), so it | |||
| 77 | + | // passes the latch. | |||
| 78 | + | func TestLeftFullscreenTaskbar(t *testing.T) { | |||
| 79 | + | m := coverMonitor(OverflowScroll, true) | |||
| 80 | + | w := m.Current() | |||
| 81 | + | w.Activate(1) | |||
| 82 | + | w.Activate(2) | |||
| 83 | + | m.SetFullscreen(2, true) | |||
| 84 | + | if w.fullscreen != 2 || w.left != 0 { | |||
| 85 | + | t.Fatalf("fullscreen %d, left %d", w.fullscreen, w.left) | |||
| 86 | + | } | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // A latched window moved away with its column, which another window of | |||
| 90 | + | // it has the focus of, drops the latch. | |||
| 91 | + | func TestLeftFullscreenColumnMoved(t *testing.T) { | |||
| 92 | + | w := cols(OverflowScroll, 2, 0, []WindowID{2, 3}, []WindowID{1}) | |||
| 93 | + | w.FocusID(2) | |||
| 94 | + | w.fullscreen = 2 | |||
| 95 | + | w.Activate(3) | |||
| 96 | + | if w.left != 2 { | |||
| 97 | + | t.Fatalf("latch %d", w.left) | |||
| 98 | + | } | |||
| 99 | + | if _, ok := w.takeColumn(); !ok || w.left != 0 { | |||
| 100 | + | t.Fatalf("left %d", w.left) | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // A latched window that goes away drops the latch. | |||
| 105 | + | func TestLeftFullscreenRemoved(t *testing.T) { | |||
| 106 | + | m := coverMonitor(OverflowScroll, true) | |||
| 107 | + | w := m.Current() | |||
| 108 | + | w.Activate(1) | |||
| 109 | + | w.RemoveWindow(2) | |||
| 110 | + | if w.left != 0 { | |||
| 111 | + | t.Fatalf("left %d", w.left) | |||
| 112 | + | } | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | // Activating a hidden stashed window leaves another window's fullscreen | |||
| 116 | + | // with the latch too. | |||
| 117 | + | func TestActivateHiddenStashLatches(t *testing.T) { | |||
| 118 | + | m := newMonitor("", "") | |||
| 119 | + | m.SetOutput(300, 200) | |||
| 120 | + | m.SetOverflow(OverflowFixed) | |||
| 121 | + | w := m.Current() | |||
| 122 | + | w.AddWindow(1) | |||
| 123 | + | w.AddWindow(5) | |||
| 124 | + | w.FocusID(5) | |||
| 125 | + | w.ToggleWindowStash() | |||
| 126 | + | w.ToggleStashVisible() | |||
| 127 | + | w.FocusID(1) | |||
| 128 | + | m.ToggleFullscreen() | |||
| 129 | + | w.Activate(5) | |||
| 130 | + | if f, _ := w.Focused(); f != 5 || w.fullscreen != 0 || w.left != 1 { | |||
| 131 | + | t.Fatalf("focused %d, fullscreen %d, left %d", f, w.fullscreen, w.left) | |||
| 132 | + | } | |||
| 133 | + | } | |||
M internal/core/layout.go +43 -6
| @@ -171,6 +171,10 @@ type Workspace struct { | |||
| 171 | 171 | border int | |
| 172 | 172 | presets []Width | |
| 173 | 173 | fullscreen WindowID | |
| 174 | + | // left is the window whose fullscreen the user left for another | |
| 175 | + | // window: its client requests are refused until it is focused again, | |
| 176 | + | // so an app that re-asks at once (Wine) does not cover that window. | |
| 177 | + | left WindowID | |
| 174 | 178 | // Floats are native floating windows in user selection order; | |
| 175 | 179 | // below/above and dialog groups determine their layout order. | |
| 176 | 180 | Floats []Float | |
| @@ -450,6 +454,9 @@ func (w *Workspace) RemoveWindow(id WindowID) { | |||
| 450 | 454 | if w.hiddenFullscreen == id { | |
| 451 | 455 | w.hiddenFullscreen = 0 | |
| 452 | 456 | } | |
| 457 | + | if w.left == id { | |
| 458 | + | w.left = 0 | |
| 459 | + | } | |
| 453 | 460 | if i := w.floatIndex(id); i >= 0 { | |
| 454 | 461 | w.Floats = slices.Delete(w.Floats, i, i+1) | |
| 455 | 462 | if len(w.Floats) == 0 || w.Floats[len(w.Floats)-1].below { | |
| @@ -811,6 +818,9 @@ func (w *Workspace) takeColumn() (Column, bool) { | |||
| 811 | 818 | if slices.Contains(col.Windows, w.fullscreen) { | |
| 812 | 819 | w.fullscreen = 0 | |
| 813 | 820 | } | |
| 821 | + | if slices.Contains(col.Windows, w.left) { | |
| 822 | + | w.left = 0 | |
| 823 | + | } | |
| 814 | 824 | w.Columns = slices.Delete(w.Columns, w.Focus, w.Focus+1) | |
| 815 | 825 | w.Focus = min(w.Focus, max(len(w.Columns)-1, 0)) | |
| 816 | 826 | if len(w.Columns) == 0 { | |
| @@ -945,6 +955,16 @@ func (w *Workspace) endFullscreen() { | |||
| 945 | 955 | w.scroll() | |
| 946 | 956 | } | |
| 947 | 957 | ||
| 958 | + | // leaveFullscreen ends the fullscreen for another window the user chose, | |
| 959 | + | // and refuses its client's requests until it is focused again (mayCover, | |
| 960 | + | // settleLeft). | |
| 961 | + | func (w *Workspace) leaveFullscreen() { | |
| 962 | + | if w.fullscreen != 0 { | |
| 963 | + | w.left = w.fullscreen | |
| 964 | + | } | |
| 965 | + | w.endFullscreen() | |
| 966 | + | } | |
| 967 | + | ||
| 948 | 968 | // Activate focuses a window and makes it visible: it leaves another | |
| 949 | 969 | // window's fullscreen, which hides everything else. | |
| 950 | 970 | func (w *Workspace) Activate(id WindowID) { | |
| @@ -952,12 +972,12 @@ func (w *Workspace) Activate(id WindowID) { | |||
| 952 | 972 | // A hidden stashed window must be seen: show the stash with it, | |
| 953 | 973 | // selected. Another window's fullscreen leaves first, so its own | |
| 954 | 974 | // pending fullscreen comes back. | |
| 955 | - | w.endFullscreen() | |
| 975 | + | w.leaveFullscreen() | |
| 956 | 976 | w.stashAt = i | |
| 957 | 977 | w.showStash() | |
| 958 | 978 | } | |
| 959 | 979 | if w.fullscreen != 0 && w.fullscreen != id { | |
| 960 | - | w.endFullscreen() | |
| 980 | + | w.leaveFullscreen() | |
| 961 | 981 | } | |
| 962 | 982 | w.FocusID(id) | |
| 963 | 983 | } | |
| @@ -1064,12 +1084,27 @@ func (w *Workspace) overviewArea() Rect { return w.reserved } | |||
| 1064 | 1084 | ||
| 1065 | 1085 | func (w *Workspace) gap() int { return min(w.Gaps, w.Usable.W/2, w.Usable.H/2) } | |
| 1066 | 1086 | ||
| 1087 | + | // settleLeft drops the latch once its window has the focus again, however | |
| 1088 | + | // it got it; core calls it after each event. | |
| 1089 | + | func (w *Workspace) settleLeft() { | |
| 1090 | + | if id, ok := w.Focused(); ok && id == w.left { | |
| 1091 | + | w.left = 0 | |
| 1092 | + | } | |
| 1093 | + | } | |
| 1094 | + | ||
| 1067 | 1095 | // mayCover reports whether a client may make id fullscreen. In fixed | |
| 1068 | 1096 | // overflow it would cover the output at once and take the keyboard from | |
| 1069 | 1097 | // the focused window, so only the focused window may; the request of | |
| 1070 | 1098 | // another is refused and the user can still toggle-fullscreen it. A | |
| 1071 | - | // hidden stash's request waits for the show (showStash). | |
| 1099 | + | // hidden stash's request waits for the show (showStash). A window whose | |
| 1100 | + | // fullscreen the user left may only once focused again, in any overflow. | |
| 1072 | 1101 | func (w *Workspace) mayCover(id WindowID) bool { | |
| 1102 | + | if id == w.left { | |
| 1103 | + | if focused, ok := w.Focused(); !ok || focused != id { | |
| 1104 | + | return false | |
| 1105 | + | } | |
| 1106 | + | w.left = 0 | |
| 1107 | + | } | |
| 1073 | 1108 | if w.Overflow != OverflowFixed || w.stashHidden && w.stashIndex(id) >= 0 { | |
| 1074 | 1109 | return true | |
| 1075 | 1110 | } | |
| @@ -1078,8 +1113,9 @@ func (w *Workspace) mayCover(id WindowID) bool { | |||
| 1078 | 1113 | } | |
| 1079 | 1114 | ||
| 1080 | 1115 | // pinned reports whether the covering fullscreen window hides every other | |
| 1081 | - | // window: focus moves leave it (leaveCover). Only in scroll overflow does | |
| 1082 | - | // moving to a tiled neighbor scroll it off and show the target instead. | |
| 1116 | + | // window: focus moves leave it (leaveCover), and the overview shows it as | |
| 1117 | + | // the front card of its row. Only in scroll overflow does moving to a | |
| 1118 | + | // tiled neighbor scroll it off and show the target instead. | |
| 1083 | 1119 | func (w *Workspace) pinned() bool { | |
| 1084 | 1120 | c := w.cover() | |
| 1085 | 1121 | return c != 0 && (w.Overflow == OverflowFixed || w.isFloat(c)) | |
| @@ -1095,7 +1131,8 @@ func (w *Workspace) leaveCover(move func()) bool { | |||
| 1095 | 1131 | w.fullscreen = 0 | |
| 1096 | 1132 | move() | |
| 1097 | 1133 | if id, ok := w.Focused(); ok && id != full { | |
| 1098 | - | w.endFullscreen() | |
| 1134 | + | w.fullscreen = full | |
| 1135 | + | w.leaveFullscreen() | |
| 1099 | 1136 | return true | |
| 1100 | 1137 | } | |
| 1101 | 1138 | w.fullscreen = full | |
| @@ -171,6 +171,10 @@ type Workspace struct { | |||||
| 171 | border int | 171 | border int | ||
| 172 | presets []Width | 172 | presets []Width | ||
| 173 | fullscreen WindowID | 173 | fullscreen WindowID | ||
| 174 | + | // left is the window whose fullscreen the user left for another | |||
| 175 | + | // window: its client requests are refused until it is focused again, | |||
| 176 | + | // so an app that re-asks at once (Wine) does not cover that window. | |||
| 177 | + | left WindowID | |||
| 174 | // Floats are native floating windows in user selection order; | 178 | // Floats are native floating windows in user selection order; | ||
| 175 | // below/above and dialog groups determine their layout order. | 179 | // below/above and dialog groups determine their layout order. | ||
| 176 | Floats []Float | 180 | Floats []Float | ||
| @@ -450,6 +454,9 @@ func (w *Workspace) RemoveWindow(id WindowID) { | |||||
| 450 | if w.hiddenFullscreen == id { | 454 | if w.hiddenFullscreen == id { | ||
| 451 | w.hiddenFullscreen = 0 | 455 | w.hiddenFullscreen = 0 | ||
| 452 | } | 456 | } | ||
| 457 | + | if w.left == id { | |||
| 458 | + | w.left = 0 | |||
| 459 | + | } | |||
| 453 | if i := w.floatIndex(id); i >= 0 { | 460 | if i := w.floatIndex(id); i >= 0 { | ||
| 454 | w.Floats = slices.Delete(w.Floats, i, i+1) | 461 | w.Floats = slices.Delete(w.Floats, i, i+1) | ||
| 455 | if len(w.Floats) == 0 || w.Floats[len(w.Floats)-1].below { | 462 | if len(w.Floats) == 0 || w.Floats[len(w.Floats)-1].below { | ||
| @@ -811,6 +818,9 @@ func (w *Workspace) takeColumn() (Column, bool) { | |||||
| 811 | if slices.Contains(col.Windows, w.fullscreen) { | 818 | if slices.Contains(col.Windows, w.fullscreen) { | ||
| 812 | w.fullscreen = 0 | 819 | w.fullscreen = 0 | ||
| 813 | } | 820 | } | ||
| 821 | + | if slices.Contains(col.Windows, w.left) { | |||
| 822 | + | w.left = 0 | |||
| 823 | + | } | |||
| 814 | w.Columns = slices.Delete(w.Columns, w.Focus, w.Focus+1) | 824 | w.Columns = slices.Delete(w.Columns, w.Focus, w.Focus+1) | ||
| 815 | w.Focus = min(w.Focus, max(len(w.Columns)-1, 0)) | 825 | w.Focus = min(w.Focus, max(len(w.Columns)-1, 0)) | ||
| 816 | if len(w.Columns) == 0 { | 826 | if len(w.Columns) == 0 { | ||
| @@ -945,6 +955,16 @@ func (w *Workspace) endFullscreen() { | |||||
| 945 | w.scroll() | 955 | w.scroll() | ||
| 946 | } | 956 | } | ||
| 947 | 957 | ||||
| 958 | + | // leaveFullscreen ends the fullscreen for another window the user chose, | |||
| 959 | + | // and refuses its client's requests until it is focused again (mayCover, | |||
| 960 | + | // settleLeft). | |||
| 961 | + | func (w *Workspace) leaveFullscreen() { | |||
| 962 | + | if w.fullscreen != 0 { | |||
| 963 | + | w.left = w.fullscreen | |||
| 964 | + | } | |||
| 965 | + | w.endFullscreen() | |||
| 966 | + | } | |||
| 967 | + | ||||
| 948 | // Activate focuses a window and makes it visible: it leaves another | 968 | // Activate focuses a window and makes it visible: it leaves another | ||
| 949 | // window's fullscreen, which hides everything else. | 969 | // window's fullscreen, which hides everything else. | ||
| 950 | func (w *Workspace) Activate(id WindowID) { | 970 | func (w *Workspace) Activate(id WindowID) { | ||
| @@ -952,12 +972,12 @@ func (w *Workspace) Activate(id WindowID) { | |||||
| 952 | // A hidden stashed window must be seen: show the stash with it, | 972 | // A hidden stashed window must be seen: show the stash with it, | ||
| 953 | // selected. Another window's fullscreen leaves first, so its own | 973 | // selected. Another window's fullscreen leaves first, so its own | ||
| 954 | // pending fullscreen comes back. | 974 | // pending fullscreen comes back. | ||
| 955 | - | w.endFullscreen() | 975 | + | w.leaveFullscreen() |
| 956 | w.stashAt = i | 976 | w.stashAt = i | ||
| 957 | w.showStash() | 977 | w.showStash() | ||
| 958 | } | 978 | } | ||
| 959 | if w.fullscreen != 0 && w.fullscreen != id { | 979 | if w.fullscreen != 0 && w.fullscreen != id { | ||
| 960 | - | w.endFullscreen() | 980 | + | w.leaveFullscreen() |
| 961 | } | 981 | } | ||
| 962 | w.FocusID(id) | 982 | w.FocusID(id) | ||
| 963 | } | 983 | } | ||
| @@ -1064,12 +1084,27 @@ func (w *Workspace) overviewArea() Rect { return w.reserved } | |||||
| 1064 | 1084 | ||||
| 1065 | func (w *Workspace) gap() int { return min(w.Gaps, w.Usable.W/2, w.Usable.H/2) } | 1085 | func (w *Workspace) gap() int { return min(w.Gaps, w.Usable.W/2, w.Usable.H/2) } | ||
| 1066 | 1086 | ||||
| 1087 | + | // settleLeft drops the latch once its window has the focus again, however | |||
| 1088 | + | // it got it; core calls it after each event. | |||
| 1089 | + | func (w *Workspace) settleLeft() { | |||
| 1090 | + | if id, ok := w.Focused(); ok && id == w.left { | |||
| 1091 | + | w.left = 0 | |||
| 1092 | + | } | |||
| 1093 | + | } | |||
| 1094 | + | ||||
| 1067 | // mayCover reports whether a client may make id fullscreen. In fixed | 1095 | // mayCover reports whether a client may make id fullscreen. In fixed | ||
| 1068 | // overflow it would cover the output at once and take the keyboard from | 1096 | // overflow it would cover the output at once and take the keyboard from | ||
| 1069 | // the focused window, so only the focused window may; the request of | 1097 | // the focused window, so only the focused window may; the request of | ||
| 1070 | // another is refused and the user can still toggle-fullscreen it. A | 1098 | // another is refused and the user can still toggle-fullscreen it. A | ||
| 1071 | - | // hidden stash's request waits for the show (showStash). | 1099 | + | // hidden stash's request waits for the show (showStash). A window whose |
| 1100 | + | // fullscreen the user left may only once focused again, in any overflow. | |||
| 1072 | func (w *Workspace) mayCover(id WindowID) bool { | 1101 | func (w *Workspace) mayCover(id WindowID) bool { | ||
| 1102 | + | if id == w.left { | |||
| 1103 | + | if focused, ok := w.Focused(); !ok || focused != id { | |||
| 1104 | + | return false | |||
| 1105 | + | } | |||
| 1106 | + | w.left = 0 | |||
| 1107 | + | } | |||
| 1073 | if w.Overflow != OverflowFixed || w.stashHidden && w.stashIndex(id) >= 0 { | 1108 | if w.Overflow != OverflowFixed || w.stashHidden && w.stashIndex(id) >= 0 { | ||
| 1074 | return true | 1109 | return true | ||
| 1075 | } | 1110 | } | ||
| @@ -1078,8 +1113,9 @@ func (w *Workspace) mayCover(id WindowID) bool { | |||||
| 1078 | } | 1113 | } | ||
| 1079 | 1114 | ||||
| 1080 | // pinned reports whether the covering fullscreen window hides every other | 1115 | // pinned reports whether the covering fullscreen window hides every other | ||
| 1081 | - | // window: focus moves leave it (leaveCover). Only in scroll overflow does | 1116 | + | // window: focus moves leave it (leaveCover), and the overview shows it as |
| 1082 | - | // moving to a tiled neighbor scroll it off and show the target instead. | 1117 | + | // the front card of its row. Only in scroll overflow does moving to a |
| 1118 | + | // tiled neighbor scroll it off and show the target instead. | |||
| 1083 | func (w *Workspace) pinned() bool { | 1119 | func (w *Workspace) pinned() bool { | ||
| 1084 | c := w.cover() | 1120 | c := w.cover() | ||
| 1085 | return c != 0 && (w.Overflow == OverflowFixed || w.isFloat(c)) | 1121 | return c != 0 && (w.Overflow == OverflowFixed || w.isFloat(c)) | ||
| @@ -1095,7 +1131,8 @@ func (w *Workspace) leaveCover(move func()) bool { | |||||
| 1095 | w.fullscreen = 0 | 1131 | w.fullscreen = 0 | ||
| 1096 | move() | 1132 | move() | ||
| 1097 | if id, ok := w.Focused(); ok && id != full { | 1133 | if id, ok := w.Focused(); ok && id != full { | ||
| 1098 | - | w.endFullscreen() | 1134 | + | w.fullscreen = full |
| 1135 | + | w.leaveFullscreen() | |||
| 1099 | return true | 1136 | return true | ||
| 1100 | } | 1137 | } | ||
| 1101 | w.fullscreen = full | 1138 | w.fullscreen = full | ||
M internal/core/overview.go +30 -34
| @@ -44,7 +44,8 @@ type rowSnapshot struct { | |||
| 44 | 44 | // The overview scales workspace rows without resizing clients. Covering | |
| 45 | 45 | // floats and the columns form a linear stack of cards; small floats stay | |
| 46 | 46 | // hidden. Up/down traverse the stack before changing workspace. A pinned | |
| 47 | - | // fullscreen window remains the only preview on its row. | |
| 47 | + | // fullscreen window is the front card of its row, the windows it hides | |
| 48 | + | // behind it; choosing one of them leaves fullscreen. | |
| 48 | 49 | ||
| 49 | 50 | // A workspace's stash shows as a pile of cards pinned to the left edge of | |
| 50 | 51 | // its row: the selected stashed window in front at overviewCardZoom, up to | |
| @@ -108,9 +109,8 @@ func (m *Monitor) selectRow() { | |||
| 108 | 109 | m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0 | |
| 109 | 110 | m.ov.selected, m.ov.selectedAt = 0, -1 | |
| 110 | 111 | w := m.Current() | |
| 111 | - | if w.pinned() { | |
| 112 | - | return | |
| 113 | - | } | |
| 112 | + | // A fullscreen window has the focus: its card is in front. | |
| 113 | + | w.focusCover() | |
| 114 | 114 | if w.floatFocus && len(w.Columns) > 0 && (len(w.stack()) < 2 || m.stackFront(w).kind == stackColumns) { | |
| 115 | 115 | w.floatFocus = false | |
| 116 | 116 | } | |
| @@ -150,9 +150,6 @@ func (m *Monitor) overviewTarget() WindowID { | |||
| 150 | 150 | return id | |
| 151 | 151 | } | |
| 152 | 152 | w := m.Current() | |
| 153 | - | if w.pinned() { | |
| 154 | - | return w.cover() | |
| 155 | - | } | |
| 156 | 153 | item := m.stackFront(w) | |
| 157 | 154 | switch item.kind { | |
| 158 | 155 | case stackFloat: | |
| @@ -211,6 +208,10 @@ func (m *Monitor) overviewMoveTo(a Action) { | |||
| 211 | 208 | if m.card() != 0 || id == 0 || !ok || m.Workspaces[i] == cur { | |
| 212 | 209 | return | |
| 213 | 210 | } | |
| 211 | + | if cur.pinned() && id != cur.cover() { | |
| 212 | + | // Focused reports the covering window while it covers. | |
| 213 | + | cur.leaveFullscreen() | |
| 214 | + | } | |
| 214 | 215 | cur.FocusID(id) | |
| 215 | 216 | m.MoveToWorkspace(i, column) | |
| 216 | 217 | // The row that received the window keeps it focused on close. | |
| @@ -222,17 +223,22 @@ func (m *Monitor) overviewMoveTo(a Action) { | |||
| 222 | 223 | ||
| 223 | 224 | // closeOverview closes the overview on the selection: a card shows the | |
| 224 | 225 | // stash on it. A fullscreen request another stashed window made while | |
| 225 | - | // the stash was hidden is dropped: the picked card stays in front. | |
| 226 | + | // the stash was hidden is dropped: the picked card stays in front. A | |
| 227 | + | // window a pinned fullscreen window hides leaves that fullscreen. | |
| 226 | 228 | func (m *Monitor) closeOverview() { | |
| 227 | 229 | w := m.Current() | |
| 230 | + | front, target := m.stackFront(w), m.overviewTarget() | |
| 231 | + | if w.pinned() && target != w.cover() { | |
| 232 | + | w.leaveFullscreen() | |
| 233 | + | } | |
| 228 | 234 | if i := m.cardAt(w); i >= 0 { | |
| 229 | 235 | if w.hiddenFullscreen != w.Stash[i].ID { | |
| 230 | 236 | w.hiddenFullscreen = 0 | |
| 231 | 237 | } | |
| 232 | 238 | w.stashAt = i | |
| 233 | 239 | w.showStash() | |
| 234 | - | } else if !w.pinned() && len(w.stack()) > 0 { | |
| 235 | - | w.apply(m.stackFront(w), m.overviewTarget()) | |
| 240 | + | } else if front != (stackItem{}) { | |
| 241 | + | w.apply(front, target) | |
| 236 | 242 | } | |
| 237 | 243 | for r, s := range m.ov.rows { | |
| 238 | 244 | if r != w && m.has(r) { | |
| @@ -373,9 +379,6 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||
| 373 | 379 | m.showOverview(rows[i]) | |
| 374 | 380 | return | |
| 375 | 381 | } | |
| 376 | - | if w.pinned() { | |
| 377 | - | return | |
| 378 | - | } | |
| 379 | 382 | if at := m.cardAt(w); at >= 0 { | |
| 380 | 383 | switch { | |
| 381 | 384 | case at+dx >= 0 && at+dx < len(w.Stash): | |
| @@ -447,7 +450,7 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||
| 447 | 450 | } | |
| 448 | 451 | if w.floatIndex(id) >= 0 && w.stashIndex(id) < 0 { | |
| 449 | 452 | i := w.floatIndex(id) | |
| 450 | - | if !w.coversFloat(w.Floats[i]) && !w.pinned() { | |
| 453 | + | if !w.coversFloat(w.Floats[i]) && !(id == w.cover() && w.pinned()) { | |
| 451 | 454 | return | |
| 452 | 455 | } | |
| 453 | 456 | } | |
| @@ -455,8 +458,6 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||
| 455 | 458 | m.ov.card, m.ov.cardOf = 0, nil | |
| 456 | 459 | if i := w.stashIndex(id); i >= 0 { | |
| 457 | 460 | m.selectCard(w, i) | |
| 458 | - | } else if w.pinned() { | |
| 459 | - | w.FocusID(id) | |
| 460 | 461 | } else if item, ok := w.itemOf(id); ok { | |
| 461 | 462 | m.setFront(w, item) | |
| 462 | 463 | if i := w.columnOf(id); i >= 0 { | |
| @@ -522,16 +523,6 @@ func (m *Monitor) overviewLayout() []Placement { | |||
| 522 | 523 | var result []Placement | |
| 523 | 524 | for w := range m.all() { | |
| 524 | 525 | ry, shown := rows[w] | |
| 525 | - | if shown && w.pinned() { | |
| 526 | - | // Only the covering window shows, as on screen. | |
| 527 | - | result = append(result, w.previewRow(ry, w != cur, true)...) | |
| 528 | - | for _, id := range w.windows() { | |
| 529 | - | if id != w.fullscreen { | |
| 530 | - | result = append(result, Placement{ID: id, Hidden: true}) | |
| 531 | - | } | |
| 532 | - | } | |
| 533 | - | continue | |
| 534 | - | } | |
| 535 | 526 | if shown { | |
| 536 | 527 | at := m.cardAt(w) | |
| 537 | 528 | result = append(result, m.stackRow(w, ry, w != cur, at < 0)...) | |
| @@ -622,7 +613,7 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | |||
| 622 | 613 | // pileWidth is the room the stash pile of w takes on the left of its row, | |
| 623 | 614 | // its margin included; 0 without a stash. | |
| 624 | 615 | func (w *Workspace) pileWidth() int { | |
| 625 | - | if len(w.Stash) == 0 || w.pinned() { | |
| 616 | + | if len(w.Stash) == 0 { | |
| 626 | 617 | return 0 | |
| 627 | 618 | } | |
| 628 | 619 | card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom)) | |
| @@ -713,15 +704,20 @@ func (w *Workspace) rowHeight() int { | |||
| 713 | 704 | // corner. Fixed overflow uses its on-screen geometry; scroll overflow uses | |
| 714 | 705 | // an unscrolled row. span is the row's width; sel is the focused column. | |
| 715 | 706 | func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) { | |
| 716 | - | g := w.gap() | |
| 717 | - | h := max(w.Usable.H-2*g, 0) | |
| 718 | 707 | if w.pinned() { | |
| 719 | - | // A covering window nothing else is reachable under (a game) is | |
| 720 | - | // the row's only tile, at the size of a fullscreen column. It | |
| 721 | - | // stays fullscreen. | |
| 722 | - | r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: h} | |
| 723 | - | return []Placement{{ID: w.cover(), Rect: r, Focused: true, Fullscreen: true}}, r.X + r.W + g, r | |
| 708 | + | c := w.cover() | |
| 709 | + | // The row shows the columns a pinned fullscreen window hides, | |
| 710 | + | // laid out as without it; its own tile stays marked fullscreen. | |
| 711 | + | v := *w | |
| 712 | + | v.fullscreen = 0 | |
| 713 | + | tiles, span, sel = v.previewTiles() | |
| 714 | + | for i := range tiles { | |
| 715 | + | tiles[i].Fullscreen = tiles[i].ID == c | |
| 716 | + | } | |
| 717 | + | return tiles, span, sel | |
| 724 | 718 | } | |
| 719 | + | g := w.gap() | |
| 720 | + | h := max(w.Usable.H-2*g, 0) | |
| 725 | 721 | var rects []Rect | |
| 726 | 722 | if w.Overflow == OverflowFixed { | |
| 727 | 723 | rects = w.columnRects() | |
| @@ -44,7 +44,8 @@ type rowSnapshot struct { | |||||
| 44 | // The overview scales workspace rows without resizing clients. Covering | 44 | // The overview scales workspace rows without resizing clients. Covering | ||
| 45 | // floats and the columns form a linear stack of cards; small floats stay | 45 | // floats and the columns form a linear stack of cards; small floats stay | ||
| 46 | // hidden. Up/down traverse the stack before changing workspace. A pinned | 46 | // hidden. Up/down traverse the stack before changing workspace. A pinned | ||
| 47 | - | // fullscreen window remains the only preview on its row. | 47 | + | // fullscreen window is the front card of its row, the windows it hides |
| 48 | + | // behind it; choosing one of them leaves fullscreen. | |||
| 48 | 49 | ||||
| 49 | // A workspace's stash shows as a pile of cards pinned to the left edge of | 50 | // A workspace's stash shows as a pile of cards pinned to the left edge of | ||
| 50 | // its row: the selected stashed window in front at overviewCardZoom, up to | 51 | // its row: the selected stashed window in front at overviewCardZoom, up to | ||
| @@ -108,9 +109,8 @@ func (m *Monitor) selectRow() { | |||||
| 108 | m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0 | 109 | m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0 | ||
| 109 | m.ov.selected, m.ov.selectedAt = 0, -1 | 110 | m.ov.selected, m.ov.selectedAt = 0, -1 | ||
| 110 | w := m.Current() | 111 | w := m.Current() | ||
| 111 | - | if w.pinned() { | 112 | + | // A fullscreen window has the focus: its card is in front. |
| 112 | - | return | 113 | + | w.focusCover() |
| 113 | - | } | |||
| 114 | if w.floatFocus && len(w.Columns) > 0 && (len(w.stack()) < 2 || m.stackFront(w).kind == stackColumns) { | 114 | if w.floatFocus && len(w.Columns) > 0 && (len(w.stack()) < 2 || m.stackFront(w).kind == stackColumns) { | ||
| 115 | w.floatFocus = false | 115 | w.floatFocus = false | ||
| 116 | } | 116 | } | ||
| @@ -150,9 +150,6 @@ func (m *Monitor) overviewTarget() WindowID { | |||||
| 150 | return id | 150 | return id | ||
| 151 | } | 151 | } | ||
| 152 | w := m.Current() | 152 | w := m.Current() | ||
| 153 | - | if w.pinned() { | |||
| 154 | - | return w.cover() | |||
| 155 | - | } | |||
| 156 | item := m.stackFront(w) | 153 | item := m.stackFront(w) | ||
| 157 | switch item.kind { | 154 | switch item.kind { | ||
| 158 | case stackFloat: | 155 | case stackFloat: | ||
| @@ -211,6 +208,10 @@ func (m *Monitor) overviewMoveTo(a Action) { | |||||
| 211 | if m.card() != 0 || id == 0 || !ok || m.Workspaces[i] == cur { | 208 | if m.card() != 0 || id == 0 || !ok || m.Workspaces[i] == cur { | ||
| 212 | return | 209 | return | ||
| 213 | } | 210 | } | ||
| 211 | + | if cur.pinned() && id != cur.cover() { | |||
| 212 | + | // Focused reports the covering window while it covers. | |||
| 213 | + | cur.leaveFullscreen() | |||
| 214 | + | } | |||
| 214 | cur.FocusID(id) | 215 | cur.FocusID(id) | ||
| 215 | m.MoveToWorkspace(i, column) | 216 | m.MoveToWorkspace(i, column) | ||
| 216 | // The row that received the window keeps it focused on close. | 217 | // The row that received the window keeps it focused on close. | ||
| @@ -222,17 +223,22 @@ func (m *Monitor) overviewMoveTo(a Action) { | |||||
| 222 | 223 | ||||
| 223 | // closeOverview closes the overview on the selection: a card shows the | 224 | // closeOverview closes the overview on the selection: a card shows the | ||
| 224 | // stash on it. A fullscreen request another stashed window made while | 225 | // stash on it. A fullscreen request another stashed window made while | ||
| 225 | - | // the stash was hidden is dropped: the picked card stays in front. | 226 | + | // the stash was hidden is dropped: the picked card stays in front. A |
| 227 | + | // window a pinned fullscreen window hides leaves that fullscreen. | |||
| 226 | func (m *Monitor) closeOverview() { | 228 | func (m *Monitor) closeOverview() { | ||
| 227 | w := m.Current() | 229 | w := m.Current() | ||
| 230 | + | front, target := m.stackFront(w), m.overviewTarget() | |||
| 231 | + | if w.pinned() && target != w.cover() { | |||
| 232 | + | w.leaveFullscreen() | |||
| 233 | + | } | |||
| 228 | if i := m.cardAt(w); i >= 0 { | 234 | if i := m.cardAt(w); i >= 0 { | ||
| 229 | if w.hiddenFullscreen != w.Stash[i].ID { | 235 | if w.hiddenFullscreen != w.Stash[i].ID { | ||
| 230 | w.hiddenFullscreen = 0 | 236 | w.hiddenFullscreen = 0 | ||
| 231 | } | 237 | } | ||
| 232 | w.stashAt = i | 238 | w.stashAt = i | ||
| 233 | w.showStash() | 239 | w.showStash() | ||
| 234 | - | } else if !w.pinned() && len(w.stack()) > 0 { | 240 | + | } else if front != (stackItem{}) { |
| 235 | - | w.apply(m.stackFront(w), m.overviewTarget()) | 241 | + | w.apply(front, target) |
| 236 | } | 242 | } | ||
| 237 | for r, s := range m.ov.rows { | 243 | for r, s := range m.ov.rows { | ||
| 238 | if r != w && m.has(r) { | 244 | if r != w && m.has(r) { | ||
| @@ -373,9 +379,6 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||||
| 373 | m.showOverview(rows[i]) | 379 | m.showOverview(rows[i]) | ||
| 374 | return | 380 | return | ||
| 375 | } | 381 | } | ||
| 376 | - | if w.pinned() { | |||
| 377 | - | return | |||
| 378 | - | } | |||
| 379 | if at := m.cardAt(w); at >= 0 { | 382 | if at := m.cardAt(w); at >= 0 { | ||
| 380 | switch { | 383 | switch { | ||
| 381 | case at+dx >= 0 && at+dx < len(w.Stash): | 384 | case at+dx >= 0 && at+dx < len(w.Stash): | ||
| @@ -447,7 +450,7 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||||
| 447 | } | 450 | } | ||
| 448 | if w.floatIndex(id) >= 0 && w.stashIndex(id) < 0 { | 451 | if w.floatIndex(id) >= 0 && w.stashIndex(id) < 0 { | ||
| 449 | i := w.floatIndex(id) | 452 | i := w.floatIndex(id) | ||
| 450 | - | if !w.coversFloat(w.Floats[i]) && !w.pinned() { | 453 | + | if !w.coversFloat(w.Floats[i]) && !(id == w.cover() && w.pinned()) { |
| 451 | return | 454 | return | ||
| 452 | } | 455 | } | ||
| 453 | } | 456 | } | ||
| @@ -455,8 +458,6 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||||
| 455 | m.ov.card, m.ov.cardOf = 0, nil | 458 | m.ov.card, m.ov.cardOf = 0, nil | ||
| 456 | if i := w.stashIndex(id); i >= 0 { | 459 | if i := w.stashIndex(id); i >= 0 { | ||
| 457 | m.selectCard(w, i) | 460 | m.selectCard(w, i) | ||
| 458 | - | } else if w.pinned() { | |||
| 459 | - | w.FocusID(id) | |||
| 460 | } else if item, ok := w.itemOf(id); ok { | 461 | } else if item, ok := w.itemOf(id); ok { | ||
| 461 | m.setFront(w, item) | 462 | m.setFront(w, item) | ||
| 462 | if i := w.columnOf(id); i >= 0 { | 463 | if i := w.columnOf(id); i >= 0 { | ||
| @@ -522,16 +523,6 @@ func (m *Monitor) overviewLayout() []Placement { | |||||
| 522 | var result []Placement | 523 | var result []Placement | ||
| 523 | for w := range m.all() { | 524 | for w := range m.all() { | ||
| 524 | ry, shown := rows[w] | 525 | ry, shown := rows[w] | ||
| 525 | - | if shown && w.pinned() { | |||
| 526 | - | // Only the covering window shows, as on screen. | |||
| 527 | - | result = append(result, w.previewRow(ry, w != cur, true)...) | |||
| 528 | - | for _, id := range w.windows() { | |||
| 529 | - | if id != w.fullscreen { | |||
| 530 | - | result = append(result, Placement{ID: id, Hidden: true}) | |||
| 531 | - | } | |||
| 532 | - | } | |||
| 533 | - | continue | |||
| 534 | - | } | |||
| 535 | if shown { | 526 | if shown { | ||
| 536 | at := m.cardAt(w) | 527 | at := m.cardAt(w) | ||
| 537 | result = append(result, m.stackRow(w, ry, w != cur, at < 0)...) | 528 | result = append(result, m.stackRow(w, ry, w != cur, at < 0)...) | ||
| @@ -622,7 +613,7 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | |||||
| 622 | // pileWidth is the room the stash pile of w takes on the left of its row, | 613 | // pileWidth is the room the stash pile of w takes on the left of its row, | ||
| 623 | // its margin included; 0 without a stash. | 614 | // its margin included; 0 without a stash. | ||
| 624 | func (w *Workspace) pileWidth() int { | 615 | func (w *Workspace) pileWidth() int { | ||
| 625 | - | if len(w.Stash) == 0 || w.pinned() { | 616 | + | if len(w.Stash) == 0 { |
| 626 | return 0 | 617 | return 0 | ||
| 627 | } | 618 | } | ||
| 628 | card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom)) | 619 | card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom)) | ||
| @@ -713,15 +704,20 @@ func (w *Workspace) rowHeight() int { | |||||
| 713 | // corner. Fixed overflow uses its on-screen geometry; scroll overflow uses | 704 | // corner. Fixed overflow uses its on-screen geometry; scroll overflow uses | ||
| 714 | // an unscrolled row. span is the row's width; sel is the focused column. | 705 | // an unscrolled row. span is the row's width; sel is the focused column. | ||
| 715 | func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) { | 706 | func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) { | ||
| 716 | - | g := w.gap() | |||
| 717 | - | h := max(w.Usable.H-2*g, 0) | |||
| 718 | if w.pinned() { | 707 | if w.pinned() { | ||
| 719 | - | // A covering window nothing else is reachable under (a game) is | 708 | + | c := w.cover() |
| 720 | - | // the row's only tile, at the size of a fullscreen column. It | 709 | + | // The row shows the columns a pinned fullscreen window hides, |
| 721 | - | // stays fullscreen. | 710 | + | // laid out as without it; its own tile stays marked fullscreen. |
| 722 | - | r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: h} | 711 | + | v := *w |
| 723 | - | return []Placement{{ID: w.cover(), Rect: r, Focused: true, Fullscreen: true}}, r.X + r.W + g, r | 712 | + | v.fullscreen = 0 |
| 713 | + | tiles, span, sel = v.previewTiles() | |||
| 714 | + | for i := range tiles { | |||
| 715 | + | tiles[i].Fullscreen = tiles[i].ID == c | |||
| 716 | + | } | |||
| 717 | + | return tiles, span, sel | |||
| 724 | } | 718 | } | ||
| 719 | + | g := w.gap() | |||
| 720 | + | h := max(w.Usable.H-2*g, 0) | |||
| 725 | var rects []Rect | 721 | var rects []Rect | ||
| 726 | if w.Overflow == OverflowFixed { | 722 | if w.Overflow == OverflowFixed { | ||
| 727 | rects = w.columnRects() | 723 | rects = w.columnRects() | ||
A internal/core/overview_cover_test.go +228 -0
| @@ -0,0 +1,228 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | // coverMonitor has, on a 300x200 output, tiles 1 and 2 on one workspace, | |
| 6 | + | // 2 fullscreen. With float, 2 is a floating game over tile 1 instead. | |
| 7 | + | func coverMonitor(o Overflow, float bool) *Monitor { | |
| 8 | + | m := newMonitor("", "") | |
| 9 | + | m.SetOutput(300, 200) | |
| 10 | + | m.SetOverflow(o) | |
| 11 | + | m.SetMaxColumns(2) | |
| 12 | + | m.AddWindow(1) | |
| 13 | + | if float { | |
| 14 | + | m.AddFloating(2, 300, 200) | |
| 15 | + | m.SetFullscreen(2, true) | |
| 16 | + | } else { | |
| 17 | + | m.AddWindow(2) | |
| 18 | + | m.ToggleFullscreen() | |
| 19 | + | } | |
| 20 | + | return m | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // pinnedCases are the setups where the fullscreen window pins its row. | |
| 24 | + | var pinnedCases = []struct { | |
| 25 | + | name string | |
| 26 | + | overflow Overflow | |
| 27 | + | float bool | |
| 28 | + | }{ | |
| 29 | + | {"fixed tile", OverflowFixed, false}, | |
| 30 | + | {"fixed float", OverflowFixed, true}, | |
| 31 | + | {"scroll float", OverflowScroll, true}, | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // uniquePlacements fails when a window is placed twice. | |
| 35 | + | func uniquePlacements(t *testing.T, ps []Placement) { | |
| 36 | + | t.Helper() | |
| 37 | + | seen := map[WindowID]bool{} | |
| 38 | + | for _, p := range ps { | |
| 39 | + | if seen[p.ID] { | |
| 40 | + | t.Fatalf("window %d placed twice: %+v", p.ID, ps) | |
| 41 | + | } | |
| 42 | + | seen[p.ID] = true | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // The overview over a pinned fullscreen window shows it in front, still | |
| 47 | + | // fullscreen and selected, and the window it hides behind it. | |
| 48 | + | func TestOverviewCoverShowsWorkspace(t *testing.T) { | |
| 49 | + | for _, tc := range pinnedCases { | |
| 50 | + | t.Run(tc.name, func(t *testing.T) { | |
| 51 | + | m := coverMonitor(tc.overflow, tc.float) | |
| 52 | + | if !m.Current().pinned() { | |
| 53 | + | t.Fatal("not pinned") | |
| 54 | + | } | |
| 55 | + | m.ToggleOverview() | |
| 56 | + | ps := m.Layout() | |
| 57 | + | uniquePlacements(t, ps) | |
| 58 | + | game, behind := previewOf(t, ps, 2), previewOf(t, ps, 1) | |
| 59 | + | if game.Hidden || !game.Focused || game.Peek || !game.Fullscreen { | |
| 60 | + | t.Fatalf("game %+v", game) | |
| 61 | + | } | |
| 62 | + | if behind.Hidden || !behind.Peek || behind.Focused { | |
| 63 | + | t.Fatalf("behind %+v", behind) | |
| 64 | + | } | |
| 65 | + | }) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // Return or a click on the fullscreen window keeps it fullscreen. | |
| 70 | + | func TestOverviewCoverConfirmKeeps(t *testing.T) { | |
| 71 | + | for _, tc := range pinnedCases { | |
| 72 | + | t.Run(tc.name, func(t *testing.T) { | |
| 73 | + | for _, pick := range []bool{false, true} { | |
| 74 | + | m := coverMonitor(tc.overflow, tc.float) | |
| 75 | + | m.ToggleOverview() | |
| 76 | + | if pick { | |
| 77 | + | m.OverviewPick(2) | |
| 78 | + | } else { | |
| 79 | + | m.ToggleOverview() | |
| 80 | + | } | |
| 81 | + | if f, _ := m.Focused(); f != 2 || m.ov.open || m.Current().fullscreen != 2 || !previewOf(t, m.Layout(), 2).Fullscreen { | |
| 82 | + | t.Fatalf("pick %v: focused %d, fullscreen %d", pick, f, m.Current().fullscreen) | |
| 83 | + | } | |
| 84 | + | } | |
| 85 | + | }) | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // Choosing the window behind leaves fullscreen and focuses it; the left | |
| 90 | + | // window shows again, a float behind the tiles. | |
| 91 | + | func TestOverviewCoverPickOther(t *testing.T) { | |
| 92 | + | for _, tc := range pinnedCases { | |
| 93 | + | t.Run(tc.name, func(t *testing.T) { | |
| 94 | + | for _, pick := range []bool{false, true} { | |
| 95 | + | m := coverMonitor(tc.overflow, tc.float) | |
| 96 | + | m.ToggleOverview() | |
| 97 | + | if pick { | |
| 98 | + | m.OverviewPick(1) | |
| 99 | + | } else { | |
| 100 | + | m.OverviewMove(-1, 0) | |
| 101 | + | m.ToggleOverview() | |
| 102 | + | } | |
| 103 | + | w := m.Current() | |
| 104 | + | if f, _ := m.Focused(); f != 1 || m.ov.open || w.fullscreen != 0 { | |
| 105 | + | t.Fatalf("pick %v: focused %d, fullscreen %d", pick, f, w.fullscreen) | |
| 106 | + | } | |
| 107 | + | if p := placement(w, 2); p.Fullscreen || (p.Hidden && !tc.float) { | |
| 108 | + | t.Fatalf("pick %v: left window %+v", pick, p) | |
| 109 | + | } | |
| 110 | + | if i := w.floatIndex(2); tc.float && (i < 0 || !w.Floats[i].below) { | |
| 111 | + | t.Fatalf("pick %v: float %+v", pick, w.Floats) | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | }) | |
| 115 | + | } | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // Escape returns to the fullscreen window, still fullscreen. | |
| 119 | + | func TestOverviewCoverCancel(t *testing.T) { | |
| 120 | + | for _, tc := range pinnedCases { | |
| 121 | + | t.Run(tc.name, func(t *testing.T) { | |
| 122 | + | m := coverMonitor(tc.overflow, tc.float) | |
| 123 | + | m.ToggleOverview() | |
| 124 | + | m.OverviewMove(-1, 0) | |
| 125 | + | m.CancelOverview() | |
| 126 | + | if f, _ := m.Focused(); f != 2 || m.Current().fullscreen != 2 { | |
| 127 | + | t.Fatalf("focused %d, fullscreen %d", f, m.Current().fullscreen) | |
| 128 | + | } | |
| 129 | + | }) | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // A stash card is reachable under a fullscreen tile; choosing it leaves | |
| 134 | + | // fullscreen. | |
| 135 | + | func TestOverviewCoverPickStashCard(t *testing.T) { | |
| 136 | + | m := newMonitor("", "") | |
| 137 | + | m.SetOutput(300, 200) | |
| 138 | + | m.SetOverflow(OverflowFixed) | |
| 139 | + | w := m.Current() | |
| 140 | + | w.AddWindow(1) | |
| 141 | + | w.AddWindow(5) | |
| 142 | + | w.FocusID(5) | |
| 143 | + | w.ToggleWindowStash() | |
| 144 | + | w.FocusID(1) | |
| 145 | + | m.ToggleFullscreen() | |
| 146 | + | if !w.pinned() { | |
| 147 | + | t.Fatal("not pinned") | |
| 148 | + | } | |
| 149 | + | m.ToggleOverview() | |
| 150 | + | uniquePlacements(t, m.Layout()) | |
| 151 | + | m.OverviewMove(-1, 0) | |
| 152 | + | if m.card() != 5 { | |
| 153 | + | t.Fatalf("card %d", m.card()) | |
| 154 | + | } | |
| 155 | + | m.ToggleOverview() | |
| 156 | + | if f, _ := m.Focused(); f != 5 || w.fullscreen != 0 { | |
| 157 | + | t.Fatalf("focused %d, fullscreen %d", f, w.fullscreen) | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // Window binds act on the selected card: a window behind the fullscreen | |
| 162 | + | // one moves or closes, not the fullscreen window. | |
| 163 | + | func TestOverviewCoverActsOnSelection(t *testing.T) { | |
| 164 | + | for _, tc := range pinnedCases { | |
| 165 | + | t.Run(tc.name, func(t *testing.T) { | |
| 166 | + | m := coverMonitor(tc.overflow, tc.float) | |
| 167 | + | m.ToggleOverview() | |
| 168 | + | if e := m.Apply(ActionCloseWindow); e.Close != 2 { | |
| 169 | + | t.Fatalf("front close %d", e.Close) | |
| 170 | + | } | |
| 171 | + | m.OverviewMove(-1, 0) | |
| 172 | + | if e := m.Apply(ActionCloseWindow); e.Close != 1 { | |
| 173 | + | t.Fatalf("behind close %d", e.Close) | |
| 174 | + | } | |
| 175 | + | w := m.Current() | |
| 176 | + | m.Apply(ActionMoveWindowToWorkspaceDown) | |
| 177 | + | if got, _ := m.find(1); got == w || !w.has(2) { | |
| 178 | + | t.Fatalf("moved the wrong window: %v", windows(m)) | |
| 179 | + | } | |
| 180 | + | }) | |
| 181 | + | } | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | // A fullscreen tile whose column is also maximized hands the maximized | |
| 185 | + | // state to the column picked behind it, as a maximized row does. | |
| 186 | + | func TestOverviewCoverMaximizedPick(t *testing.T) { | |
| 187 | + | m := newMonitor("", "") | |
| 188 | + | m.SetOutput(300, 200) | |
| 189 | + | m.SetOverflow(OverflowFixed) | |
| 190 | + | m.AddWindow(1) | |
| 191 | + | m.AddWindow(2) | |
| 192 | + | w := m.Current() | |
| 193 | + | w.ToggleFullWidth() | |
| 194 | + | m.ToggleFullscreen() | |
| 195 | + | m.ToggleOverview() | |
| 196 | + | m.OverviewPick(1) | |
| 197 | + | if f, _ := m.Focused(); f != 1 || w.fullscreen != 0 || !w.Columns[0].FullWidth || w.Columns[1].FullWidth { | |
| 198 | + | t.Fatalf("focused %d, fullscreen %d, columns %+v", f, w.fullscreen, w.Columns) | |
| 199 | + | } | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | // A fixed fullscreen tile among several columns is the front card, the | |
| 203 | + | // other columns behind it, as for a maximized column. | |
| 204 | + | func TestOverviewCoverTileAmongColumns(t *testing.T) { | |
| 205 | + | m := newMonitor("", "") | |
| 206 | + | m.SetOutput(300, 200) | |
| 207 | + | m.SetOverflow(OverflowFixed) | |
| 208 | + | m.AddWindow(1) | |
| 209 | + | m.AddWindow(2) | |
| 210 | + | m.AddWindow(3) | |
| 211 | + | m.Current().FocusID(2) | |
| 212 | + | m.ToggleFullscreen() | |
| 213 | + | m.ToggleOverview() | |
| 214 | + | ps := m.Layout() | |
| 215 | + | uniquePlacements(t, ps) | |
| 216 | + | if g := previewOf(t, ps, 2); g.Hidden || g.Peek || !g.Fullscreen { | |
| 217 | + | t.Fatalf("game %+v", g) | |
| 218 | + | } | |
| 219 | + | for _, id := range []WindowID{1, 3} { | |
| 220 | + | if p := previewOf(t, ps, id); p.Hidden || !p.Peek { | |
| 221 | + | t.Fatalf("%d: %+v", id, p) | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | m.OverviewPick(3) | |
| 225 | + | if f, _ := m.Focused(); f != 3 || m.Current().fullscreen != 0 { | |
| 226 | + | t.Fatalf("focused %d, fullscreen %d", f, m.Current().fullscreen) | |
| 227 | + | } | |
| 228 | + | } | |
| @@ -0,0 +1,228 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | // coverMonitor has, on a 300x200 output, tiles 1 and 2 on one workspace, | |||
| 6 | + | // 2 fullscreen. With float, 2 is a floating game over tile 1 instead. | |||
| 7 | + | func coverMonitor(o Overflow, float bool) *Monitor { | |||
| 8 | + | m := newMonitor("", "") | |||
| 9 | + | m.SetOutput(300, 200) | |||
| 10 | + | m.SetOverflow(o) | |||
| 11 | + | m.SetMaxColumns(2) | |||
| 12 | + | m.AddWindow(1) | |||
| 13 | + | if float { | |||
| 14 | + | m.AddFloating(2, 300, 200) | |||
| 15 | + | m.SetFullscreen(2, true) | |||
| 16 | + | } else { | |||
| 17 | + | m.AddWindow(2) | |||
| 18 | + | m.ToggleFullscreen() | |||
| 19 | + | } | |||
| 20 | + | return m | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // pinnedCases are the setups where the fullscreen window pins its row. | |||
| 24 | + | var pinnedCases = []struct { | |||
| 25 | + | name string | |||
| 26 | + | overflow Overflow | |||
| 27 | + | float bool | |||
| 28 | + | }{ | |||
| 29 | + | {"fixed tile", OverflowFixed, false}, | |||
| 30 | + | {"fixed float", OverflowFixed, true}, | |||
| 31 | + | {"scroll float", OverflowScroll, true}, | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // uniquePlacements fails when a window is placed twice. | |||
| 35 | + | func uniquePlacements(t *testing.T, ps []Placement) { | |||
| 36 | + | t.Helper() | |||
| 37 | + | seen := map[WindowID]bool{} | |||
| 38 | + | for _, p := range ps { | |||
| 39 | + | if seen[p.ID] { | |||
| 40 | + | t.Fatalf("window %d placed twice: %+v", p.ID, ps) | |||
| 41 | + | } | |||
| 42 | + | seen[p.ID] = true | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // The overview over a pinned fullscreen window shows it in front, still | |||
| 47 | + | // fullscreen and selected, and the window it hides behind it. | |||
| 48 | + | func TestOverviewCoverShowsWorkspace(t *testing.T) { | |||
| 49 | + | for _, tc := range pinnedCases { | |||
| 50 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 51 | + | m := coverMonitor(tc.overflow, tc.float) | |||
| 52 | + | if !m.Current().pinned() { | |||
| 53 | + | t.Fatal("not pinned") | |||
| 54 | + | } | |||
| 55 | + | m.ToggleOverview() | |||
| 56 | + | ps := m.Layout() | |||
| 57 | + | uniquePlacements(t, ps) | |||
| 58 | + | game, behind := previewOf(t, ps, 2), previewOf(t, ps, 1) | |||
| 59 | + | if game.Hidden || !game.Focused || game.Peek || !game.Fullscreen { | |||
| 60 | + | t.Fatalf("game %+v", game) | |||
| 61 | + | } | |||
| 62 | + | if behind.Hidden || !behind.Peek || behind.Focused { | |||
| 63 | + | t.Fatalf("behind %+v", behind) | |||
| 64 | + | } | |||
| 65 | + | }) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // Return or a click on the fullscreen window keeps it fullscreen. | |||
| 70 | + | func TestOverviewCoverConfirmKeeps(t *testing.T) { | |||
| 71 | + | for _, tc := range pinnedCases { | |||
| 72 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 73 | + | for _, pick := range []bool{false, true} { | |||
| 74 | + | m := coverMonitor(tc.overflow, tc.float) | |||
| 75 | + | m.ToggleOverview() | |||
| 76 | + | if pick { | |||
| 77 | + | m.OverviewPick(2) | |||
| 78 | + | } else { | |||
| 79 | + | m.ToggleOverview() | |||
| 80 | + | } | |||
| 81 | + | if f, _ := m.Focused(); f != 2 || m.ov.open || m.Current().fullscreen != 2 || !previewOf(t, m.Layout(), 2).Fullscreen { | |||
| 82 | + | t.Fatalf("pick %v: focused %d, fullscreen %d", pick, f, m.Current().fullscreen) | |||
| 83 | + | } | |||
| 84 | + | } | |||
| 85 | + | }) | |||
| 86 | + | } | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // Choosing the window behind leaves fullscreen and focuses it; the left | |||
| 90 | + | // window shows again, a float behind the tiles. | |||
| 91 | + | func TestOverviewCoverPickOther(t *testing.T) { | |||
| 92 | + | for _, tc := range pinnedCases { | |||
| 93 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 94 | + | for _, pick := range []bool{false, true} { | |||
| 95 | + | m := coverMonitor(tc.overflow, tc.float) | |||
| 96 | + | m.ToggleOverview() | |||
| 97 | + | if pick { | |||
| 98 | + | m.OverviewPick(1) | |||
| 99 | + | } else { | |||
| 100 | + | m.OverviewMove(-1, 0) | |||
| 101 | + | m.ToggleOverview() | |||
| 102 | + | } | |||
| 103 | + | w := m.Current() | |||
| 104 | + | if f, _ := m.Focused(); f != 1 || m.ov.open || w.fullscreen != 0 { | |||
| 105 | + | t.Fatalf("pick %v: focused %d, fullscreen %d", pick, f, w.fullscreen) | |||
| 106 | + | } | |||
| 107 | + | if p := placement(w, 2); p.Fullscreen || (p.Hidden && !tc.float) { | |||
| 108 | + | t.Fatalf("pick %v: left window %+v", pick, p) | |||
| 109 | + | } | |||
| 110 | + | if i := w.floatIndex(2); tc.float && (i < 0 || !w.Floats[i].below) { | |||
| 111 | + | t.Fatalf("pick %v: float %+v", pick, w.Floats) | |||
| 112 | + | } | |||
| 113 | + | } | |||
| 114 | + | }) | |||
| 115 | + | } | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // Escape returns to the fullscreen window, still fullscreen. | |||
| 119 | + | func TestOverviewCoverCancel(t *testing.T) { | |||
| 120 | + | for _, tc := range pinnedCases { | |||
| 121 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 122 | + | m := coverMonitor(tc.overflow, tc.float) | |||
| 123 | + | m.ToggleOverview() | |||
| 124 | + | m.OverviewMove(-1, 0) | |||
| 125 | + | m.CancelOverview() | |||
| 126 | + | if f, _ := m.Focused(); f != 2 || m.Current().fullscreen != 2 { | |||
| 127 | + | t.Fatalf("focused %d, fullscreen %d", f, m.Current().fullscreen) | |||
| 128 | + | } | |||
| 129 | + | }) | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | // A stash card is reachable under a fullscreen tile; choosing it leaves | |||
| 134 | + | // fullscreen. | |||
| 135 | + | func TestOverviewCoverPickStashCard(t *testing.T) { | |||
| 136 | + | m := newMonitor("", "") | |||
| 137 | + | m.SetOutput(300, 200) | |||
| 138 | + | m.SetOverflow(OverflowFixed) | |||
| 139 | + | w := m.Current() | |||
| 140 | + | w.AddWindow(1) | |||
| 141 | + | w.AddWindow(5) | |||
| 142 | + | w.FocusID(5) | |||
| 143 | + | w.ToggleWindowStash() | |||
| 144 | + | w.FocusID(1) | |||
| 145 | + | m.ToggleFullscreen() | |||
| 146 | + | if !w.pinned() { | |||
| 147 | + | t.Fatal("not pinned") | |||
| 148 | + | } | |||
| 149 | + | m.ToggleOverview() | |||
| 150 | + | uniquePlacements(t, m.Layout()) | |||
| 151 | + | m.OverviewMove(-1, 0) | |||
| 152 | + | if m.card() != 5 { | |||
| 153 | + | t.Fatalf("card %d", m.card()) | |||
| 154 | + | } | |||
| 155 | + | m.ToggleOverview() | |||
| 156 | + | if f, _ := m.Focused(); f != 5 || w.fullscreen != 0 { | |||
| 157 | + | t.Fatalf("focused %d, fullscreen %d", f, w.fullscreen) | |||
| 158 | + | } | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | // Window binds act on the selected card: a window behind the fullscreen | |||
| 162 | + | // one moves or closes, not the fullscreen window. | |||
| 163 | + | func TestOverviewCoverActsOnSelection(t *testing.T) { | |||
| 164 | + | for _, tc := range pinnedCases { | |||
| 165 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 166 | + | m := coverMonitor(tc.overflow, tc.float) | |||
| 167 | + | m.ToggleOverview() | |||
| 168 | + | if e := m.Apply(ActionCloseWindow); e.Close != 2 { | |||
| 169 | + | t.Fatalf("front close %d", e.Close) | |||
| 170 | + | } | |||
| 171 | + | m.OverviewMove(-1, 0) | |||
| 172 | + | if e := m.Apply(ActionCloseWindow); e.Close != 1 { | |||
| 173 | + | t.Fatalf("behind close %d", e.Close) | |||
| 174 | + | } | |||
| 175 | + | w := m.Current() | |||
| 176 | + | m.Apply(ActionMoveWindowToWorkspaceDown) | |||
| 177 | + | if got, _ := m.find(1); got == w || !w.has(2) { | |||
| 178 | + | t.Fatalf("moved the wrong window: %v", windows(m)) | |||
| 179 | + | } | |||
| 180 | + | }) | |||
| 181 | + | } | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | // A fullscreen tile whose column is also maximized hands the maximized | |||
| 185 | + | // state to the column picked behind it, as a maximized row does. | |||
| 186 | + | func TestOverviewCoverMaximizedPick(t *testing.T) { | |||
| 187 | + | m := newMonitor("", "") | |||
| 188 | + | m.SetOutput(300, 200) | |||
| 189 | + | m.SetOverflow(OverflowFixed) | |||
| 190 | + | m.AddWindow(1) | |||
| 191 | + | m.AddWindow(2) | |||
| 192 | + | w := m.Current() | |||
| 193 | + | w.ToggleFullWidth() | |||
| 194 | + | m.ToggleFullscreen() | |||
| 195 | + | m.ToggleOverview() | |||
| 196 | + | m.OverviewPick(1) | |||
| 197 | + | if f, _ := m.Focused(); f != 1 || w.fullscreen != 0 || !w.Columns[0].FullWidth || w.Columns[1].FullWidth { | |||
| 198 | + | t.Fatalf("focused %d, fullscreen %d, columns %+v", f, w.fullscreen, w.Columns) | |||
| 199 | + | } | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | // A fixed fullscreen tile among several columns is the front card, the | |||
| 203 | + | // other columns behind it, as for a maximized column. | |||
| 204 | + | func TestOverviewCoverTileAmongColumns(t *testing.T) { | |||
| 205 | + | m := newMonitor("", "") | |||
| 206 | + | m.SetOutput(300, 200) | |||
| 207 | + | m.SetOverflow(OverflowFixed) | |||
| 208 | + | m.AddWindow(1) | |||
| 209 | + | m.AddWindow(2) | |||
| 210 | + | m.AddWindow(3) | |||
| 211 | + | m.Current().FocusID(2) | |||
| 212 | + | m.ToggleFullscreen() | |||
| 213 | + | m.ToggleOverview() | |||
| 214 | + | ps := m.Layout() | |||
| 215 | + | uniquePlacements(t, ps) | |||
| 216 | + | if g := previewOf(t, ps, 2); g.Hidden || g.Peek || !g.Fullscreen { | |||
| 217 | + | t.Fatalf("game %+v", g) | |||
| 218 | + | } | |||
| 219 | + | for _, id := range []WindowID{1, 3} { | |||
| 220 | + | if p := previewOf(t, ps, id); p.Hidden || !p.Peek { | |||
| 221 | + | t.Fatalf("%d: %+v", id, p) | |||
| 222 | + | } | |||
| 223 | + | } | |||
| 224 | + | m.OverviewPick(3) | |||
| 225 | + | if f, _ := m.Focused(); f != 3 || m.Current().fullscreen != 0 { | |||
| 226 | + | t.Fatalf("focused %d, fullscreen %d", f, m.Current().fullscreen) | |||
| 227 | + | } | |||
| 228 | + | } | |||
M internal/core/overview_stack.go +35 -6
| @@ -18,8 +18,17 @@ type stackItem struct { | |||
| 18 | 18 | id WindowID | |
| 19 | 19 | } | |
| 20 | 20 | ||
| 21 | + | // overviewMaximized reports a fixed row whose focused column hides the | |
| 22 | + | // others: maximized, or holding the covering fullscreen tile. Its card | |
| 23 | + | // shows in front of the hidden columns' card. | |
| 21 | 24 | func (w *Workspace) overviewMaximized() bool { | |
| 22 | - | return w.Overflow == OverflowFixed && len(w.Columns) > 1 && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && !w.pinned() | |
| 25 | + | if w.Overflow != OverflowFixed || len(w.Columns) < 2 || w.Focus < 0 || w.Focus >= len(w.Columns) { | |
| 26 | + | return false | |
| 27 | + | } | |
| 28 | + | if c := w.cover(); c != 0 && !w.isFloat(c) { | |
| 29 | + | return w.columnOf(c) == w.Focus | |
| 30 | + | } | |
| 31 | + | return w.Columns[w.Focus].FullWidth | |
| 23 | 32 | } | |
| 24 | 33 | ||
| 25 | 34 | func (w *Workspace) columnOf(id WindowID) int { | |
| @@ -32,21 +41,36 @@ func (w *Workspace) columnOf(id WindowID) int { | |||
| 32 | 41 | } | |
| 33 | 42 | ||
| 34 | 43 | // stack lists what is on screen followed by what it hides, top to bottom. | |
| 44 | + | // A pinned fullscreen window is the front card, a float on its own, a | |
| 45 | + | // fixed tile with its column, so every window it hides stays reachable; a | |
| 46 | + | // stashed one is its card in the pile. | |
| 35 | 47 | func (w *Workspace) stack() []stackItem { | |
| 48 | + | var items []stackItem | |
| 49 | + | c, columns := WindowID(0), len(w.Columns) > 0 | |
| 36 | 50 | if w.pinned() { | |
| 37 | - | return nil | |
| 51 | + | c = w.cover() | |
| 52 | + | } | |
| 53 | + | switch { | |
| 54 | + | case c == 0, w.stashIndex(c) >= 0: | |
| 55 | + | case w.floatIndex(c) >= 0: | |
| 56 | + | items = append(items, stackItem{stackFloat, c}) | |
| 57 | + | case w.overviewMaximized(): | |
| 58 | + | items = append(items, stackItem{stackColumn, c}) | |
| 59 | + | default: | |
| 60 | + | // One card of all columns, its own marked fullscreen. | |
| 61 | + | items = append(items, stackItem{kind: stackColumns}) | |
| 62 | + | columns = false | |
| 38 | 63 | } | |
| 39 | - | var items []stackItem | |
| 40 | 64 | for i := len(w.Floats) - 1; i >= 0; i-- { | |
| 41 | 65 | f := w.Floats[i] | |
| 42 | 66 | if !f.below && w.coversFloat(f) { | |
| 43 | 67 | items = append(items, stackItem{stackFloat, f.ID}) | |
| 44 | 68 | } | |
| 45 | 69 | } | |
| 46 | - | if w.overviewMaximized() { | |
| 70 | + | if (c == 0 || w.isFloat(c)) && w.overviewMaximized() { | |
| 47 | 71 | items = append(items, stackItem{stackColumn, w.Columns[w.Focus].Windows[0]}) | |
| 48 | 72 | } | |
| 49 | - | if len(w.Columns) > 0 { | |
| 73 | + | if columns { | |
| 50 | 74 | items = append(items, stackItem{kind: stackColumns}) | |
| 51 | 75 | } | |
| 52 | 76 | for i := len(w.Floats) - 1; i >= 0; i-- { | |
| @@ -242,6 +266,11 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool | |||
| 242 | 266 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel) | |
| 243 | 267 | } | |
| 244 | 268 | g := w.gap() | |
| 269 | + | if item.kind == stackFloat && item.id == w.cover() { | |
| 270 | + | // A fullscreen float shows at the size of a fullscreen column. | |
| 271 | + | r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)} | |
| 272 | + | return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r) | |
| 273 | + | } | |
| 245 | 274 | if item.kind == stackFloat { | |
| 246 | 275 | f := w.Floats[w.floatIndex(item.id)] | |
| 247 | 276 | r := w.floatRect(f) | |
| @@ -259,7 +288,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool | |||
| 259 | 288 | r.Y = (w.Usable.H - r.H) / 2 | |
| 260 | 289 | tiles := make([]Placement, 0, len(c.Windows)) | |
| 261 | 290 | for j, t := range rowRects(r, c, g) { | |
| 262 | - | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus}) | |
| 291 | + | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()}) | |
| 263 | 292 | } | |
| 264 | 293 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r) | |
| 265 | 294 | } | |
| @@ -18,8 +18,17 @@ type stackItem struct { | |||||
| 18 | id WindowID | 18 | id WindowID | ||
| 19 | } | 19 | } | ||
| 20 | 20 | ||||
| 21 | + | // overviewMaximized reports a fixed row whose focused column hides the | |||
| 22 | + | // others: maximized, or holding the covering fullscreen tile. Its card | |||
| 23 | + | // shows in front of the hidden columns' card. | |||
| 21 | func (w *Workspace) overviewMaximized() bool { | 24 | func (w *Workspace) overviewMaximized() bool { | ||
| 22 | - | return w.Overflow == OverflowFixed && len(w.Columns) > 1 && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && !w.pinned() | 25 | + | if w.Overflow != OverflowFixed || len(w.Columns) < 2 || w.Focus < 0 || w.Focus >= len(w.Columns) { |
| 26 | + | return false | |||
| 27 | + | } | |||
| 28 | + | if c := w.cover(); c != 0 && !w.isFloat(c) { | |||
| 29 | + | return w.columnOf(c) == w.Focus | |||
| 30 | + | } | |||
| 31 | + | return w.Columns[w.Focus].FullWidth | |||
| 23 | } | 32 | } | ||
| 24 | 33 | ||||
| 25 | func (w *Workspace) columnOf(id WindowID) int { | 34 | func (w *Workspace) columnOf(id WindowID) int { | ||
| @@ -32,21 +41,36 @@ func (w *Workspace) columnOf(id WindowID) int { | |||||
| 32 | } | 41 | } | ||
| 33 | 42 | ||||
| 34 | // stack lists what is on screen followed by what it hides, top to bottom. | 43 | // stack lists what is on screen followed by what it hides, top to bottom. | ||
| 44 | + | // A pinned fullscreen window is the front card, a float on its own, a | |||
| 45 | + | // fixed tile with its column, so every window it hides stays reachable; a | |||
| 46 | + | // stashed one is its card in the pile. | |||
| 35 | func (w *Workspace) stack() []stackItem { | 47 | func (w *Workspace) stack() []stackItem { | ||
| 48 | + | var items []stackItem | |||
| 49 | + | c, columns := WindowID(0), len(w.Columns) > 0 | |||
| 36 | if w.pinned() { | 50 | if w.pinned() { | ||
| 37 | - | return nil | 51 | + | c = w.cover() |
| 52 | + | } | |||
| 53 | + | switch { | |||
| 54 | + | case c == 0, w.stashIndex(c) >= 0: | |||
| 55 | + | case w.floatIndex(c) >= 0: | |||
| 56 | + | items = append(items, stackItem{stackFloat, c}) | |||
| 57 | + | case w.overviewMaximized(): | |||
| 58 | + | items = append(items, stackItem{stackColumn, c}) | |||
| 59 | + | default: | |||
| 60 | + | // One card of all columns, its own marked fullscreen. | |||
| 61 | + | items = append(items, stackItem{kind: stackColumns}) | |||
| 62 | + | columns = false | |||
| 38 | } | 63 | } | ||
| 39 | - | var items []stackItem | |||
| 40 | for i := len(w.Floats) - 1; i >= 0; i-- { | 64 | for i := len(w.Floats) - 1; i >= 0; i-- { | ||
| 41 | f := w.Floats[i] | 65 | f := w.Floats[i] | ||
| 42 | if !f.below && w.coversFloat(f) { | 66 | if !f.below && w.coversFloat(f) { | ||
| 43 | items = append(items, stackItem{stackFloat, f.ID}) | 67 | items = append(items, stackItem{stackFloat, f.ID}) | ||
| 44 | } | 68 | } | ||
| 45 | } | 69 | } | ||
| 46 | - | if w.overviewMaximized() { | 70 | + | if (c == 0 || w.isFloat(c)) && w.overviewMaximized() { |
| 47 | items = append(items, stackItem{stackColumn, w.Columns[w.Focus].Windows[0]}) | 71 | items = append(items, stackItem{stackColumn, w.Columns[w.Focus].Windows[0]}) | ||
| 48 | } | 72 | } | ||
| 49 | - | if len(w.Columns) > 0 { | 73 | + | if columns { |
| 50 | items = append(items, stackItem{kind: stackColumns}) | 74 | items = append(items, stackItem{kind: stackColumns}) | ||
| 51 | } | 75 | } | ||
| 52 | for i := len(w.Floats) - 1; i >= 0; i-- { | 76 | for i := len(w.Floats) - 1; i >= 0; i-- { | ||
| @@ -242,6 +266,11 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool | |||||
| 242 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel) | 266 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel) | ||
| 243 | } | 267 | } | ||
| 244 | g := w.gap() | 268 | g := w.gap() | ||
| 269 | + | if item.kind == stackFloat && item.id == w.cover() { | |||
| 270 | + | // A fullscreen float shows at the size of a fullscreen column. | |||
| 271 | + | r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)} | |||
| 272 | + | return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r) | |||
| 273 | + | } | |||
| 245 | if item.kind == stackFloat { | 274 | if item.kind == stackFloat { | ||
| 246 | f := w.Floats[w.floatIndex(item.id)] | 275 | f := w.Floats[w.floatIndex(item.id)] | ||
| 247 | r := w.floatRect(f) | 276 | r := w.floatRect(f) | ||
| @@ -259,7 +288,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool | |||||
| 259 | r.Y = (w.Usable.H - r.H) / 2 | 288 | r.Y = (w.Usable.H - r.H) / 2 | ||
| 260 | tiles := make([]Placement, 0, len(c.Windows)) | 289 | tiles := make([]Placement, 0, len(c.Windows)) | ||
| 261 | for j, t := range rowRects(r, c, g) { | 290 | for j, t := range rowRects(r, c, g) { | ||
| 262 | - | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus}) | 291 | + | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()}) |
| 263 | } | 292 | } | ||
| 264 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r) | 293 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r) | ||
| 265 | } | 294 | } | ||
M internal/core/overview_test.go +32 -20
| @@ -208,26 +208,35 @@ func TestOverviewOpensOverFullscreenFloat(t *testing.T) { | |||
| 208 | 208 | } | |
| 209 | 209 | } | |
| 210 | 210 | ||
| 211 | - | // A tiled fullscreen window (fixed overflow) is its row's only preview | |
| 212 | - | // too: the selection cannot leave it for a hidden column, and Return keeps | |
| 213 | - | // it fullscreen. | |
| 211 | + | // A tiled fullscreen window (fixed overflow) is its row's front card: | |
| 212 | + | // Return keeps it fullscreen; the column it hides is behind, and picking | |
| 213 | + | // it leaves fullscreen. | |
| 214 | 214 | func TestOverviewOpensOverFullscreenTile(t *testing.T) { | |
| 215 | - | m := newMonitor("", "") | |
| 216 | - | m.SetOutput(300, 200) | |
| 217 | - | m.SetOverflow(OverflowFixed) | |
| 218 | - | m.AddWindow(1) | |
| 219 | - | m.AddWindow(2) | |
| 220 | - | m.ToggleFullscreen() | |
| 221 | - | m.ToggleOverview() | |
| 215 | + | open := func() *Monitor { | |
| 216 | + | m := newMonitor("", "") | |
| 217 | + | m.SetOutput(300, 200) | |
| 218 | + | m.SetOverflow(OverflowFixed) | |
| 219 | + | m.AddWindow(1) | |
| 220 | + | m.AddWindow(2) | |
| 221 | + | m.ToggleFullscreen() | |
| 222 | + | m.ToggleOverview() | |
| 223 | + | return m | |
| 224 | + | } | |
| 225 | + | m := open() | |
| 222 | 226 | game := previewOf(t, m.Layout(), 2) | |
| 223 | 227 | if !m.ov.open || !game.Focused || game.Peek || !game.Fullscreen { | |
| 224 | 228 | t.Fatalf("game %+v", game) | |
| 225 | 229 | } | |
| 226 | - | m.OverviewMove(-1, 0) | |
| 227 | 230 | m.ToggleOverview() | |
| 228 | 231 | if f, _ := m.Focused(); f != 2 || m.Active != 0 || !previewOf(t, m.Layout(), 2).Fullscreen { | |
| 229 | 232 | t.Fatalf("confirm: %d on %d", f, m.Active) | |
| 230 | 233 | } | |
| 234 | + | m = open() | |
| 235 | + | m.OverviewMove(-1, 0) | |
| 236 | + | m.ToggleOverview() | |
| 237 | + | if f, _ := m.Focused(); f != 1 || m.Current().fullscreen != 0 { | |
| 238 | + | t.Fatalf("behind: focused %d, fullscreen %d", f, m.Current().fullscreen) | |
| 239 | + | } | |
| 231 | 240 | } | |
| 232 | 241 | ||
| 233 | 242 | // Under scroll overflow a fullscreen column does not pin the row: another | |
| @@ -250,9 +259,9 @@ func TestOverviewOverScrollFullscreenPicksColumn(t *testing.T) { | |||
| 250 | 259 | } | |
| 251 | 260 | } | |
| 252 | 261 | ||
| 253 | - | // Only the game shows on its row: its dialogs and the columns under it | |
| 254 | - | // stay hidden, as on screen. | |
| 255 | - | func TestOverviewFullscreenFloatHidesOthers(t *testing.T) { | |
| 262 | + | // A neighbor row with a fullscreen game shows it in front, the columns it | |
| 263 | + | // hides behind; a dialog under it stays hidden, as for any small float. | |
| 264 | + | func TestOverviewFullscreenFloatRow(t *testing.T) { | |
| 256 | 265 | m := gameMonitor(OverflowScroll) | |
| 257 | 266 | m.Focus(1) | |
| 258 | 267 | m.AddWindow(5) | |
| @@ -262,14 +271,17 @@ func TestOverviewFullscreenFloatHidesOthers(t *testing.T) { | |||
| 262 | 271 | m.Focus(0) | |
| 263 | 272 | m.ToggleOverview() | |
| 264 | 273 | ps := m.Layout() | |
| 265 | - | if previewOf(t, ps, 2).Hidden { | |
| 266 | - | t.Fatal("game hidden") | |
| 267 | - | } | |
| 268 | - | for _, id := range []WindowID{3, 4, 5} { | |
| 269 | - | if p := previewOf(t, ps, id); !p.Hidden { | |
| 270 | - | t.Fatalf("%d shown: %+v", id, p) | |
| 274 | + | for _, id := range []WindowID{2, 3, 5} { | |
| 275 | + | if p := previewOf(t, ps, id); p.Hidden || !p.Peek { | |
| 276 | + | t.Fatalf("%d: %+v", id, p) | |
| 271 | 277 | } | |
| 272 | 278 | } | |
| 279 | + | if p := previewOf(t, ps, 4); !p.Hidden { | |
| 280 | + | t.Fatalf("dialog shown: %+v", p) | |
| 281 | + | } | |
| 282 | + | if g := previewOf(t, ps, 2); !g.Fullscreen { | |
| 283 | + | t.Fatalf("game %+v", g) | |
| 284 | + | } | |
| 273 | 285 | } | |
| 274 | 286 | ||
| 275 | 287 | // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1 | |
| @@ -208,26 +208,35 @@ func TestOverviewOpensOverFullscreenFloat(t *testing.T) { | |||||
| 208 | } | 208 | } | ||
| 209 | } | 209 | } | ||
| 210 | 210 | ||||
| 211 | - | // A tiled fullscreen window (fixed overflow) is its row's only preview | 211 | + | // A tiled fullscreen window (fixed overflow) is its row's front card: |
| 212 | - | // too: the selection cannot leave it for a hidden column, and Return keeps | 212 | + | // Return keeps it fullscreen; the column it hides is behind, and picking |
| 213 | - | // it fullscreen. | 213 | + | // it leaves fullscreen. |
| 214 | func TestOverviewOpensOverFullscreenTile(t *testing.T) { | 214 | func TestOverviewOpensOverFullscreenTile(t *testing.T) { | ||
| 215 | - | m := newMonitor("", "") | 215 | + | open := func() *Monitor { |
| 216 | - | m.SetOutput(300, 200) | 216 | + | m := newMonitor("", "") |
| 217 | - | m.SetOverflow(OverflowFixed) | 217 | + | m.SetOutput(300, 200) |
| 218 | - | m.AddWindow(1) | 218 | + | m.SetOverflow(OverflowFixed) |
| 219 | - | m.AddWindow(2) | 219 | + | m.AddWindow(1) |
| 220 | - | m.ToggleFullscreen() | 220 | + | m.AddWindow(2) |
| 221 | - | m.ToggleOverview() | 221 | + | m.ToggleFullscreen() |
| 222 | + | m.ToggleOverview() | |||
| 223 | + | return m | |||
| 224 | + | } | |||
| 225 | + | m := open() | |||
| 222 | game := previewOf(t, m.Layout(), 2) | 226 | game := previewOf(t, m.Layout(), 2) | ||
| 223 | if !m.ov.open || !game.Focused || game.Peek || !game.Fullscreen { | 227 | if !m.ov.open || !game.Focused || game.Peek || !game.Fullscreen { | ||
| 224 | t.Fatalf("game %+v", game) | 228 | t.Fatalf("game %+v", game) | ||
| 225 | } | 229 | } | ||
| 226 | - | m.OverviewMove(-1, 0) | |||
| 227 | m.ToggleOverview() | 230 | m.ToggleOverview() | ||
| 228 | if f, _ := m.Focused(); f != 2 || m.Active != 0 || !previewOf(t, m.Layout(), 2).Fullscreen { | 231 | if f, _ := m.Focused(); f != 2 || m.Active != 0 || !previewOf(t, m.Layout(), 2).Fullscreen { | ||
| 229 | t.Fatalf("confirm: %d on %d", f, m.Active) | 232 | t.Fatalf("confirm: %d on %d", f, m.Active) | ||
| 230 | } | 233 | } | ||
| 234 | + | m = open() | |||
| 235 | + | m.OverviewMove(-1, 0) | |||
| 236 | + | m.ToggleOverview() | |||
| 237 | + | if f, _ := m.Focused(); f != 1 || m.Current().fullscreen != 0 { | |||
| 238 | + | t.Fatalf("behind: focused %d, fullscreen %d", f, m.Current().fullscreen) | |||
| 239 | + | } | |||
| 231 | } | 240 | } | ||
| 232 | 241 | ||||
| 233 | // Under scroll overflow a fullscreen column does not pin the row: another | 242 | // Under scroll overflow a fullscreen column does not pin the row: another | ||
| @@ -250,9 +259,9 @@ func TestOverviewOverScrollFullscreenPicksColumn(t *testing.T) { | |||||
| 250 | } | 259 | } | ||
| 251 | } | 260 | } | ||
| 252 | 261 | ||||
| 253 | - | // Only the game shows on its row: its dialogs and the columns under it | 262 | + | // A neighbor row with a fullscreen game shows it in front, the columns it |
| 254 | - | // stay hidden, as on screen. | 263 | + | // hides behind; a dialog under it stays hidden, as for any small float. |
| 255 | - | func TestOverviewFullscreenFloatHidesOthers(t *testing.T) { | 264 | + | func TestOverviewFullscreenFloatRow(t *testing.T) { |
| 256 | m := gameMonitor(OverflowScroll) | 265 | m := gameMonitor(OverflowScroll) | ||
| 257 | m.Focus(1) | 266 | m.Focus(1) | ||
| 258 | m.AddWindow(5) | 267 | m.AddWindow(5) | ||
| @@ -262,14 +271,17 @@ func TestOverviewFullscreenFloatHidesOthers(t *testing.T) { | |||||
| 262 | m.Focus(0) | 271 | m.Focus(0) | ||
| 263 | m.ToggleOverview() | 272 | m.ToggleOverview() | ||
| 264 | ps := m.Layout() | 273 | ps := m.Layout() | ||
| 265 | - | if previewOf(t, ps, 2).Hidden { | 274 | + | for _, id := range []WindowID{2, 3, 5} { |
| 266 | - | t.Fatal("game hidden") | 275 | + | if p := previewOf(t, ps, id); p.Hidden || !p.Peek { |
| 267 | - | } | 276 | + | t.Fatalf("%d: %+v", id, p) |
| 268 | - | for _, id := range []WindowID{3, 4, 5} { | |||
| 269 | - | if p := previewOf(t, ps, id); !p.Hidden { | |||
| 270 | - | t.Fatalf("%d shown: %+v", id, p) | |||
| 271 | } | 277 | } | ||
| 272 | } | 278 | } | ||
| 279 | + | if p := previewOf(t, ps, 4); !p.Hidden { | |||
| 280 | + | t.Fatalf("dialog shown: %+v", p) | |||
| 281 | + | } | |||
| 282 | + | if g := previewOf(t, ps, 2); !g.Fullscreen { | |||
| 283 | + | t.Fatalf("game %+v", g) | |||
| 284 | + | } | |||
| 273 | } | 285 | } | ||
| 274 | 286 | ||||
| 275 | // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1 | 287 | // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1 | ||