package mobile import ( "context" "image" "math" "sync" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/mobile" "fyne.io/fyne/v2/internal/app" intdriver "fyne.io/fyne/v2/internal/driver" "fyne.io/fyne/v2/internal/driver/common" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) var _ fyne.Canvas = (*canvas)(nil) type canvas struct { common.Canvas content fyne.CanvasObject device *device initialized bool lastTapDown map[int]time.Time lastTapDownPos map[int]fyne.Position lastTapDelta map[int]fyne.Delta menu fyne.CanvasObject padded bool scale float32 size fyne.Size touched map[int]mobile.Touchable windowHead fyne.CanvasObject dragOffset fyne.Position dragStart fyne.Position dragging fyne.Draggable onTypedKey func(event *fyne.KeyEvent) onTypedRune func(rune) touchCancelFunc context.CancelFunc touchCancelLock sync.Mutex touchLastTapped fyne.CanvasObject touchTapCount int } func newCanvas(dev fyne.Device) fyne.Canvas { d, _ := dev.(*device) ret := &canvas{ Canvas: common.Canvas{ OnFocus: d.handleKeyboard, OnUnfocus: d.hideVirtualKeyboard, }, device: d, lastTapDown: make(map[int]time.Time), lastTapDownPos: make(map[int]fyne.Position), lastTapDelta: make(map[int]fyne.Delta), padded: true, scale: dev.SystemScaleForWindow(nil), // we don't need a window parameter on mobile, touched: make(map[int]mobile.Touchable), } ret.Initialize(ret, ret.overlayChanged) return ret } func (c *canvas) Capture() image.Image { return c.Painter().Capture(c) } func (c *canvas) Content() fyne.CanvasObject { return c.content } func (c *canvas) InteractiveArea() (fyne.Position, fyne.Size) { var pos fyne.Position var size fyne.Size if c.device == nil { // running in test mode size = c.Size() } else { safeLeft := float32(c.device.safeLeft) / c.scale safeTop := float32(c.device.safeTop) / c.scale safeRight := float32(c.device.safeRight) / c.scale safeBottom := float32(c.device.safeBottom) / c.scale pos = fyne.NewPos(safeLeft, safeTop) size = c.size.SubtractWidthHeight(safeLeft+safeRight, safeTop+safeBottom) } if c.windowHeadIsDisplacing() { offset := c.windowHead.MinSize().Height pos = pos.AddXY(0, offset) size = size.SubtractWidthHeight(0, offset) } return pos, size } func (c *canvas) MinSize() fyne.Size { return c.size // TODO check } func (c *canvas) OnTypedKey() func(*fyne.KeyEvent) { return c.onTypedKey } func (c *canvas) OnTypedRune() func(rune) { return c.onTypedRune } func (c *canvas) PixelCoordinateForPosition(pos fyne.Position) (int, int) { return int(float32(pos.X) * c.scale), int(float32(pos.Y) * c.scale) } func (c *canvas) Resize(size fyne.Size) { if size == c.size { return } c.sizeContent(size) } func (c *canvas) Scale() float32 { return c.scale } func (c *canvas) SetContent(content fyne.CanvasObject) { c.setContent(content) c.sizeContent(c.Size()) // fixed window size for mobile, cannot stretch to new content c.SetDirty() } func (c *canvas) SetOnTypedKey(typed func(*fyne.KeyEvent)) { c.onTypedKey = typed } func (c *canvas) SetOnTypedRune(typed func(rune)) { c.onTypedRune = typed } func (c *canvas) Size() fyne.Size { return c.size } func (c *canvas) applyThemeOutOfTreeObjects() { if c.menu != nil { app.ApplyThemeTo(c.menu, c) // Ensure our menu gets the theme change message as it's out-of-tree } if c.windowHead != nil { app.ApplyThemeTo(c.windowHead, c) // Ensure our child windows get the theme change message as it's out-of-tree } } func (c *canvas) findObjectAtPositionMatching(pos fyne.Position, test func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position, int) { if c.menu != nil { return intdriver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.menu) } return intdriver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.windowHead, c.content) } func (c *canvas) overlayChanged() { c.device.handleKeyboard(c.Focused()) c.SetDirty() } func (c *canvas) setContent(content fyne.CanvasObject) { c.content = content c.SetContentTreeAndFocusMgr(content) } func (c *canvas) setMenu(menu fyne.CanvasObject) { c.menu = menu c.SetMenuTreeAndFocusMgr(menu) } func (c *canvas) setWindowHead(head fyne.CanvasObject) { if c.padded { head = container.NewPadded(head) } c.windowHead = head c.SetMobileWindowHeadTree(head) } func (c *canvas) sizeContent(size fyne.Size) { if c.content == nil { // window may not be configured yet return } c.size = size areaPos, areaSize := c.InteractiveArea() if c.windowHead != nil { var headSize fyne.Size headPos := areaPos if c.windowHeadIsDisplacing() { headSize = fyne.NewSize(areaSize.Width, c.windowHead.MinSize().Height) headPos = headPos.SubtractXY(0, headSize.Height) } else { headSize = c.windowHead.MinSize() } c.windowHead.Resize(headSize) c.windowHead.Move(headPos) } for _, overlay := range c.Overlays().List() { if p, ok := overlay.(*widget.PopUp); ok { // TODO: remove this when #707 is being addressed. // “Notifies” the PopUp of the canvas size change. p.Refresh() } else { overlay.Resize(areaSize) overlay.Move(areaPos) } } if c.padded { c.content.Resize(areaSize.Subtract(fyne.NewSize(theme.Padding()*2, theme.Padding()*2))) c.content.Move(areaPos.Add(fyne.NewPos(theme.Padding(), theme.Padding()))) } else { c.content.Resize(areaSize) c.content.Move(areaPos) } } func (c *canvas) tapDown(pos fyne.Position, tapID int) { c.lastTapDown[tapID] = time.Now() c.lastTapDownPos[tapID] = pos c.dragging = nil co, objPos, layer := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { switch object.(type) { case mobile.Touchable, fyne.Focusable: return true } return false }) if wid, ok := co.(mobile.Touchable); ok { touchEv := &mobile.TouchEvent{} touchEv.Position = objPos touchEv.AbsolutePosition = pos wid.TouchDown(touchEv) c.touched[tapID] = wid } if layer != 1 { // 0 - overlay, 1 - window head / menu, 2 - content if wid, ok := co.(fyne.Focusable); !ok || wid != c.Focused() { c.Unfocus() } } } func (c *canvas) tapMove(pos fyne.Position, tapID int, dragCallback func(fyne.Draggable, *fyne.DragEvent)) { previousPos := c.lastTapDownPos[tapID] deltaX := pos.X - previousPos.X deltaY := pos.Y - previousPos.Y if c.dragging == nil && (math.Abs(float64(deltaX)) < tapMoveThreshold && math.Abs(float64(deltaY)) < tapMoveThreshold) { return } c.lastTapDownPos[tapID] = pos offset := fyne.Delta{DX: deltaX, DY: deltaY} c.lastTapDelta[tapID] = offset 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 { return true } return false }) if c.touched[tapID] != nil { if touch, ok := co.(mobile.Touchable); !ok || c.touched[tapID] != touch { touchEv := &mobile.TouchEvent{} touchEv.Position = objPos touchEv.AbsolutePosition = pos c.touched[tapID].TouchCancel(touchEv) c.touched[tapID] = nil } } if c.dragging == nil { if drag, ok := co.(fyne.Draggable); ok { c.dragging = drag c.dragOffset = previousPos.Subtract(objPos) c.dragStart = co.Position() } else { return } } ev := &fyne.DragEvent{} draggedObjDelta := c.dragStart.Subtract(c.dragging.(fyne.CanvasObject).Position()) ev.Position = pos.Subtract(c.dragOffset).Add(draggedObjDelta) ev.Dragged = offset dragCallback(c.dragging, ev) } func (c *canvas) tapUp(pos fyne.Position, tapID int, tapCallback func(fyne.Tappable, *fyne.PointEvent), tapAltCallback func(fyne.SecondaryTappable, *fyne.PointEvent), doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent), dragCallback func(fyne.Draggable, *fyne.DragEvent)) { if c.dragging != nil { previousDelta := c.lastTapDelta[tapID] ev := &fyne.DragEvent{Dragged: previousDelta} draggedObjDelta := c.dragStart.Subtract(c.dragging.(fyne.CanvasObject).Position()) ev.Position = pos.Subtract(c.dragOffset).Add(draggedObjDelta) ev.AbsolutePosition = pos dragCallback(c.dragging, ev) c.dragging = nil return } duration := time.Since(c.lastTapDown[tapID]) if c.menu != nil && c.Overlays().Top() == nil && pos.X > c.menu.Size().Width { c.menu.Hide() c.menu.Refresh() c.setMenu(nil) return } 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 { return true } else if _, ok := object.(mobile.Touchable); ok { return true } else if _, ok := object.(fyne.DoubleTappable); ok { return true } return false }) if wid, ok := co.(mobile.Touchable); ok { touchEv := &mobile.TouchEvent{} touchEv.Position = objPos touchEv.AbsolutePosition = pos wid.TouchUp(touchEv) c.touched[tapID] = nil } ev := &fyne.PointEvent{ Position: objPos, AbsolutePosition: pos, } if duration < tapSecondaryDelay { _, doubleTap := co.(fyne.DoubleTappable) if doubleTap { c.touchCancelLock.Lock() c.touchTapCount++ c.touchLastTapped = co cancel := c.touchCancelFunc c.touchCancelLock.Unlock() if cancel != nil { cancel() return } go c.waitForDoubleTap(co, ev, tapCallback, doubleTapCallback) } else { if wid, ok := co.(fyne.Tappable); ok { tapCallback(wid, ev) } } } else { if wid, ok := co.(fyne.SecondaryTappable); ok { tapAltCallback(wid, ev) } } } func (c *canvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent, tapCallback func(fyne.Tappable, *fyne.PointEvent), doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent)) { ctx, cancel := context.WithDeadline(context.TODO(), time.Now().Add(tapDoubleDelay)) c.touchCancelLock.Lock() c.touchCancelFunc = cancel c.touchCancelLock.Unlock() defer cancel() <-ctx.Done() fyne.CurrentApp().Driver().DoFromGoroutine(func() { c.touchCancelLock.Lock() touchCount := c.touchTapCount touchLast := c.touchLastTapped c.touchCancelLock.Unlock() if touchCount == 2 && touchLast == co { if wid, ok := co.(fyne.DoubleTappable); ok { doubleTapCallback(wid, ev) } } else { if wid, ok := co.(fyne.Tappable); ok { tapCallback(wid, ev) } } c.touchCancelLock.Lock() c.touchTapCount = 0 c.touchCancelFunc = nil c.touchLastTapped = nil c.touchCancelLock.Unlock() }, true) } func (c *canvas) windowHeadIsDisplacing() bool { if c.windowHead == nil { return false } chromeBox := c.windowHead.(*fyne.Container) if c.padded { chromeBox = chromeBox.Objects[0].(*fyne.Container) // the padded container } return len(chromeBox.Objects) > 1 }