Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ had changed in them - which is the gap this file exists to close.

### Fixed

- **Menus close on Escape or a click outside them.** A dropdown stayed open
until its title was clicked again or an item picked. A click outside now
closes it without acting on whatever was underneath.
- **The Nodes panel shows its filter once.** The panel drew the table's
filter in its own box and the table drew it again underneath, so typing
showed the text twice with two carets.
Expand Down
47 changes: 47 additions & 0 deletions internal/ui/shell/interact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"gioui.org/f32"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
Expand Down Expand Up @@ -141,3 +142,49 @@ func TestClickingAViewTabSwitchesTheView(t *testing.T) {
t.Fatalf("the view did not change from %v", before)
}
}

// Escape closes an open menu (#769).
func TestEscapeClosesAMenu(t *testing.T) {
h := newHarness(t)
h.sh.SetMenu("View", []MenuItem{{Label: "Something", Action: "x"}})
h.frame()
h.click(f32.Pt(float32(h.sh.menus[1].x)+6, 8))
if h.sh.openMenu != 1 {
t.Fatalf("the menu did not open: %d", h.sh.openMenu)
}
h.r.Queue(key.Event{Name: key.NameEscape, State: key.Press})
h.frame()
if h.sh.openMenu != -1 {
t.Fatalf("Escape left menu %d open", h.sh.openMenu)
}
}

// A press anywhere outside an open menu closes it, and a press on another
// title moves to that menu instead (#769).
func TestAClickOutsideClosesAMenu(t *testing.T) {
h := newHarness(t)
h.sh.OnMenu = func(a string) { h.actions = append(h.actions, a) }
h.sh.SetMenu("View", []MenuItem{{Label: "Something", Action: "x", Section: "One"}})
h.sh.SetMenu("Simulation", []MenuItem{{Label: "Else", Action: "y"}})
h.frame()
h.click(f32.Pt(float32(h.sh.menus[1].x)+6, 8))
if h.sh.openMenu != 1 {
t.Fatalf("the menu did not open: %d", h.sh.openMenu)
}
// Inside the dropdown, on its heading: stays open.
h.click(f32.Pt(float32(h.sh.menus[1].x)+20, float32(h.th.RowHeight())+12))
if h.sh.openMenu != 1 {
t.Fatalf("a press inside the dropdown closed it")
}
h.click(f32.Pt(float32(h.sh.menus[2].x)+6, 8))
if h.sh.openMenu != 2 {
t.Fatalf("pressing the next title left %d open, want 2", h.sh.openMenu)
}
h.click(f32.Pt(900, 600))
if h.sh.openMenu != -1 {
t.Fatalf("a press in the body left menu %d open", h.sh.openMenu)
}
if len(h.actions) != 0 {
t.Errorf("closing the menu ran %v", h.actions)
}
}
16 changes: 15 additions & 1 deletion internal/ui/shell/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ func (sh *Shell) menuDrop(t *theme.Theme, gtx layout.Context) {
if len(m.items) == 0 {
return
}
if sh.menuDismissed(gtx) {
sh.openMenu = -1
return
}
for i := range m.items {
if m.items[i].click.Clicked(gtx) {
sh.openMenu = -1
Expand Down Expand Up @@ -348,8 +352,18 @@ func (sh *Shell) menuDrop(t *theme.Theme, gtx layout.Context) {
if x+box.X > gtx.Constraints.Max.X {
x = gtx.Constraints.Max.X - box.X
}
off := op.Offset(image.Pt(x, gtx.Dp(t.RowHeight()))).Push(gtx.Ops)
top := gtx.Dp(t.RowHeight())
// The scrim: everything below the bar, so a press anywhere else closes
// the menu. The bar itself stays live, so pressing another title moves
// to that menu rather than only closing this one.
scrim := clip.Rect{Min: image.Pt(0, top), Max: gtx.Constraints.Max}.Push(gtx.Ops)
event.Op(gtx.Ops, &sh.menuScrim)
scrim.Pop()
off := op.Offset(image.Pt(x, top)).Push(gtx.Ops)
defer off.Pop()
area := clip.Rect{Max: box}.Push(gtx.Ops)
event.Op(gtx.Ops, &sh.menuBox)
area.Pop()
paint.FillShape(gtx.Ops, theme.Alpha(t.P.Panel, 0.98), clip.Rect{Max: box}.Op())
comp.Border(gtx, box, 2, 1, t.P.Rule)
in := op.Offset(image.Pt(pad, pad)).Push(gtx.Ops)
Expand Down
41 changes: 41 additions & 0 deletions internal/ui/shell/menudismiss.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Closing an open menu without choosing anything from it.
package shell

import (
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/layout"
)

// menuDismissed reports whether the open menu was asked to close: Escape, or a
// press outside it. The press is taken by the scrim rather than passed on, so
// a click that closes a menu does not also act on the map or panel beneath.
func (sh *Shell) menuDismissed(gtx layout.Context) bool {
gone := false
for {
ev, ok := gtx.Event(key.Filter{Name: key.NameEscape})
if !ok {
break
}
if ke, ok := ev.(key.Event); ok && ke.State == key.Press {
gone = true
}
}
for {
ev, ok := gtx.Event(pointer.Filter{Target: &sh.menuScrim, Kinds: pointer.Press})
if !ok {
break
}
if pe, ok := ev.(pointer.Event); ok && pe.Kind == pointer.Press {
gone = true
}
}
// Presses on the box itself are drained and ignored: they are what keeps
// a click on a heading or the padding from falling through to the scrim.
for {
if _, ok := gtx.Event(pointer.Filter{Target: &sh.menuBox, Kinds: pointer.Press}); !ok {
break
}
}
return gone
}
6 changes: 5 additions & 1 deletion internal/ui/shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ type Shell struct {
// openMenu is which menu is showing, or -1. Not zero: zero is the first
// menu, and a zero value that means "File is open" opens it on launch.
openMenu int
tr transport
// menuScrim and menuBox are the pointer targets an open dropdown lays
// down: the scrim over the window below the bar, which closes it, and the
// dropdown's own box, which keeps a press on a heading from reaching it.
menuScrim, menuBox struct{}
tr transport
// rfChip is the counts line's physics toggle.
rfChip widget.Clickable
snap *state.Snapshot
Expand Down