canvas.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. package mobile
  2. import (
  3. "context"
  4. "image"
  5. "math"
  6. "time"
  7. "fyne.io/fyne/v2"
  8. "fyne.io/fyne/v2/container"
  9. "fyne.io/fyne/v2/driver/mobile"
  10. "fyne.io/fyne/v2/internal/app"
  11. "fyne.io/fyne/v2/internal/driver"
  12. "fyne.io/fyne/v2/internal/driver/common"
  13. "fyne.io/fyne/v2/theme"
  14. "fyne.io/fyne/v2/widget"
  15. )
  16. const (
  17. doubleClickDelay = 500 // ms (maximum interval between clicks for double click detection)
  18. )
  19. var _ fyne.Canvas = (*mobileCanvas)(nil)
  20. type mobileCanvas struct {
  21. common.Canvas
  22. content fyne.CanvasObject
  23. windowHead, menu fyne.CanvasObject
  24. scale float32
  25. size fyne.Size
  26. touched map[int]mobile.Touchable
  27. padded, debug bool
  28. onTypedRune func(rune)
  29. onTypedKey func(event *fyne.KeyEvent)
  30. inited bool
  31. lastTapDown map[int]time.Time
  32. lastTapDownPos map[int]fyne.Position
  33. dragging fyne.Draggable
  34. dragStart, dragOffset fyne.Position
  35. touchTapCount int
  36. touchCancelFunc context.CancelFunc
  37. touchLastTapped fyne.CanvasObject
  38. }
  39. // NewCanvas creates a new gomobile mobileCanvas. This is a mobileCanvas that will render on a mobile device using OpenGL.
  40. func NewCanvas() fyne.Canvas {
  41. ret := &mobileCanvas{padded: true}
  42. ret.debug = fyne.CurrentApp().Settings().BuildType() == fyne.BuildDebug
  43. ret.scale = fyne.CurrentDevice().SystemScaleForWindow(nil) // we don't need a window parameter on mobile
  44. ret.touched = make(map[int]mobile.Touchable)
  45. ret.lastTapDownPos = make(map[int]fyne.Position)
  46. ret.lastTapDown = make(map[int]time.Time)
  47. ret.Initialize(ret, ret.overlayChanged)
  48. ret.OnFocus = ret.handleKeyboard
  49. ret.OnUnfocus = hideVirtualKeyboard
  50. return ret
  51. }
  52. func (c *mobileCanvas) Capture() image.Image {
  53. return c.Painter().Capture(c)
  54. }
  55. func (c *mobileCanvas) Content() fyne.CanvasObject {
  56. return c.content
  57. }
  58. func (c *mobileCanvas) InteractiveArea() (fyne.Position, fyne.Size) {
  59. scale := fyne.CurrentDevice().SystemScaleForWindow(nil) // we don't need a window parameter on mobile
  60. dev, ok := fyne.CurrentDevice().(*device)
  61. if !ok || dev.safeWidth == 0 || dev.safeHeight == 0 {
  62. return fyne.NewPos(0, 0), c.Size() // running in test mode
  63. }
  64. return fyne.NewPos(float32(dev.safeLeft)/scale, float32(dev.safeTop)/scale),
  65. fyne.NewSize(float32(dev.safeWidth)/scale, float32(dev.safeHeight)/scale)
  66. }
  67. func (c *mobileCanvas) OnTypedKey() func(*fyne.KeyEvent) {
  68. return c.onTypedKey
  69. }
  70. func (c *mobileCanvas) OnTypedRune() func(rune) {
  71. return c.onTypedRune
  72. }
  73. func (c *mobileCanvas) PixelCoordinateForPosition(pos fyne.Position) (int, int) {
  74. return int(float32(pos.X) * c.scale), int(float32(pos.Y) * c.scale)
  75. }
  76. func (c *mobileCanvas) Scale() float32 {
  77. return c.scale
  78. }
  79. func (c *mobileCanvas) SetContent(content fyne.CanvasObject) {
  80. c.setContent(content)
  81. c.sizeContent(c.Size()) // fixed window size for mobile, cannot stretch to new content
  82. c.SetDirty()
  83. }
  84. func (c *mobileCanvas) SetOnTypedKey(typed func(*fyne.KeyEvent)) {
  85. c.onTypedKey = typed
  86. }
  87. func (c *mobileCanvas) SetOnTypedRune(typed func(rune)) {
  88. c.onTypedRune = typed
  89. }
  90. func (c *mobileCanvas) Size() fyne.Size {
  91. return c.size
  92. }
  93. func (c *mobileCanvas) MinSize() fyne.Size {
  94. return c.size // TODO check
  95. }
  96. func (c *mobileCanvas) findObjectAtPositionMatching(pos fyne.Position, test func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position, int) {
  97. if c.menu != nil {
  98. return driver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.menu)
  99. }
  100. return driver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.windowHead, c.content)
  101. }
  102. func (c *mobileCanvas) handleKeyboard(obj fyne.Focusable) {
  103. isDisabled := false
  104. if disWid, ok := obj.(fyne.Disableable); ok {
  105. isDisabled = disWid.Disabled()
  106. }
  107. if obj != nil && !isDisabled {
  108. if keyb, ok := obj.(mobile.Keyboardable); ok {
  109. showVirtualKeyboard(keyb.Keyboard())
  110. } else {
  111. showVirtualKeyboard(mobile.DefaultKeyboard)
  112. }
  113. } else {
  114. hideVirtualKeyboard()
  115. }
  116. }
  117. func (c *mobileCanvas) overlayChanged() {
  118. c.handleKeyboard(c.Focused())
  119. c.SetDirty()
  120. }
  121. func (c *mobileCanvas) Resize(size fyne.Size) {
  122. if size == c.size {
  123. return
  124. }
  125. c.sizeContent(size)
  126. }
  127. func (c *mobileCanvas) setContent(content fyne.CanvasObject) {
  128. c.content = content
  129. c.SetContentTreeAndFocusMgr(content)
  130. }
  131. func (c *mobileCanvas) setMenu(menu fyne.CanvasObject) {
  132. c.menu = menu
  133. c.SetMenuTreeAndFocusMgr(menu)
  134. }
  135. func (c *mobileCanvas) setWindowHead(head fyne.CanvasObject) {
  136. if c.padded {
  137. head = container.NewPadded(head)
  138. }
  139. c.windowHead = head
  140. c.SetMobileWindowHeadTree(head)
  141. }
  142. func (c *mobileCanvas) applyThemeOutOfTreeObjects() {
  143. if c.menu != nil {
  144. app.ApplyThemeTo(c.menu, c) // Ensure our menu gets the theme change message as it's out-of-tree
  145. }
  146. if c.windowHead != nil {
  147. app.ApplyThemeTo(c.windowHead, c) // Ensure our child windows get the theme change message as it's out-of-tree
  148. }
  149. }
  150. func (c *mobileCanvas) sizeContent(size fyne.Size) {
  151. if c.content == nil { // window may not be configured yet
  152. return
  153. }
  154. c.size = size
  155. offset := fyne.NewPos(0, 0)
  156. areaPos, areaSize := c.InteractiveArea()
  157. if c.windowHead != nil {
  158. topHeight := c.windowHead.MinSize().Height
  159. if len(c.windowHead.(*fyne.Container).Objects) > 1 {
  160. c.windowHead.Resize(fyne.NewSize(areaSize.Width, topHeight))
  161. offset = fyne.NewPos(0, topHeight)
  162. areaSize = areaSize.Subtract(offset)
  163. } else {
  164. c.windowHead.Resize(c.windowHead.MinSize())
  165. }
  166. c.windowHead.Move(areaPos)
  167. }
  168. topLeft := areaPos.Add(offset)
  169. for _, overlay := range c.Overlays().List() {
  170. if p, ok := overlay.(*widget.PopUp); ok {
  171. // TODO: remove this when #707 is being addressed.
  172. // “Notifies” the PopUp of the canvas size change.
  173. p.Refresh()
  174. } else {
  175. overlay.Resize(areaSize)
  176. overlay.Move(topLeft)
  177. }
  178. }
  179. if c.padded {
  180. c.content.Resize(areaSize.Subtract(fyne.NewSize(theme.Padding()*2, theme.Padding()*2)))
  181. c.content.Move(topLeft.Add(fyne.NewPos(theme.Padding(), theme.Padding())))
  182. } else {
  183. c.content.Resize(areaSize)
  184. c.content.Move(topLeft)
  185. }
  186. }
  187. func (c *mobileCanvas) tapDown(pos fyne.Position, tapID int) {
  188. c.lastTapDown[tapID] = time.Now()
  189. c.lastTapDownPos[tapID] = pos
  190. c.dragging = nil
  191. co, objPos, layer := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool {
  192. switch object.(type) {
  193. case mobile.Touchable, fyne.Focusable:
  194. return true
  195. }
  196. return false
  197. })
  198. if wid, ok := co.(mobile.Touchable); ok {
  199. touchEv := &mobile.TouchEvent{}
  200. touchEv.Position = objPos
  201. touchEv.AbsolutePosition = pos
  202. wid.TouchDown(touchEv)
  203. c.touched[tapID] = wid
  204. }
  205. if layer != 1 { // 0 - overlay, 1 - window head / menu, 2 - content
  206. if wid, ok := co.(fyne.Focusable); !ok || wid != c.Focused() {
  207. c.Unfocus()
  208. }
  209. }
  210. }
  211. func (c *mobileCanvas) tapMove(pos fyne.Position, tapID int,
  212. dragCallback func(fyne.Draggable, *fyne.DragEvent)) {
  213. previousPos := c.lastTapDownPos[tapID]
  214. deltaX := pos.X - previousPos.X
  215. deltaY := pos.Y - previousPos.Y
  216. if c.dragging == nil && (math.Abs(float64(deltaX)) < tapMoveThreshold && math.Abs(float64(deltaY)) < tapMoveThreshold) {
  217. return
  218. }
  219. c.lastTapDownPos[tapID] = pos
  220. co, objPos, _ := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool {
  221. if _, ok := object.(fyne.Draggable); ok {
  222. return true
  223. } else if _, ok := object.(mobile.Touchable); ok {
  224. return true
  225. }
  226. return false
  227. })
  228. if c.touched[tapID] != nil {
  229. if touch, ok := co.(mobile.Touchable); !ok || c.touched[tapID] != touch {
  230. touchEv := &mobile.TouchEvent{}
  231. touchEv.Position = objPos
  232. touchEv.AbsolutePosition = pos
  233. c.touched[tapID].TouchCancel(touchEv)
  234. c.touched[tapID] = nil
  235. }
  236. }
  237. if c.dragging == nil {
  238. if drag, ok := co.(fyne.Draggable); ok {
  239. c.dragging = drag
  240. c.dragOffset = previousPos.Subtract(objPos)
  241. c.dragStart = co.Position()
  242. } else {
  243. return
  244. }
  245. }
  246. ev := &fyne.DragEvent{}
  247. draggedObjDelta := c.dragStart.Subtract(c.dragging.(fyne.CanvasObject).Position())
  248. ev.Position = pos.Subtract(c.dragOffset).Add(draggedObjDelta)
  249. ev.Dragged = fyne.Delta{DX: deltaX, DY: deltaY}
  250. dragCallback(c.dragging, ev)
  251. }
  252. func (c *mobileCanvas) tapUp(pos fyne.Position, tapID int,
  253. tapCallback func(fyne.Tappable, *fyne.PointEvent),
  254. tapAltCallback func(fyne.SecondaryTappable, *fyne.PointEvent),
  255. doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent),
  256. dragCallback func(fyne.Draggable)) {
  257. if c.dragging != nil {
  258. dragCallback(c.dragging)
  259. c.dragging = nil
  260. return
  261. }
  262. duration := time.Since(c.lastTapDown[tapID])
  263. if c.menu != nil && c.Overlays().Top() == nil && pos.X > c.menu.Size().Width {
  264. c.menu.Hide()
  265. c.menu.Refresh()
  266. c.setMenu(nil)
  267. return
  268. }
  269. co, objPos, _ := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool {
  270. if _, ok := object.(fyne.Tappable); ok {
  271. return true
  272. } else if _, ok := object.(fyne.SecondaryTappable); ok {
  273. return true
  274. } else if _, ok := object.(mobile.Touchable); ok {
  275. return true
  276. } else if _, ok := object.(fyne.DoubleTappable); ok {
  277. return true
  278. }
  279. return false
  280. })
  281. if wid, ok := co.(mobile.Touchable); ok {
  282. touchEv := &mobile.TouchEvent{}
  283. touchEv.Position = objPos
  284. touchEv.AbsolutePosition = pos
  285. wid.TouchUp(touchEv)
  286. c.touched[tapID] = nil
  287. }
  288. ev := &fyne.PointEvent{
  289. Position: objPos,
  290. AbsolutePosition: pos,
  291. }
  292. if duration < tapSecondaryDelay {
  293. _, doubleTap := co.(fyne.DoubleTappable)
  294. if doubleTap {
  295. c.touchTapCount++
  296. c.touchLastTapped = co
  297. if c.touchCancelFunc != nil {
  298. c.touchCancelFunc()
  299. return
  300. }
  301. go c.waitForDoubleTap(co, ev, tapCallback, doubleTapCallback)
  302. } else {
  303. if wid, ok := co.(fyne.Tappable); ok {
  304. tapCallback(wid, ev)
  305. }
  306. }
  307. } else {
  308. if wid, ok := co.(fyne.SecondaryTappable); ok {
  309. tapAltCallback(wid, ev)
  310. }
  311. }
  312. }
  313. func (c *mobileCanvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent, tapCallback func(fyne.Tappable, *fyne.PointEvent), doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent)) {
  314. var ctx context.Context
  315. ctx, c.touchCancelFunc = context.WithDeadline(context.TODO(), time.Now().Add(time.Millisecond*doubleClickDelay))
  316. defer c.touchCancelFunc()
  317. <-ctx.Done()
  318. if c.touchTapCount == 2 && c.touchLastTapped == co {
  319. if wid, ok := co.(fyne.DoubleTappable); ok {
  320. doubleTapCallback(wid, ev)
  321. }
  322. } else {
  323. if wid, ok := co.(fyne.Tappable); ok {
  324. tapCallback(wid, ev)
  325. }
  326. }
  327. c.touchTapCount = 0
  328. c.touchCancelFunc = nil
  329. c.touchLastTapped = nil
  330. }