fix(core): show dialog chains over the cover
A dialog of a dialog (an overwrite confirm over a save dialog) was hidden under a fullscreen cover, or made it leave fullscreen when modal. overCover now follows the dialog parent chain.
- commit
- 92f3c09d79b15b10d40ebdfb4f8174169ff834cc
- author
- brice · · 2026-10-13 04:16 UTC
- parent
- ee436b8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +77 -4
M internal/core/floating_test.go +56 -0
| @@ -578,6 +578,62 @@ func TestActivateStashModalKeepsFullscreen(t *testing.T) { | |||
| 578 | 578 | } | |
| 579 | 579 | } | |
| 580 | 580 | ||
| 581 | + | // A dialog of a dialog (an "Overwrite?" over a "Save as…") shows over | |
| 582 | + | // the cover like its parent: modal chains of a stashed window or of the | |
| 583 | + | // cover keep the game fullscreen, and a new one takes the focus. | |
| 584 | + | func TestDialogChainOverCover(t *testing.T) { | |
| 585 | + | t.Run("stashed modal chain", func(t *testing.T) { | |
| 586 | + | w := workspace() | |
| 587 | + | w.AddWindow(14) | |
| 588 | + | w.ToggleWindowStash() | |
| 589 | + | w.ToggleStashVisible() | |
| 590 | + | w.AddDialog(9, 14, 20, 10) | |
| 591 | + | w.SetModal(9, true) | |
| 592 | + | w.AddDialog(8, 9, 10, 5) | |
| 593 | + | w.SetModal(8, true) | |
| 594 | + | w.AddFloating(72, 50, 40) | |
| 595 | + | w.SetFullscreen(72, true) | |
| 596 | + | w.Activate(14) | |
| 597 | + | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 || w.left != 0 { | |
| 598 | + | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |
| 599 | + | } | |
| 600 | + | if p := placementOf(w.Layout(), 8); p.Hidden { | |
| 601 | + | t.Fatalf("confirm hidden: %+v", p) | |
| 602 | + | } | |
| 603 | + | }) | |
| 604 | + | t.Run("stashed dialog maps a dialog", func(t *testing.T) { | |
| 605 | + | w := workspace() | |
| 606 | + | w.AddWindow(14) | |
| 607 | + | w.ToggleWindowStash() | |
| 608 | + | w.ToggleStashVisible() | |
| 609 | + | w.AddFloating(72, 50, 40) | |
| 610 | + | w.SetFullscreen(72, true) | |
| 611 | + | w.Activate(14) | |
| 612 | + | w.AddDialog(9, 14, 20, 10) | |
| 613 | + | w.AddDialog(8, 9, 10, 5) | |
| 614 | + | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 { | |
| 615 | + | t.Fatalf("focused %d fullscreen %d", id, w.fullscreen) | |
| 616 | + | } | |
| 617 | + | if p := placementOf(w.Layout(), 8); p.Hidden { | |
| 618 | + | t.Fatalf("confirm hidden: %+v", p) | |
| 619 | + | } | |
| 620 | + | }) | |
| 621 | + | t.Run("cover modal chain", func(t *testing.T) { | |
| 622 | + | w := workspace() | |
| 623 | + | w.AddFloating(72, 50, 40) | |
| 624 | + | w.AddDialog(9, 72, 20, 10) | |
| 625 | + | w.SetModal(9, true) | |
| 626 | + | w.AddDialog(8, 9, 10, 5) | |
| 627 | + | w.SetModal(8, true) | |
| 628 | + | w.SetFullscreen(72, true) | |
| 629 | + | w.AddWindow(1) | |
| 630 | + | w.Activate(72) | |
| 631 | + | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 || w.left != 0 { | |
| 632 | + | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |
| 633 | + | } | |
| 634 | + | }) | |
| 635 | + | } | |
| 636 | + | ||
| 581 | 637 | func placementOf(ps []Placement, id WindowID) Placement { | |
| 582 | 638 | if i := placementIndex(ps, id); i >= 0 { | |
| 583 | 639 | return ps[i] | |
| @@ -578,6 +578,62 @@ func TestActivateStashModalKeepsFullscreen(t *testing.T) { | |||||
| 578 | } | 578 | } | ||
| 579 | } | 579 | } | ||
| 580 | 580 | ||||
| 581 | + | // A dialog of a dialog (an "Overwrite?" over a "Save as…") shows over | |||
| 582 | + | // the cover like its parent: modal chains of a stashed window or of the | |||
| 583 | + | // cover keep the game fullscreen, and a new one takes the focus. | |||
| 584 | + | func TestDialogChainOverCover(t *testing.T) { | |||
| 585 | + | t.Run("stashed modal chain", func(t *testing.T) { | |||
| 586 | + | w := workspace() | |||
| 587 | + | w.AddWindow(14) | |||
| 588 | + | w.ToggleWindowStash() | |||
| 589 | + | w.ToggleStashVisible() | |||
| 590 | + | w.AddDialog(9, 14, 20, 10) | |||
| 591 | + | w.SetModal(9, true) | |||
| 592 | + | w.AddDialog(8, 9, 10, 5) | |||
| 593 | + | w.SetModal(8, true) | |||
| 594 | + | w.AddFloating(72, 50, 40) | |||
| 595 | + | w.SetFullscreen(72, true) | |||
| 596 | + | w.Activate(14) | |||
| 597 | + | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 || w.left != 0 { | |||
| 598 | + | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |||
| 599 | + | } | |||
| 600 | + | if p := placementOf(w.Layout(), 8); p.Hidden { | |||
| 601 | + | t.Fatalf("confirm hidden: %+v", p) | |||
| 602 | + | } | |||
| 603 | + | }) | |||
| 604 | + | t.Run("stashed dialog maps a dialog", func(t *testing.T) { | |||
| 605 | + | w := workspace() | |||
| 606 | + | w.AddWindow(14) | |||
| 607 | + | w.ToggleWindowStash() | |||
| 608 | + | w.ToggleStashVisible() | |||
| 609 | + | w.AddFloating(72, 50, 40) | |||
| 610 | + | w.SetFullscreen(72, true) | |||
| 611 | + | w.Activate(14) | |||
| 612 | + | w.AddDialog(9, 14, 20, 10) | |||
| 613 | + | w.AddDialog(8, 9, 10, 5) | |||
| 614 | + | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 { | |||
| 615 | + | t.Fatalf("focused %d fullscreen %d", id, w.fullscreen) | |||
| 616 | + | } | |||
| 617 | + | if p := placementOf(w.Layout(), 8); p.Hidden { | |||
| 618 | + | t.Fatalf("confirm hidden: %+v", p) | |||
| 619 | + | } | |||
| 620 | + | }) | |||
| 621 | + | t.Run("cover modal chain", func(t *testing.T) { | |||
| 622 | + | w := workspace() | |||
| 623 | + | w.AddFloating(72, 50, 40) | |||
| 624 | + | w.AddDialog(9, 72, 20, 10) | |||
| 625 | + | w.SetModal(9, true) | |||
| 626 | + | w.AddDialog(8, 9, 10, 5) | |||
| 627 | + | w.SetModal(8, true) | |||
| 628 | + | w.SetFullscreen(72, true) | |||
| 629 | + | w.AddWindow(1) | |||
| 630 | + | w.Activate(72) | |||
| 631 | + | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 || w.left != 0 { | |||
| 632 | + | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |||
| 633 | + | } | |||
| 634 | + | }) | |||
| 635 | + | } | |||
| 636 | + | ||||
| 581 | func placementOf(ps []Placement, id WindowID) Placement { | 637 | func placementOf(ps []Placement, id WindowID) Placement { | ||
| 582 | if i := placementIndex(ps, id); i >= 0 { | 638 | if i := placementIndex(ps, id); i >= 0 { | ||
| 583 | return ps[i] | 639 | return ps[i] | ||
M internal/core/layout.go +21 -4
| @@ -401,13 +401,29 @@ func (w *Workspace) focusModal(from WindowID) { | |||
| 401 | 401 | ||
| 402 | 402 | // overCover reports whether float f is drawn over the covering fullscreen | |
| 403 | 403 | // window: a dialog of the cover, or of a stashed window while the stash | |
| 404 | - | // shows over the cover (a "Save as…" of the app the user picked there). | |
| 404 | + | // shows over the cover (a "Save as…" of the app the user picked there), | |
| 405 | + | // or a dialog of such a dialog (an "Overwrite?" over the "Save as…"). | |
| 405 | 406 | func (w *Workspace) overCover(f Float) bool { | |
| 406 | 407 | c := w.cover() | |
| 407 | - | if c == 0 || f.ID == c || f.over == 0 { | |
| 408 | + | if c == 0 || f.ID == c { | |
| 408 | 409 | return false | |
| 409 | 410 | } | |
| 410 | - | return f.over == c || w.stashOverCover() && w.stashIndex(f.over) >= 0 | |
| 411 | + | over := w.stashOverCover() | |
| 412 | + | // Each hop goes to another float: at most len(w.Floats) hops. | |
| 413 | + | for range len(w.Floats) { | |
| 414 | + | if f.over == 0 || f.over == f.ID { | |
| 415 | + | return false | |
| 416 | + | } | |
| 417 | + | if f.over == c || over && w.stashIndex(f.over) >= 0 { | |
| 418 | + | return true | |
| 419 | + | } | |
| 420 | + | i := w.floatIndex(f.over) | |
| 421 | + | if i < 0 { | |
| 422 | + | return false | |
| 423 | + | } | |
| 424 | + | f = w.Floats[i] | |
| 425 | + | } | |
| 426 | + | return false | |
| 411 | 427 | } | |
| 412 | 428 | ||
| 413 | 429 | // coverDialog is the topmost dialog shown over the covering fullscreen | |
| @@ -683,7 +699,8 @@ func (w *Workspace) FocusColumn(dir int) bool { | |||
| 683 | 699 | from, _ := w.Focused() | |
| 684 | 700 | defer w.focusModal(from) | |
| 685 | 701 | if w.coverDialogFocused() { | |
| 686 | - | // The first move goes back to the fullscreen window under it. | |
| 702 | + | // The first move goes back to the window under it: the stash | |
| 703 | + | // over the cover, or the cover. | |
| 687 | 704 | w.floatFocus = false | |
| 688 | 705 | return true | |
| 689 | 706 | } | |
| @@ -401,13 +401,29 @@ func (w *Workspace) focusModal(from WindowID) { | |||||
| 401 | 401 | ||||
| 402 | // overCover reports whether float f is drawn over the covering fullscreen | 402 | // overCover reports whether float f is drawn over the covering fullscreen | ||
| 403 | // window: a dialog of the cover, or of a stashed window while the stash | 403 | // window: a dialog of the cover, or of a stashed window while the stash | ||
| 404 | - | // shows over the cover (a "Save as…" of the app the user picked there). | 404 | + | // shows over the cover (a "Save as…" of the app the user picked there), |
| 405 | + | // or a dialog of such a dialog (an "Overwrite?" over the "Save as…"). | |||
| 405 | func (w *Workspace) overCover(f Float) bool { | 406 | func (w *Workspace) overCover(f Float) bool { | ||
| 406 | c := w.cover() | 407 | c := w.cover() | ||
| 407 | - | if c == 0 || f.ID == c || f.over == 0 { | 408 | + | if c == 0 || f.ID == c { |
| 408 | return false | 409 | return false | ||
| 409 | } | 410 | } | ||
| 410 | - | return f.over == c || w.stashOverCover() && w.stashIndex(f.over) >= 0 | 411 | + | over := w.stashOverCover() |
| 412 | + | // Each hop goes to another float: at most len(w.Floats) hops. | |||
| 413 | + | for range len(w.Floats) { | |||
| 414 | + | if f.over == 0 || f.over == f.ID { | |||
| 415 | + | return false | |||
| 416 | + | } | |||
| 417 | + | if f.over == c || over && w.stashIndex(f.over) >= 0 { | |||
| 418 | + | return true | |||
| 419 | + | } | |||
| 420 | + | i := w.floatIndex(f.over) | |||
| 421 | + | if i < 0 { | |||
| 422 | + | return false | |||
| 423 | + | } | |||
| 424 | + | f = w.Floats[i] | |||
| 425 | + | } | |||
| 426 | + | return false | |||
| 411 | } | 427 | } | ||
| 412 | 428 | ||||
| 413 | // coverDialog is the topmost dialog shown over the covering fullscreen | 429 | // coverDialog is the topmost dialog shown over the covering fullscreen | ||
| @@ -683,7 +699,8 @@ func (w *Workspace) FocusColumn(dir int) bool { | |||||
| 683 | from, _ := w.Focused() | 699 | from, _ := w.Focused() | ||
| 684 | defer w.focusModal(from) | 700 | defer w.focusModal(from) | ||
| 685 | if w.coverDialogFocused() { | 701 | if w.coverDialogFocused() { | ||
| 686 | - | // The first move goes back to the fullscreen window under it. | 702 | + | // The first move goes back to the window under it: the stash |
| 703 | + | // over the cover, or the cover. | |||
| 687 | w.floatFocus = false | 704 | w.floatFocus = false | ||
| 688 | return true | 705 | return true | ||
| 689 | } | 706 | } | ||