diff --git a/container.go b/container.go index 9ea516b..728c988 100644 --- a/container.go +++ b/container.go @@ -368,15 +368,29 @@ func (c *Context) bringToFront(cnt *container) { } func (c *Context) hoveringRootContainer() *container { - p := c.pointingPosition() + return c.rootContainerAt(c.pointingPosition()) +} + +// rootContainerAt returns the topmost open root container whose hit area contains p, or nil if there is none. +func (c *Context) rootContainerAt(p image.Point) *container { for i := len(c.rootContainers) - 1; i >= 0; i-- { cnt := c.rootContainers[i] if !cnt.open { continue } - if p.In(cnt.layout.Bounds) { + if p.In(c.rootContainerHitBounds(cnt)) { return cnt } } return nil } + +// rootContainerHitBounds returns the area of a root container that receives pointing input. +// A collapsed container shows only its title bar, so only the title bar receives input. +func (c *Context) rootContainerHitBounds(cnt *container) image.Rectangle { + bounds := cnt.layout.Bounds + if cnt.collapsed { + bounds.Max.Y = min(bounds.Max.Y, bounds.Min.Y+c.style().titleHeight) + } + return bounds +} diff --git a/context.go b/context.go index 2cfce8a..3e5eaa2 100644 --- a/context.go +++ b/context.go @@ -113,11 +113,7 @@ func (c *Context) update(f func(ctx *Context) error) (inputCapturingState InputC // Check whether the cursor is on any of the root containers. pt := c.pointingPosition() for _, cnt := range c.rootContainers { - bounds := cnt.layout.Bounds - if cnt.collapsed { - bounds.Max.Y = cnt.layout.BodyBounds.Min.Y - } - if pt.In(bounds) { + if pt.In(c.rootContainerHitBounds(cnt)) { inputCapturingState |= InputCapturingStateHover } } diff --git a/debugui_test.go b/debugui_test.go index 7bd8f9b..f1e28f2 100644 --- a/debugui_test.go +++ b/debugui_test.go @@ -138,3 +138,58 @@ func TestUnusedContainer(t *testing.T) { t.Errorf("got: %v, want: %v", got, want) } } + +func TestCollapsedWindowDoesNotCoverWindowsBelow(t *testing.T) { + const ( + bottom = 0 + top = 1 + ) + var d debugui.DebugUI + update := func() { + t.Helper() + if _, err := d.Update(func(ctx *debugui.Context) error { + ctx.Window("Bottom", image.Rect(50, 70, 260, 180), func(layout debugui.ContainerLayout) {}) + ctx.Window("Top", image.Rect(0, 0, 320, 240), func(layout debugui.ContainerLayout) {}) + return nil + }); err != nil { + t.Fatal(err) + } + } + update() + + // The title bar of Top is 24 pixels tall by default. + onTopTitleBar := image.Pt(10, 10) + onBottomBelowTopTitleBar := image.Pt(100, 100) + onTopBodyOnly := image.Pt(10, 200) + outside := image.Pt(400, 400) + + testCases := []struct { + name string + pt image.Point + want int + setup func() + }{ + {name: "expanded title bar", pt: onTopTitleBar, want: top}, + {name: "expanded body over bottom", pt: onBottomBelowTopTitleBar, want: top}, + {name: "expanded body only", pt: onTopBodyOnly, want: top}, + {name: "outside", pt: outside, want: -1}, + {name: "collapsed title bar", pt: onTopTitleBar, want: top, setup: func() { d.SetRootContainerCollapsed(top, true) }}, + {name: "collapsed title bar bottom edge", pt: image.Pt(10, 23), want: top}, + {name: "collapsed just below title bar", pt: image.Pt(10, 24), want: -1}, + {name: "collapsed body over bottom", pt: onBottomBelowTopTitleBar, want: bottom}, + {name: "collapsed body only", pt: onTopBodyOnly, want: -1}, + {name: "collapsed and moved, old title bar", pt: onTopTitleBar, want: -1, setup: func() { d.MoveRootContainer(top, image.Pt(0, 150)) }}, + {name: "collapsed and moved, new title bar", pt: image.Pt(10, 160), want: top}, + {name: "collapsed and moved, over bottom", pt: onBottomBelowTopTitleBar, want: bottom}, + {name: "expanded again", pt: image.Pt(10, 200), want: top, setup: func() { d.SetRootContainerCollapsed(top, false) }}, + } + for _, tc := range testCases { + if tc.setup != nil { + tc.setup() + update() + } + if got := d.RootContainerIndexAt(tc.pt); got != tc.want { + t.Errorf("%s: RootContainerIndexAt(%v): got: %d, want: %d", tc.name, tc.pt, got, tc.want) + } + } +} diff --git a/export_test.go b/export_test.go index 280af4c..1e7784c 100644 --- a/export_test.go +++ b/export_test.go @@ -3,6 +3,8 @@ package debugui +import "image" + func IDPartFromCaller() string { pc := caller() return idPartFromCaller(pc) @@ -89,3 +91,23 @@ func TextIndexFromX(str string, x int) int { func TextWidth(str string) int { return textWidth(str) } + +// RootContainerIndexAt returns the z-order index of the topmost root container hit at p, or -1. +func (d *DebugUI) RootContainerIndexAt(p image.Point) int { + cnt := d.ctx.rootContainerAt(p) + for i, c := range d.ctx.rootContainers { + if c == cnt { + return i + } + } + return -1 +} + +func (d *DebugUI) SetRootContainerCollapsed(index int, collapsed bool) { + d.ctx.rootContainers[index].collapsed = collapsed +} + +func (d *DebugUI) MoveRootContainer(index int, delta image.Point) { + cnt := d.ctx.rootContainers[index] + cnt.layout.Bounds = cnt.layout.Bounds.Add(delta) +}