diff --git a/internal/driver/glfw/window.go b/internal/driver/glfw/window.go index 1890f5ca41..d5a3d15415 100644 --- a/internal/driver/glfw/window.go +++ b/internal/driver/glfw/window.go @@ -472,15 +472,8 @@ func (w *window) refresh(viewport *glfw.Window) { w.canvas.setDirty(true) } -func (w *window) findObjectAtPositionMatching(canvas *glCanvas, mouse fyne.Position, - matches func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position) { - roots := []fyne.CanvasObject{canvas.content} - - if canvas.menu != nil { - roots = []fyne.CanvasObject{canvas.menu, canvas.content} - } - - return driver.FindObjectAtPositionMatching(mouse, matches, canvas.Overlays().Top(), roots...) +func (w *window) findObjectAtPositionMatching(canvas *glCanvas, mouse fyne.Position, matches func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position, int) { + return driver.FindObjectAtPositionMatching(mouse, matches, canvas.Overlays().Top(), canvas.menu, canvas.content) } func fyneToNativeCursor(cursor desktop.Cursor) *glfw.Cursor { @@ -495,7 +488,7 @@ func (w *window) mouseMoved(viewport *glfw.Window, xpos float64, ypos float64) { w.mousePos = fyne.NewPos(internal.UnscaleInt(w.canvas, int(xpos)), internal.UnscaleInt(w.canvas, int(ypos))) cursor := cursorMap[desktop.DefaultCursor] - obj, pos := w.findObjectAtPositionMatching(w.canvas, w.mousePos, func(object fyne.CanvasObject) bool { + obj, pos, _ := w.findObjectAtPositionMatching(w.canvas, w.mousePos, func(object fyne.CanvasObject) bool { if cursorable, ok := object.(desktop.Cursorable); ok { fyneCursor := cursorable.Cursor() cursor = fyneToNativeCursor(fyneCursor) @@ -567,18 +560,9 @@ func (w *window) mouseOut() { } func (w *window) mouseClicked(_ *glfw.Window, btn glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) { - co, pos := w.findObjectAtPositionMatching(w.canvas, w.mousePos, func(object fyne.CanvasObject) bool { - if _, ok := object.(fyne.Tappable); ok { - return true - } else if _, ok := object.(fyne.SecondaryTappable); ok { - return true - } else if _, ok := object.(fyne.Focusable); ok { - return true - } else if _, ok := object.(fyne.Draggable); ok { - return true - } else if _, ok := object.(desktop.Mouseable); ok { - return true - } else if _, ok := object.(desktop.Hoverable); ok { + co, pos, layer := w.findObjectAtPositionMatching(w.canvas, w.mousePos, func(object fyne.CanvasObject) bool { + switch object.(type) { + case fyne.Tappable, fyne.SecondaryTappable, fyne.Focusable, fyne.Draggable, desktop.Mouseable, desktop.Hoverable: return true } @@ -609,13 +593,16 @@ func (w *window) mouseClicked(_ *glfw.Window, btn glfw.MouseButton, action glfw. } } - needsfocus := true - wid := w.canvas.Focused() - if wid != nil { - if wid.(fyne.CanvasObject) != co { - w.canvas.Unfocus() - } else { - needsfocus = false + needsfocus := false + if layer != 1 { // 0 - overlay, 1 - menu, 2 - content + needsfocus = true + + if wid := w.canvas.Focused(); wid != nil { + if wid.(fyne.CanvasObject) != co { + w.canvas.Unfocus() + } else { + needsfocus = false + } } } @@ -684,7 +671,7 @@ func (w *window) mouseClicked(_ *glfw.Window, btn glfw.MouseButton, action glfw. } func (w *window) mouseScrolled(viewport *glfw.Window, xoff float64, yoff float64) { - co, _ := w.findObjectAtPositionMatching(w.canvas, w.mousePos, func(object fyne.CanvasObject) bool { + co, _, _ := w.findObjectAtPositionMatching(w.canvas, w.mousePos, func(object fyne.CanvasObject) bool { _, ok := object.(fyne.Scrollable) return ok }) diff --git a/internal/driver/gomobile/canvas.go b/internal/driver/gomobile/canvas.go index e2ef73c8e1..2ff557fdf5 100644 --- a/internal/driver/gomobile/canvas.go +++ b/internal/driver/gomobile/canvas.go @@ -225,9 +225,9 @@ func (c *mobileCanvas) walkTree( } } -func (c *mobileCanvas) findObjectAtPositionMatching(pos fyne.Position, test func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position) { - if c.menu != nil && c.overlays.Top() == nil { - return driver.FindObjectAtPositionMatching(pos, test, c.menu) +func (c *mobileCanvas) findObjectAtPositionMatching(pos fyne.Position, test func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position, int) { + if c.menu != nil { + return driver.FindObjectAtPositionMatching(pos, test, c.overlays.Top(), c.menu) } return driver.FindObjectAtPositionMatching(pos, test, c.overlays.Top(), c.windowHead, c.content) @@ -238,7 +238,7 @@ func (c *mobileCanvas) tapDown(pos fyne.Position, tapID int) { c.lastTapDownPos[tapID] = pos c.dragging = nil - co, objPos := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { + co, objPos, layer := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { if _, ok := object.(fyne.Tappable); ok { return true } else if _, ok := object.(mobile.Touchable); ok { @@ -258,13 +258,15 @@ func (c *mobileCanvas) tapDown(pos fyne.Position, tapID int) { c.touched[tapID] = wid } - needsFocus := true - wid := c.Focused() - if wid != nil { - if wid.(fyne.CanvasObject) != co { - c.Unfocus() - } else { - needsFocus = false + needsFocus := false + if layer != 1 { // 0 - overlay, 1 - window head / menu, 2 - content + needsFocus = true + if wid := c.Focused(); wid != nil { + if wid.(fyne.CanvasObject) != co { + c.Unfocus() + } else { + needsFocus = false + } } } if wid, ok := co.(fyne.Focusable); ok && needsFocus { @@ -284,7 +286,7 @@ func (c *mobileCanvas) tapMove(pos fyne.Position, tapID int, } c.lastTapDownPos[tapID] = pos - co, objPos := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { + co, objPos, _ := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { if _, ok := object.(fyne.Draggable); ok { return true } else if _, ok := object.(mobile.Touchable); ok { @@ -339,7 +341,7 @@ func (c *mobileCanvas) tapUp(pos fyne.Position, tapID int, return } - co, objPos := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { + co, objPos, _ := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { if _, ok := object.(fyne.Tappable); ok { return true } else if _, ok := object.(fyne.SecondaryTappable); ok { diff --git a/internal/driver/util.go b/internal/driver/util.go index 7352d43b84..5d71463328 100644 --- a/internal/driver/util.go +++ b/internal/driver/util.go @@ -93,8 +93,7 @@ func walkObjectTree( // FindObjectAtPositionMatching is used to find an object in a canvas at the specified position. // The matches function determines of the type of object that is found at this position is of a suitable type. // The various canvas roots and overlays that can be searched are also passed in. -func FindObjectAtPositionMatching(mouse fyne.Position, matches func(object fyne.CanvasObject) bool, - overlay fyne.CanvasObject, roots ...fyne.CanvasObject) (fyne.CanvasObject, fyne.Position) { +func FindObjectAtPositionMatching(mouse fyne.Position, matches func(object fyne.CanvasObject) bool, overlay fyne.CanvasObject, roots ...fyne.CanvasObject) (fyne.CanvasObject, fyne.Position, int) { var found fyne.CanvasObject var foundPos fyne.Position @@ -126,10 +125,12 @@ func FindObjectAtPositionMatching(mouse fyne.Position, matches func(object fyne. return false } + layer := 0 if overlay != nil { WalkVisibleObjectTree(overlay, findFunc, nil) } else { for _, root := range roots { + layer++ if root == nil { continue } @@ -140,7 +141,7 @@ func FindObjectAtPositionMatching(mouse fyne.Position, matches func(object fyne. } } - return found, foundPos + return found, foundPos, layer } // AbsolutePositionForObject returns the absolute position of an object in a set of object trees. diff --git a/internal/driver/util_test.go b/internal/driver/util_test.go index a32018c5fb..882fada630 100644 --- a/internal/driver/util_test.go +++ b/internal/driver/util_test.go @@ -4,68 +4,16 @@ import ( "image/color" "testing" - "github.com/stretchr/testify/assert" - "fyne.io/fyne" "fyne.io/fyne/canvas" "fyne.io/fyne/internal/driver" + internal_widget "fyne.io/fyne/internal/widget" "fyne.io/fyne/layout" _ "fyne.io/fyne/test" "fyne.io/fyne/widget" -) - -func TestWalkVisibleObjectTree(t *testing.T) { - rect := canvas.NewRectangle(color.White) - rect.SetMinSize(fyne.NewSize(100, 100)) - child := canvas.NewRectangle(color.Black) - child.Hide() - base := widget.NewHBox(rect, child) - - walked := 0 - driver.WalkVisibleObjectTree(base, func(object fyne.CanvasObject, position fyne.Position, clippingPos fyne.Position, clippingSize fyne.Size) bool { - walked++ - return false - }, nil) - - assert.Equal(t, 2, walked) -} - -func TestWalkWholeObjectTree(t *testing.T) { - rect := canvas.NewRectangle(color.White) - rect.SetMinSize(fyne.NewSize(100, 100)) - child := canvas.NewRectangle(color.Black) - child.Hide() - base := widget.NewHBox(rect, child) - - walked := 0 - driver.WalkCompleteObjectTree(base, func(object fyne.CanvasObject, position fyne.Position, clippingPos fyne.Position, clippingSize fyne.Size) bool { - walked++ - return false - }, nil) - - assert.Equal(t, 3, walked) -} - -func TestWalkVisibleObjectTree_Clip(t *testing.T) { - rect := canvas.NewRectangle(color.White) - rect.SetMinSize(fyne.NewSize(100, 100)) - child := canvas.NewRectangle(color.Black) - base := fyne.NewContainerWithLayout(layout.NewGridLayout(1), rect, widget.NewScrollContainer(child)) - - clipPos := fyne.NewPos(0, 0) - clipSize := rect.MinSize() - driver.WalkVisibleObjectTree(base, func(object fyne.CanvasObject, position fyne.Position, clippingPos fyne.Position, clippingSize fyne.Size) bool { - if _, ok := object.(*widget.ScrollContainer); ok { - clipPos = clippingPos - clipSize = clippingSize - } - return false - }, nil) - - assert.Equal(t, fyne.NewPos(0, 104), clipPos) - assert.Equal(t, fyne.NewSize(100, 100), clipSize) -} + "github.com/stretchr/testify/assert" +) func TestAbsolutePositionForObject(t *testing.T) { t1r1c1 := widget.NewLabel("row 1 col 1") @@ -164,3 +112,277 @@ func TestAbsolutePositionForObject(t *testing.T) { }) } } + +func TestFindObjectAtPositionMatching(t *testing.T) { + col1cell1 := &objectTree{ + pos: fyne.NewPos(10, 10), + size: fyne.NewSize(15, 15), + } + col1cell2 := &objectTree{ + pos: fyne.NewPos(10, 35), + size: fyne.NewSize(15, 15), + } + col1cell3 := &objectTree{ + pos: fyne.NewPos(10, 60), + size: fyne.NewSize(15, 15), + } + col1 := &objectTree{ + children: []fyne.CanvasObject{col1cell1, col1cell2, col1cell3}, + pos: fyne.NewPos(10, 10), + size: fyne.NewSize(35, 80), + } + col2cell1 := &objectTree{ + pos: fyne.NewPos(10, 10), + size: fyne.NewSize(15, 15), + } + col2cell2 := &objectTree{ + pos: fyne.NewPos(10, 35), + size: fyne.NewSize(15, 15), + } + col2cell3 := &objectTree{ + pos: fyne.NewPos(10, 60), + size: fyne.NewSize(15, 15), + } + col2 := &objectTree{ + children: []fyne.CanvasObject{col2cell1, col2cell2, col2cell3}, + pos: fyne.NewPos(55, 10), + size: fyne.NewSize(35, 80), + } + colTree := &objectTree{ + children: []fyne.CanvasObject{col1, col2}, + pos: fyne.NewPos(10, 10), + size: fyne.NewSize(100, 100), + } + row1cell1 := &objectTree{ + pos: fyne.NewPos(10, 10), + size: fyne.NewSize(15, 15), + } + row1cell2 := &objectTree{ + pos: fyne.NewPos(35, 10), + size: fyne.NewSize(15, 15), + } + row1cell3 := &objectTree{ + pos: fyne.NewPos(60, 10), + size: fyne.NewSize(15, 15), + } + row1 := &objectTree{ + children: []fyne.CanvasObject{row1cell1, row1cell2, row1cell3}, + pos: fyne.NewPos(10, 10), + size: fyne.NewSize(80, 35), + } + row2cell1 := &objectTree{ + pos: fyne.NewPos(10, 10), + size: fyne.NewSize(15, 15), + } + row2cell2 := &objectTree{ + pos: fyne.NewPos(35, 10), + size: fyne.NewSize(15, 15), + } + row2cell3 := &objectTree{ + pos: fyne.NewPos(60, 10), + size: fyne.NewSize(15, 15), + } + row2 := &objectTree{ + children: []fyne.CanvasObject{row2cell1, row2cell2, row2cell3}, + pos: fyne.NewPos(10, 55), + size: fyne.NewSize(80, 35), + } + rowTree := &objectTree{ + children: []fyne.CanvasObject{row1, row2}, + pos: fyne.NewPos(10, 10), + size: fyne.NewSize(100, 100), + } + tree1 := &objectTree{ + pos: fyne.NewPos(100, 100), + size: fyne.NewSize(5, 5), + } + tree2 := &objectTree{ + pos: fyne.NewPos(0, 0), + size: fyne.NewSize(5, 5), + } + tree3 := &objectTree{ + pos: fyne.NewPos(50, 50), + size: fyne.NewSize(5, 5), + } + for name, tt := range map[string]struct { + matcher func(object fyne.CanvasObject) bool + overlay fyne.CanvasObject + pos fyne.Position + roots []fyne.CanvasObject + wantObject fyne.CanvasObject + wantPos fyne.Position + wantLayer int + }{ + "match in overlay and roots": { + matcher: func(o fyne.CanvasObject) bool { return o.Size().Width == 15 }, + overlay: colTree, + pos: fyne.NewPos(35, 60), + roots: []fyne.CanvasObject{rowTree}, + wantObject: col1cell2, + wantPos: fyne.NewPos(5, 5), + wantLayer: 0, + }, + "match in root but overlay without match present": { + matcher: func(o fyne.CanvasObject) bool { return o.Size().Width == 15 }, + overlay: tree1, + pos: fyne.NewPos(35, 60), + roots: []fyne.CanvasObject{colTree, rowTree}, + wantObject: nil, + wantPos: fyne.Position{}, + wantLayer: 0, + }, + "match in multiple roots without overlay": { + matcher: func(o fyne.CanvasObject) bool { return o.Size().Width == 15 }, + overlay: nil, + pos: fyne.NewPos(83, 83), + roots: []fyne.CanvasObject{tree1, rowTree, tree2, colTree}, + wantObject: row2cell3, + wantPos: fyne.NewPos(3, 8), + wantLayer: 2, + }, + "no match in roots without overlay": { + matcher: func(o fyne.CanvasObject) bool { return true }, + overlay: nil, + pos: fyne.NewPos(66, 66), + roots: []fyne.CanvasObject{tree1, tree2, tree3}, + wantObject: nil, + wantPos: fyne.Position{}, + wantLayer: 3, + }, + "no overlay and no roots": { + matcher: func(o fyne.CanvasObject) bool { return true }, + overlay: nil, + pos: fyne.NewPos(66, 66), + roots: nil, + wantObject: nil, + wantPos: fyne.Position{}, + wantLayer: 0, + }, + } { + t.Run(name, func(t *testing.T) { + o, p, l := driver.FindObjectAtPositionMatching(tt.pos, tt.matcher, tt.overlay, tt.roots...) + assert.Equal(t, tt.wantObject, o, "found object") + assert.Equal(t, tt.wantPos, p, "position of found object") + assert.Equal(t, tt.wantLayer, l, "layer of found object (0 - overlay, 1, 2, 3… - roots") + }) + } +} + +func TestWalkVisibleObjectTree(t *testing.T) { + rect := canvas.NewRectangle(color.White) + rect.SetMinSize(fyne.NewSize(100, 100)) + child := canvas.NewRectangle(color.Black) + child.Hide() + base := widget.NewHBox(rect, child) + + walked := 0 + driver.WalkVisibleObjectTree(base, func(object fyne.CanvasObject, position fyne.Position, clippingPos fyne.Position, clippingSize fyne.Size) bool { + walked++ + return false + }, nil) + + assert.Equal(t, 2, walked) +} + +func TestWalkVisibleObjectTree_Clip(t *testing.T) { + rect := canvas.NewRectangle(color.White) + rect.SetMinSize(fyne.NewSize(100, 100)) + child := canvas.NewRectangle(color.Black) + base := fyne.NewContainerWithLayout(layout.NewGridLayout(1), rect, widget.NewScrollContainer(child)) + + clipPos := fyne.NewPos(0, 0) + clipSize := rect.MinSize() + + driver.WalkVisibleObjectTree(base, func(object fyne.CanvasObject, position fyne.Position, clippingPos fyne.Position, clippingSize fyne.Size) bool { + if _, ok := object.(*widget.ScrollContainer); ok { + clipPos = clippingPos + clipSize = clippingSize + } + return false + }, nil) + + assert.Equal(t, fyne.NewPos(0, 104), clipPos) + assert.Equal(t, fyne.NewSize(100, 100), clipSize) +} + +func TestWalkWholeObjectTree(t *testing.T) { + rect := canvas.NewRectangle(color.White) + rect.SetMinSize(fyne.NewSize(100, 100)) + child := canvas.NewRectangle(color.Black) + child.Hide() + base := widget.NewHBox(rect, child) + + walked := 0 + driver.WalkCompleteObjectTree(base, func(object fyne.CanvasObject, position fyne.Position, clippingPos fyne.Position, clippingSize fyne.Size) bool { + walked++ + return false + }, nil) + + assert.Equal(t, 3, walked) +} + +var _ fyne.Widget = (*objectTree)(nil) + +type objectTree struct { + children []fyne.CanvasObject + hidden bool + pos fyne.Position + size fyne.Size +} + +func (o objectTree) Size() fyne.Size { + return o.size +} + +func (o objectTree) Resize(size fyne.Size) { + o.size = size +} + +func (o objectTree) Position() fyne.Position { + return o.pos +} + +func (o objectTree) Move(position fyne.Position) { + o.pos = position +} + +func (o objectTree) MinSize() fyne.Size { + return o.size +} + +func (o objectTree) Visible() bool { + return !o.hidden +} + +func (o objectTree) Show() { + o.hidden = false +} + +func (o objectTree) Hide() { + o.hidden = true +} + +func (o objectTree) Refresh() { +} + +func (o objectTree) CreateRenderer() fyne.WidgetRenderer { + r := &objectTreeRenderer{} + r.SetObjects(o.children) + return r +} + +var _ fyne.WidgetRenderer = (*objectTreeRenderer)(nil) + +type objectTreeRenderer struct { + internal_widget.BaseRenderer +} + +func (o objectTreeRenderer) Layout(_ fyne.Size) { +} + +func (o objectTreeRenderer) MinSize() fyne.Size { + return fyne.NewSize(0, 0) +} + +func (o objectTreeRenderer) Refresh() { +} diff --git a/internal/widget/menu_bar.go b/internal/widget/menu_bar.go index 32f37fbbe8..e7a5ce87fa 100644 --- a/internal/widget/menu_bar.go +++ b/internal/widget/menu_bar.go @@ -142,7 +142,7 @@ type menuBarBackground struct { } var _ fyne.Widget = (*menuBarBackground)(nil) -var _ fyne.Tappable = (*menuBarBackground)(nil) // unfocus menu on click outside +var _ fyne.Tappable = (*menuBarBackground)(nil) // deactivate menu on click outside var _ desktop.Hoverable = (*menuBarBackground)(nil) // block hover events on main content // CreateRenderer satisfies the fyne.Widget interface. diff --git a/test/util.go b/test/util.go index 76ef890409..fbf591c1f6 100644 --- a/test/util.go +++ b/test/util.go @@ -77,7 +77,7 @@ func MoveMouse(c fyne.Canvas, pos fyne.Position) { } return false } - o, absPos := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + o, absPos, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) if o != nil { hovered = o.(desktop.Hoverable) me := &desktop.MouseEvent{ @@ -172,14 +172,15 @@ func WithTestTheme(t *testing.T, f func()) { f() } -func findTappable(c fyne.Canvas, pos fyne.Position) (fyne.CanvasObject, fyne.Position) { +func findTappable(c fyne.Canvas, pos fyne.Position) (o fyne.CanvasObject, p fyne.Position) { matches := func(object fyne.CanvasObject) bool { if _, ok := object.(fyne.Tappable); ok { return true } return false } - return driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + o, p, _ = driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + return } func prepareTap(obj interface{}, pos fyne.Position) (*fyne.PointEvent, fyne.Canvas) {