package mobile import ( "math" "runtime" "strconv" "time" "fyne.io/fyne/v2" fynecanvas "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/driver/mobile" "fyne.io/fyne/v2/internal" "fyne.io/fyne/v2/internal/animation" intapp "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/async" "fyne.io/fyne/v2/internal/build" "fyne.io/fyne/v2/internal/cache" intdriver "fyne.io/fyne/v2/internal/driver" "fyne.io/fyne/v2/internal/driver/common" "fyne.io/fyne/v2/internal/driver/mobile/app" "fyne.io/fyne/v2/internal/driver/mobile/event/key" "fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle" "fyne.io/fyne/v2/internal/driver/mobile/event/paint" "fyne.io/fyne/v2/internal/driver/mobile/event/size" "fyne.io/fyne/v2/internal/driver/mobile/event/touch" "fyne.io/fyne/v2/internal/driver/mobile/gl" "fyne.io/fyne/v2/internal/painter" pgl "fyne.io/fyne/v2/internal/painter/gl" "fyne.io/fyne/v2/internal/scale" "fyne.io/fyne/v2/theme" ) const ( tapMoveDecay = 0.92 // how much should the scroll continue decay on each frame? tapMoveEndThreshold = 2.0 // at what offset will we stop decaying? tapMoveThreshold = 4.0 // how far can we move before it is a drag tapSecondaryDelay = 300 * time.Millisecond // how long before secondary tap tapDoubleDelay = 500 * time.Millisecond // max duration between taps for a DoubleTap event ) // Configuration is the system information about the current device type Configuration struct { SystemTheme fyne.ThemeVariant } // ConfiguredDriver is a simple type that allows packages to hook into configuration changes of this driver. type ConfiguredDriver interface { SetOnConfigurationChanged(func(*Configuration)) } type driver struct { app app.App glctx gl.Context windows []fyne.Window device device animation animation.Runner currentSize size.Event theme fyne.ThemeVariant onConfigChanged func(*Configuration) painting bool running bool queuedFuncs *async.UnboundedChan[func()] } // Declare conformity with Driver var _ fyne.Driver = (*driver)(nil) var _ ConfiguredDriver = (*driver)(nil) func init() { runtime.LockOSThread() } func (d *driver) DoFromGoroutine(fn func(), wait bool) { caller := func() { if d.queuedFuncs == nil { fn() // before the app actually starts return } var done chan struct{} if wait { done = common.DonePool.Get() defer common.DonePool.Put(done) } d.queuedFuncs.In() <- func() { fn() if wait { done <- struct{}{} } } if wait { <-done } } if wait { async.EnsureNotMain(caller) } else { caller() } } func (d *driver) CreateWindow(title string) fyne.Window { c := newCanvas(fyne.CurrentDevice()).(*canvas) // silence lint ret := &window{title: title, canvas: c, isChild: len(d.windows) > 0} c.setContent(&fynecanvas.Rectangle{FillColor: theme.Color(theme.ColorNameBackground)}) c.SetPainter(pgl.NewPainter(c, ret)) d.windows = append(d.windows, ret) return ret } func (d *driver) AllWindows() []fyne.Window { return d.windows } // currentWindow returns the most recently opened window - we can only show one at a time. func (d *driver) currentWindow() *window { if len(d.windows) == 0 { return nil } var last *window for i := len(d.windows) - 1; i >= 0; i-- { last = d.windows[i].(*window) if last.visible { return last } } return last } func (d *driver) Clipboard() fyne.Clipboard { return NewClipboard() } func (d *driver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { return painter.RenderedTextSize(text, textSize, style, source) } func (d *driver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas { if len(d.windows) == 0 { return nil } // TODO figure out how we handle multiple windows... return d.currentWindow().Canvas() } func (d *driver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position { c := d.CanvasForObject(co) if c == nil { return fyne.NewPos(0, 0) } mc := c.(*canvas) pos := intdriver.AbsolutePositionForObject(co, mc.ObjectTrees()) inset, _ := c.InteractiveArea() return pos.Subtract(inset) } func (d *driver) GoBack() { app.GoBack() } func (d *driver) Quit() { // Android and iOS guidelines say this should not be allowed! } func (d *driver) Run() { if d.running { return // Run was called twice. } d.running = true app.Main(func(a app.App) { async.SetMainGoroutine() d.app = a d.queuedFuncs = async.NewUnboundedChan[func()]() fyne.CurrentApp().Settings().AddListener(func(s fyne.Settings) { painter.ClearFontCache() cache.ResetThemeCaches() intapp.ApplySettingsWithCallback(s, fyne.CurrentApp(), func(w fyne.Window) { c, ok := w.Canvas().(*canvas) if !ok { return } c.applyThemeOutOfTreeObjects() }) }) draw := time.NewTicker(time.Second / 60) defer func() { l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) // exhaust the event queue go func() { l.WaitForEvents() d.queuedFuncs.Close() }() for fn := range d.queuedFuncs.Out() { fn() } l.DestroyEventQueue() }() for { select { case <-draw.C: d.sendPaintEvent() case fn := <-d.queuedFuncs.Out(): fn() case e, ok := <-a.Events(): if !ok { return // events channel closed, app done } current := d.currentWindow() if current == nil { continue } c := current.Canvas().(*canvas) switch e := a.Filter(e).(type) { case lifecycle.Event: d.handleLifecycle(e, current) case size.Event: if e.WidthPx <= 0 { continue } d.currentSize = e currentOrientation = e.Orientation currentDPI = e.PixelsPerPt * 72 d.setTheme(e.DarkMode) dev := &d.device insetChange := dev.safeTop != e.InsetTopPx || dev.safeBottom != e.InsetBottomPx || dev.safeLeft != e.InsetLeftPx || dev.safeRight != e.InsetRightPx dev.safeTop = e.InsetTopPx dev.safeLeft = e.InsetLeftPx dev.safeBottom = e.InsetBottomPx dev.safeRight = e.InsetRightPx c.scale = fyne.CurrentDevice().SystemScaleForWindow(nil) c.Painter().SetFrameBufferScale(1.0) if insetChange { current.canvas.sizeContent(current.canvas.size) // even if size didn't change we invalidate } // make sure that we paint on the next frame c.Content().Refresh() case paint.Event: d.handlePaint(e, current) case touch.Event: switch e.Type { case touch.TypeBegin: d.tapDownCanvas(current, e.X, e.Y, e.Sequence) case touch.TypeMove: d.tapMoveCanvas(current, e.X, e.Y, e.Sequence) case touch.TypeEnd: d.tapUpCanvas(current, e.X, e.Y, e.Sequence) } case key.Event: if runtime.GOOS == "android" && e.Code == key.CodeDeleteBackspace && e.Rune < 0 && d.device.keyboardShown { break // we are getting release/press on backspace during soft backspace } if e.Direction == key.DirPress { d.typeDownCanvas(c, e.Rune, e.Code, e.Modifiers) } else if e.Direction == key.DirRelease { d.typeUpCanvas(c, e.Rune, e.Code, e.Modifiers) } } } } }) } func (*driver) SetDisableScreenBlanking(disable bool) { setDisableScreenBlank(disable) } func (d *driver) handleLifecycle(e lifecycle.Event, w *window) { c := w.Canvas().(*canvas) switch e.Crosses(lifecycle.StageVisible) { case lifecycle.CrossOn: d.glctx, _ = e.DrawContext.(gl.Context) d.onStart() // this is a fix for some android phone to prevent the app from being drawn as a blank screen after being pushed in the background c.Content().Refresh() d.sendPaintEvent() case lifecycle.CrossOff: d.onStop() d.glctx = nil } switch e.Crosses(lifecycle.StageFocused) { case lifecycle.CrossOn: // foregrounding if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnEnteredForeground(); f != nil { f() } case lifecycle.CrossOff: // will enter background if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { if d.glctx == nil { return } s := fyne.NewSize(float32(d.currentSize.WidthPx)/c.scale, float32(d.currentSize.HeightPx)/c.scale) d.paintWindow(w, s) d.app.Publish() } if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnExitedForeground(); f != nil { f() } } } func (d *driver) handlePaint(e paint.Event, w *window) { c := w.Canvas().(*canvas) if e.Window != 0 { // not all paint events come from hardware w.handle = e.Window } d.painting = false if d.glctx == nil || e.External { return } if !c.initialized { c.initialized = true c.Painter().Init() // we cannot init until the context is set above } d.animation.TickAnimations() canvasNeedRefresh := c.FreeDirtyTextures() > 0 || c.CheckDirtyAndClear() if canvasNeedRefresh { newSize := fyne.NewSize(float32(d.currentSize.WidthPx)/c.scale, float32(d.currentSize.HeightPx)/c.scale) if c.EnsureMinSize() { c.sizeContent(newSize) // force resize of content } else { // if screen changed w.Resize(newSize) } d.paintWindow(w, newSize) d.app.Publish() } cache.Clean(canvasNeedRefresh) } func (d *driver) onStart() { if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnStarted(); f != nil { f() } } func (d *driver) onStop() { l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) if f := l.OnStopped(); f != nil { l.QueueEvent(f) } } func (d *driver) paintWindow(window fyne.Window, size fyne.Size) { clips := &internal.ClipStack{} c := window.Canvas().(*canvas) r, g, b, a := theme.Color(theme.ColorNameBackground).RGBA() max16bit := float32(255 * 255) d.glctx.ClearColor(float32(r)/max16bit, float32(g)/max16bit, float32(b)/max16bit, float32(a)/max16bit) d.glctx.Clear(gl.ColorBufferBit) draw := func(node *common.RenderCacheNode, pos fyne.Position) { obj := node.Obj() if _, ok := obj.(fyne.Scrollable); ok { inner := clips.Push(pos, obj.Size()) c.Painter().StartClipping(inner.Rect()) } if size.Width <= 0 || size.Height <= 0 { // iconifying on Windows can do bad things return } c.Painter().Paint(obj, pos, size) } afterDraw := func(node *common.RenderCacheNode, pos fyne.Position) { if _, ok := node.Obj().(fyne.Scrollable); ok { c.Painter().StopClipping() clips.Pop() if top := clips.Top(); top != nil { c.Painter().StartClipping(top.Rect()) } } if build.Mode == fyne.BuildDebug { c.DrawDebugOverlay(node.Obj(), pos, size) } } c.WalkTrees(draw, afterDraw) } func (d *driver) sendPaintEvent() { if d.painting { return } d.app.Send(paint.Event{}) d.painting = true } func (d *driver) setTheme(dark bool) { var mode fyne.ThemeVariant if dark { mode = theme.VariantDark } else { mode = theme.VariantLight } if d.theme != mode && d.onConfigChanged != nil { d.onConfigChanged(&Configuration{SystemTheme: mode}) } d.theme = mode } func (d *driver) tapDownCanvas(w *window, x, y float32, tapID touch.Sequence) { tapX := scale.ToFyneCoordinate(w.canvas, int(x)) tapY := scale.ToFyneCoordinate(w.canvas, int(y)) pos := fyne.NewPos(tapX, tapY+tapYOffset) w.canvas.tapDown(pos, int(tapID)) } func (d *driver) tapMoveCanvas(w *window, x, y float32, tapID touch.Sequence) { tapX := scale.ToFyneCoordinate(w.canvas, int(x)) tapY := scale.ToFyneCoordinate(w.canvas, int(y)) pos := fyne.NewPos(tapX, tapY+tapYOffset) w.canvas.tapMove(pos, int(tapID), func(wid fyne.Draggable, ev *fyne.DragEvent) { wid.Dragged(ev) }) } func (d *driver) tapUpCanvas(w *window, x, y float32, tapID touch.Sequence) { tapX := scale.ToFyneCoordinate(w.canvas, int(x)) tapY := scale.ToFyneCoordinate(w.canvas, int(y)) pos := fyne.NewPos(tapX, tapY+tapYOffset) w.canvas.tapUp(pos, int(tapID), func(wid fyne.Tappable, ev *fyne.PointEvent) { wid.Tapped(ev) }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { wid.TappedSecondary(ev) }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { wid.DoubleTapped(ev) }, func(wid fyne.Draggable, ev *fyne.DragEvent) { if math.Abs(float64(ev.Dragged.DX)) <= tapMoveEndThreshold && math.Abs(float64(ev.Dragged.DY)) <= tapMoveEndThreshold { wid.DragEnd() return } go func() { for math.Abs(float64(ev.Dragged.DX)) > tapMoveEndThreshold || math.Abs(float64(ev.Dragged.DY)) > tapMoveEndThreshold { if math.Abs(float64(ev.Dragged.DX)) > 0 { ev.Dragged.DX *= tapMoveDecay } if math.Abs(float64(ev.Dragged.DY)) > 0 { ev.Dragged.DY *= tapMoveDecay } d.DoFromGoroutine(func() { wid.Dragged(ev) }, true) time.Sleep(time.Millisecond * 16) } d.DoFromGoroutine(wid.DragEnd, true) }() }) } var keyCodeMap = map[key.Code]fyne.KeyName{ // non-printable key.CodeEscape: fyne.KeyEscape, key.CodeReturnEnter: fyne.KeyReturn, key.CodeTab: fyne.KeyTab, key.CodeDeleteBackspace: fyne.KeyBackspace, key.CodeInsert: fyne.KeyInsert, key.CodePageUp: fyne.KeyPageUp, key.CodePageDown: fyne.KeyPageDown, key.CodeHome: fyne.KeyHome, key.CodeEnd: fyne.KeyEnd, key.CodeLeftArrow: fyne.KeyLeft, key.CodeRightArrow: fyne.KeyRight, key.CodeUpArrow: fyne.KeyUp, key.CodeDownArrow: fyne.KeyDown, key.CodeF1: fyne.KeyF1, key.CodeF2: fyne.KeyF2, key.CodeF3: fyne.KeyF3, key.CodeF4: fyne.KeyF4, key.CodeF5: fyne.KeyF5, key.CodeF6: fyne.KeyF6, key.CodeF7: fyne.KeyF7, key.CodeF8: fyne.KeyF8, key.CodeF9: fyne.KeyF9, key.CodeF10: fyne.KeyF10, key.CodeF11: fyne.KeyF11, key.CodeF12: fyne.KeyF12, key.CodeKeypadEnter: fyne.KeyEnter, // printable key.CodeA: fyne.KeyA, key.CodeB: fyne.KeyB, key.CodeC: fyne.KeyC, key.CodeD: fyne.KeyD, key.CodeE: fyne.KeyE, key.CodeF: fyne.KeyF, key.CodeG: fyne.KeyG, key.CodeH: fyne.KeyH, key.CodeI: fyne.KeyI, key.CodeJ: fyne.KeyJ, key.CodeK: fyne.KeyK, key.CodeL: fyne.KeyL, key.CodeM: fyne.KeyM, key.CodeN: fyne.KeyN, key.CodeO: fyne.KeyO, key.CodeP: fyne.KeyP, key.CodeQ: fyne.KeyQ, key.CodeR: fyne.KeyR, key.CodeS: fyne.KeyS, key.CodeT: fyne.KeyT, key.CodeU: fyne.KeyU, key.CodeV: fyne.KeyV, key.CodeW: fyne.KeyW, key.CodeX: fyne.KeyX, key.CodeY: fyne.KeyY, key.CodeZ: fyne.KeyZ, key.Code0: fyne.Key0, key.CodeKeypad0: fyne.Key0, key.Code1: fyne.Key1, key.CodeKeypad1: fyne.Key1, key.Code2: fyne.Key2, key.CodeKeypad2: fyne.Key2, key.Code3: fyne.Key3, key.CodeKeypad3: fyne.Key3, key.Code4: fyne.Key4, key.CodeKeypad4: fyne.Key4, key.Code5: fyne.Key5, key.CodeKeypad5: fyne.Key5, key.Code6: fyne.Key6, key.CodeKeypad6: fyne.Key6, key.Code7: fyne.Key7, key.CodeKeypad7: fyne.Key7, key.Code8: fyne.Key8, key.CodeKeypad8: fyne.Key8, key.Code9: fyne.Key9, key.CodeKeypad9: fyne.Key9, key.CodeSemicolon: fyne.KeySemicolon, key.CodeEqualSign: fyne.KeyEqual, key.CodeSpacebar: fyne.KeySpace, key.CodeApostrophe: fyne.KeyApostrophe, key.CodeComma: fyne.KeyComma, key.CodeHyphenMinus: fyne.KeyMinus, key.CodeKeypadHyphenMinus: fyne.KeyMinus, key.CodeFullStop: fyne.KeyPeriod, key.CodeKeypadFullStop: fyne.KeyPeriod, key.CodeSlash: fyne.KeySlash, key.CodeLeftSquareBracket: fyne.KeyLeftBracket, key.CodeBackslash: fyne.KeyBackslash, key.CodeRightSquareBracket: fyne.KeyRightBracket, key.CodeGraveAccent: fyne.KeyBackTick, key.CodeBackButton: mobile.KeyBack, } func keyToName(code key.Code) fyne.KeyName { ret, ok := keyCodeMap[code] if !ok { return "" } return ret } func runeToPrintable(r rune) rune { if strconv.IsPrint(r) { return r } return 0 } func (d *driver) typeDownCanvas(canvas *canvas, r rune, code key.Code, mod key.Modifiers) { keyName := keyToName(code) switch keyName { case fyne.KeyTab: capture := false if ent, ok := canvas.Focused().(fyne.Tabbable); ok { capture = ent.AcceptsTab() } if !capture { switch mod { case 0: canvas.FocusNext() return case key.ModShift: canvas.FocusPrevious() return } } } r = runeToPrintable(r) keyEvent := &fyne.KeyEvent{Name: keyName} if canvas.Focused() != nil { if keyName != "" { canvas.Focused().TypedKey(keyEvent) } if r > 0 { canvas.Focused().TypedRune(r) } } else { if keyName != "" { if canvas.onTypedKey != nil { canvas.onTypedKey(keyEvent) } else if keyName == mobile.KeyBack { d.GoBack() } } if r > 0 && canvas.onTypedRune != nil { canvas.onTypedRune(r) } } } func (d *driver) typeUpCanvas(_ *canvas, _ rune, _ key.Code, _ key.Modifiers) { } func (d *driver) Device() fyne.Device { return &d.device } func (d *driver) SetOnConfigurationChanged(f func(*Configuration)) { d.onConfigChanged = f } func (d *driver) DoubleTapDelay() time.Duration { return tapDoubleDelay } // NewGoMobileDriver sets up a new Driver instance implemented using the Go // Mobile extension and OpenGL bindings. func NewGoMobileDriver() fyne.Driver { d := &driver{ theme: fyne.ThemeVariant(2), // unspecified } registerRepository(d) return d }