adam-gui/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver.go
2024-04-29 19:13:50 +02:00

557 lines
14 KiB
Go

package mobile
import (
"runtime"
"strconv"
"time"
"fyne.io/fyne/v2"
"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/cache"
"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 (
tapMoveThreshold = 4.0 // how far can we move before it is a drag
tapSecondaryDelay = 300 * time.Millisecond // how long before secondary tap
)
// 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 mobileDriver 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
}
// Declare conformity with Driver
var _ fyne.Driver = (*mobileDriver)(nil)
var _ ConfiguredDriver = (*mobileDriver)(nil)
func init() {
runtime.LockOSThread()
}
func (d *mobileDriver) CreateWindow(title string) fyne.Window {
c := NewCanvas().(*mobileCanvas) // silence lint
ret := &window{title: title, canvas: c, isChild: len(d.windows) > 0}
ret.InitEventQueue()
go ret.RunEventQueue()
c.setContent(&canvas.Rectangle{FillColor: theme.BackgroundColor()})
c.SetPainter(pgl.NewPainter(c, ret))
d.windows = append(d.windows, ret)
return ret
}
func (d *mobileDriver) AllWindows() []fyne.Window {
return d.windows
}
// currentWindow returns the most recently opened window - we can only show one at a time.
func (d *mobileDriver) 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 *mobileDriver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle) (size fyne.Size, baseline float32) {
return painter.RenderedTextSize(text, textSize, style)
}
func (d *mobileDriver) 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 *mobileDriver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position {
c := d.CanvasForObject(co)
if c == nil {
return fyne.NewPos(0, 0)
}
mc := c.(*mobileCanvas)
pos := driver.AbsolutePositionForObject(co, mc.ObjectTrees())
inset, _ := c.InteractiveArea()
if mc.windowHead != nil {
if len(mc.windowHead.(*fyne.Container).Objects) > 1 {
topHeight := mc.windowHead.MinSize().Height
pos = pos.Subtract(fyne.NewSize(0, topHeight))
}
}
return pos.Subtract(inset)
}
func (d *mobileDriver) GoBack() {
app.GoBack()
}
func (d *mobileDriver) Quit() {
// Android and iOS guidelines say this should not be allowed!
}
func (d *mobileDriver) Run() {
app.Main(func(a app.App) {
d.app = a
settingsChange := make(chan fyne.Settings)
fyne.CurrentApp().Settings().AddChangeListener(settingsChange)
draw := time.NewTicker(time.Second / 60)
for {
select {
case <-draw.C:
d.sendPaintEvent()
case set := <-settingsChange:
painter.ClearFontCache()
cache.ResetThemeCaches()
intapp.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) {
c, ok := w.Canvas().(*mobileCanvas)
if !ok {
return
}
c.applyThemeOutOfTreeObjects()
})
case e, ok := <-a.Events():
if !ok {
return // events channel closed, app done
}
current := d.currentWindow()
if current == nil {
continue
}
c := current.Canvas().(*mobileCanvas)
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
dev.safeTop = e.InsetTopPx
dev.safeLeft = e.InsetLeftPx
dev.safeHeight = e.HeightPx - e.InsetTopPx - e.InsetBottomPx
dev.safeWidth = e.WidthPx - e.InsetLeftPx - e.InsetRightPx
c.scale = fyne.CurrentDevice().SystemScaleForWindow(nil)
c.Painter().SetFrameBufferScale(1.0)
// 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 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 (d *mobileDriver) handleLifecycle(e lifecycle.Event, w fyne.Window) {
c := w.Canvas().(*mobileCanvas)
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
fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).TriggerEnteredForeground()
case lifecycle.CrossOff: // will enter background
if runtime.GOOS == "darwin" {
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()
}
fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).TriggerExitedForeground()
}
}
func (d *mobileDriver) handlePaint(e paint.Event, w fyne.Window) {
c := w.Canvas().(*mobileCanvas)
d.painting = false
if d.glctx == nil || e.External {
return
}
if !c.inited {
c.inited = true
c.Painter().Init() // we cannot init until the context is set above
}
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 *mobileDriver) onStart() {
fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).TriggerStarted()
}
func (d *mobileDriver) onStop() {
fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).TriggerStopped()
}
func (d *mobileDriver) paintWindow(window fyne.Window, size fyne.Size) {
clips := &internal.ClipStack{}
c := window.Canvas().(*mobileCanvas)
r, g, b, a := theme.BackgroundColor().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 c.debug {
c.DrawDebugOverlay(node.Obj(), pos, size)
}
}
c.WalkTrees(draw, afterDraw)
}
func (d *mobileDriver) sendPaintEvent() {
if d.painting {
return
}
d.app.Send(paint.Event{})
d.painting = true
}
func (d *mobileDriver) 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 *mobileDriver) 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 *mobileDriver) 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) {
w.QueueEvent(func() { wid.Dragged(ev) })
})
}
func (d *mobileDriver) 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) {
w.QueueEvent(func() { wid.Tapped(ev) })
}, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) {
w.QueueEvent(func() { wid.TappedSecondary(ev) })
}, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) {
w.QueueEvent(func() { wid.DoubleTapped(ev) })
}, func(wid fyne.Draggable) {
w.QueueEvent(wid.DragEnd)
})
}
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.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 *mobileDriver) typeDownCanvas(canvas *mobileCanvas, 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 *mobileDriver) typeUpCanvas(_ *mobileCanvas, _ rune, _ key.Code, _ key.Modifiers) {
}
func (d *mobileDriver) Device() fyne.Device {
if d.device == nil {
d.device = &device{}
}
return d.device
}
func (d *mobileDriver) SetOnConfigurationChanged(f func(*Configuration)) {
d.onConfigChanged = f
}
// NewGoMobileDriver sets up a new Driver instance implemented using the Go
// Mobile extension and OpenGL bindings.
func NewGoMobileDriver() fyne.Driver {
d := &mobileDriver{
theme: fyne.ThemeVariant(2), // unspecified
animation: &animation.Runner{},
}
registerRepository(d)
return d
}