611 lines
15 KiB
Go
611 lines
15 KiB
Go
package common
|
||
|
||
import (
|
||
"image/color"
|
||
"reflect"
|
||
"sync"
|
||
"sync/atomic"
|
||
|
||
"fyne.io/fyne/v2"
|
||
"fyne.io/fyne/v2/canvas"
|
||
"fyne.io/fyne/v2/internal"
|
||
"fyne.io/fyne/v2/internal/app"
|
||
"fyne.io/fyne/v2/internal/async"
|
||
"fyne.io/fyne/v2/internal/cache"
|
||
"fyne.io/fyne/v2/internal/driver"
|
||
"fyne.io/fyne/v2/internal/painter/gl"
|
||
)
|
||
|
||
// SizeableCanvas defines a canvas with size related functions.
|
||
type SizeableCanvas interface {
|
||
fyne.Canvas
|
||
Resize(fyne.Size)
|
||
MinSize() fyne.Size
|
||
}
|
||
|
||
// Canvas defines common canvas implementation.
|
||
type Canvas struct {
|
||
sync.RWMutex
|
||
|
||
OnFocus func(obj fyne.Focusable)
|
||
OnUnfocus func()
|
||
|
||
impl SizeableCanvas
|
||
|
||
contentFocusMgr *app.FocusManager
|
||
menuFocusMgr *app.FocusManager
|
||
overlays *overlayStack
|
||
|
||
shortcut fyne.ShortcutHandler
|
||
|
||
painter gl.Painter
|
||
|
||
// Any object that requestes to enter to the refresh queue should
|
||
// not be omitted as it is always a rendering task's decision
|
||
// for skipping frames or drawing calls.
|
||
//
|
||
// If an object failed to ender the refresh queue, the object may
|
||
// disappear or blink from the view at any frames. As of this reason,
|
||
// the refreshQueue is an unbounded queue which is bale to cache
|
||
// arbitrary number of fyne.CanvasObject for the rendering.
|
||
refreshQueue *async.CanvasObjectQueue
|
||
dirty uint32 // atomic
|
||
|
||
mWindowHeadTree, contentTree, menuTree *renderCacheTree
|
||
}
|
||
|
||
// AddShortcut adds a shortcut to the canvas.
|
||
func (c *Canvas) AddShortcut(shortcut fyne.Shortcut, handler func(shortcut fyne.Shortcut)) {
|
||
c.shortcut.AddShortcut(shortcut, handler)
|
||
}
|
||
|
||
func (c *Canvas) DrawDebugOverlay(obj fyne.CanvasObject, pos fyne.Position, size fyne.Size) {
|
||
switch obj.(type) {
|
||
case fyne.Widget:
|
||
r := canvas.NewRectangle(color.Transparent)
|
||
r.StrokeColor = color.NRGBA{R: 0xcc, G: 0x33, B: 0x33, A: 0xff}
|
||
r.StrokeWidth = 1
|
||
r.Resize(obj.Size())
|
||
c.Painter().Paint(r, pos, size)
|
||
|
||
t := canvas.NewText(reflect.ValueOf(obj).Elem().Type().Name(), r.StrokeColor)
|
||
t.TextSize = 10
|
||
c.Painter().Paint(t, pos.AddXY(2, 2), size)
|
||
case *fyne.Container:
|
||
r := canvas.NewRectangle(color.Transparent)
|
||
r.StrokeColor = color.NRGBA{R: 0x33, G: 0x33, B: 0xcc, A: 0xff}
|
||
r.StrokeWidth = 1
|
||
r.Resize(obj.Size())
|
||
c.Painter().Paint(r, pos, size)
|
||
}
|
||
}
|
||
|
||
// EnsureMinSize ensure canvas min size.
|
||
//
|
||
// This function uses lock.
|
||
func (c *Canvas) EnsureMinSize() bool {
|
||
if c.impl.Content() == nil {
|
||
return false
|
||
}
|
||
windowNeedsMinSizeUpdate := false
|
||
csize := c.impl.Size()
|
||
min := c.impl.MinSize()
|
||
|
||
c.RLock()
|
||
defer c.RUnlock()
|
||
|
||
var parentNeedingUpdate *RenderCacheNode
|
||
|
||
ensureMinSize := func(node *RenderCacheNode, pos fyne.Position) {
|
||
obj := node.obj
|
||
cache.SetCanvasForObject(obj, c.impl, func() {
|
||
if img, ok := obj.(*canvas.Image); ok {
|
||
c.RUnlock()
|
||
img.Refresh() // this may now have a different texScale
|
||
c.RLock()
|
||
}
|
||
})
|
||
|
||
if parentNeedingUpdate == node {
|
||
c.updateLayout(obj)
|
||
parentNeedingUpdate = nil
|
||
}
|
||
|
||
c.RUnlock()
|
||
if !obj.Visible() {
|
||
c.RLock()
|
||
return
|
||
}
|
||
minSize := obj.MinSize()
|
||
c.RLock()
|
||
|
||
minSizeChanged := node.minSize != minSize
|
||
if minSizeChanged {
|
||
node.minSize = minSize
|
||
if node.parent != nil {
|
||
parentNeedingUpdate = node.parent
|
||
} else {
|
||
windowNeedsMinSizeUpdate = true
|
||
c.RUnlock()
|
||
size := obj.Size()
|
||
c.RLock()
|
||
expectedSize := minSize.Max(size)
|
||
if expectedSize != size && size != csize {
|
||
c.RUnlock()
|
||
obj.Resize(expectedSize)
|
||
c.RLock()
|
||
} else {
|
||
c.updateLayout(obj)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
c.WalkTrees(nil, ensureMinSize)
|
||
|
||
shouldResize := windowNeedsMinSizeUpdate && (csize.Width < min.Width || csize.Height < min.Height)
|
||
if shouldResize {
|
||
c.RUnlock()
|
||
c.impl.Resize(csize.Max(min))
|
||
c.RLock()
|
||
}
|
||
return windowNeedsMinSizeUpdate
|
||
}
|
||
|
||
// Focus makes the provided item focused.
|
||
func (c *Canvas) Focus(obj fyne.Focusable) {
|
||
focusMgr := c.focusManager()
|
||
if focusMgr != nil && focusMgr.Focus(obj) { // fast path – probably >99.9% of all cases
|
||
if c.OnFocus != nil {
|
||
c.OnFocus(obj)
|
||
}
|
||
return
|
||
}
|
||
|
||
c.RLock()
|
||
focusMgrs := append([]*app.FocusManager{c.contentFocusMgr, c.menuFocusMgr}, c.overlays.ListFocusManagers()...)
|
||
c.RUnlock()
|
||
|
||
for _, mgr := range focusMgrs {
|
||
if mgr == nil {
|
||
continue
|
||
}
|
||
if focusMgr != mgr {
|
||
if mgr.Focus(obj) {
|
||
if c.OnFocus != nil {
|
||
c.OnFocus(obj)
|
||
}
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
fyne.LogError("Failed to focus object which is not part of the canvas’ content, menu or overlays.", nil)
|
||
}
|
||
|
||
// Focused returns the current focused object.
|
||
func (c *Canvas) Focused() fyne.Focusable {
|
||
mgr := c.focusManager()
|
||
if mgr == nil {
|
||
return nil
|
||
}
|
||
return mgr.Focused()
|
||
}
|
||
|
||
// FocusGained signals to the manager that its content got focus.
|
||
// Valid only on Desktop.
|
||
func (c *Canvas) FocusGained() {
|
||
mgr := c.focusManager()
|
||
if mgr == nil {
|
||
return
|
||
}
|
||
mgr.FocusGained()
|
||
}
|
||
|
||
// FocusLost signals to the manager that its content lost focus.
|
||
// Valid only on Desktop.
|
||
func (c *Canvas) FocusLost() {
|
||
mgr := c.focusManager()
|
||
if mgr == nil {
|
||
return
|
||
}
|
||
mgr.FocusLost()
|
||
}
|
||
|
||
// FocusNext focuses the next focusable item.
|
||
func (c *Canvas) FocusNext() {
|
||
mgr := c.focusManager()
|
||
if mgr == nil {
|
||
return
|
||
}
|
||
mgr.FocusNext()
|
||
}
|
||
|
||
// FocusPrevious focuses the previous focusable item.
|
||
func (c *Canvas) FocusPrevious() {
|
||
mgr := c.focusManager()
|
||
if mgr == nil {
|
||
return
|
||
}
|
||
mgr.FocusPrevious()
|
||
}
|
||
|
||
// FreeDirtyTextures frees dirty textures and returns the number of freed textures.
|
||
func (c *Canvas) FreeDirtyTextures() (freed uint64) {
|
||
freeObject := func(object fyne.CanvasObject) {
|
||
freeWalked := func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
|
||
// No image refresh while recursing to avoid double texture upload.
|
||
if _, ok := obj.(*canvas.Image); ok {
|
||
return false
|
||
}
|
||
if c.painter != nil {
|
||
c.painter.Free(obj)
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Image.Refresh will trigger a refresh specific to the object, while recursing on parent widget would just lead to
|
||
// a double texture upload.
|
||
if img, ok := object.(*canvas.Image); ok {
|
||
if c.painter != nil {
|
||
c.painter.Free(img)
|
||
}
|
||
} else {
|
||
driver.WalkCompleteObjectTree(object, freeWalked, nil)
|
||
}
|
||
}
|
||
|
||
// Within a frame, refresh tasks are requested from the Refresh method,
|
||
// and we desire to clear out all requested operations within a frame.
|
||
// See https://github.com/fyne-io/fyne/issues/2548.
|
||
tasksToDo := c.refreshQueue.Len()
|
||
|
||
shouldFilterDuplicates := (tasksToDo > 200) // filtering has overhead, not worth enabling for few tasks
|
||
var refreshSet map[fyne.CanvasObject]struct{}
|
||
if shouldFilterDuplicates {
|
||
refreshSet = make(map[fyne.CanvasObject]struct{})
|
||
}
|
||
|
||
for c.refreshQueue.Len() > 0 {
|
||
object := c.refreshQueue.Out()
|
||
if !shouldFilterDuplicates {
|
||
freed++
|
||
freeObject(object)
|
||
} else {
|
||
refreshSet[object] = struct{}{}
|
||
tasksToDo--
|
||
if tasksToDo == 0 {
|
||
shouldFilterDuplicates = false // stop collecting messages to avoid starvation
|
||
for object := range refreshSet {
|
||
freed++
|
||
freeObject(object)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if c.painter != nil {
|
||
cache.RangeExpiredTexturesFor(c.impl, c.painter.Free)
|
||
}
|
||
return
|
||
}
|
||
|
||
// Initialize initializes the canvas.
|
||
func (c *Canvas) Initialize(impl SizeableCanvas, onOverlayChanged func()) {
|
||
c.impl = impl
|
||
c.refreshQueue = async.NewCanvasObjectQueue()
|
||
c.overlays = &overlayStack{
|
||
OverlayStack: internal.OverlayStack{
|
||
OnChange: onOverlayChanged,
|
||
Canvas: impl,
|
||
},
|
||
}
|
||
}
|
||
|
||
// ObjectTrees return canvas object trees.
|
||
//
|
||
// This function uses lock.
|
||
func (c *Canvas) ObjectTrees() []fyne.CanvasObject {
|
||
c.RLock()
|
||
var content, menu fyne.CanvasObject
|
||
if c.contentTree != nil && c.contentTree.root != nil {
|
||
content = c.contentTree.root.obj
|
||
}
|
||
if c.menuTree != nil && c.menuTree.root != nil {
|
||
menu = c.menuTree.root.obj
|
||
}
|
||
c.RUnlock()
|
||
trees := make([]fyne.CanvasObject, 0, len(c.Overlays().List())+2)
|
||
trees = append(trees, content)
|
||
if menu != nil {
|
||
trees = append(trees, menu)
|
||
}
|
||
trees = append(trees, c.Overlays().List()...)
|
||
return trees
|
||
}
|
||
|
||
// Overlays returns the overlay stack.
|
||
func (c *Canvas) Overlays() fyne.OverlayStack {
|
||
// we don't need to lock here, because overlays never changes
|
||
return c.overlays
|
||
}
|
||
|
||
// Painter returns the canvas painter.
|
||
func (c *Canvas) Painter() gl.Painter {
|
||
return c.painter
|
||
}
|
||
|
||
// Refresh refreshes a canvas object.
|
||
func (c *Canvas) Refresh(obj fyne.CanvasObject) {
|
||
walkNeeded := false
|
||
switch obj.(type) {
|
||
case *fyne.Container:
|
||
walkNeeded = true
|
||
case fyne.Widget:
|
||
walkNeeded = true
|
||
}
|
||
|
||
if walkNeeded {
|
||
driver.WalkCompleteObjectTree(obj, func(co fyne.CanvasObject, p1, p2 fyne.Position, s fyne.Size) bool {
|
||
if i, ok := co.(*canvas.Image); ok {
|
||
i.Refresh()
|
||
}
|
||
return false
|
||
}, nil)
|
||
}
|
||
|
||
c.refreshQueue.In(obj)
|
||
c.SetDirty()
|
||
}
|
||
|
||
// RemoveShortcut removes a shortcut from the canvas.
|
||
func (c *Canvas) RemoveShortcut(shortcut fyne.Shortcut) {
|
||
c.shortcut.RemoveShortcut(shortcut)
|
||
}
|
||
|
||
// SetContentTreeAndFocusMgr sets content tree and focus manager.
|
||
//
|
||
// This function does not use the canvas lock.
|
||
func (c *Canvas) SetContentTreeAndFocusMgr(content fyne.CanvasObject) {
|
||
c.contentTree = &renderCacheTree{root: &RenderCacheNode{obj: content}}
|
||
var focused fyne.Focusable
|
||
if c.contentFocusMgr != nil {
|
||
focused = c.contentFocusMgr.Focused() // keep old focus if possible
|
||
}
|
||
c.contentFocusMgr = app.NewFocusManager(content)
|
||
if focused != nil {
|
||
c.contentFocusMgr.Focus(focused)
|
||
}
|
||
}
|
||
|
||
// CheckDirtyAndClear returns true if the canvas is dirty and
|
||
// clears the dirty state atomically.
|
||
func (c *Canvas) CheckDirtyAndClear() bool {
|
||
return atomic.SwapUint32(&c.dirty, 0) != 0
|
||
}
|
||
|
||
// SetDirty sets canvas dirty flag atomically.
|
||
func (c *Canvas) SetDirty() {
|
||
atomic.AddUint32(&c.dirty, 1)
|
||
}
|
||
|
||
// SetMenuTreeAndFocusMgr sets menu tree and focus manager.
|
||
//
|
||
// This function does not use the canvas lock.
|
||
func (c *Canvas) SetMenuTreeAndFocusMgr(menu fyne.CanvasObject) {
|
||
c.menuTree = &renderCacheTree{root: &RenderCacheNode{obj: menu}}
|
||
if menu != nil {
|
||
c.menuFocusMgr = app.NewFocusManager(menu)
|
||
} else {
|
||
c.menuFocusMgr = nil
|
||
}
|
||
}
|
||
|
||
// SetMobileWindowHeadTree sets window head tree.
|
||
//
|
||
// This function does not use the canvas lock.
|
||
func (c *Canvas) SetMobileWindowHeadTree(head fyne.CanvasObject) {
|
||
c.mWindowHeadTree = &renderCacheTree{root: &RenderCacheNode{obj: head}}
|
||
}
|
||
|
||
// SetPainter sets the canvas painter.
|
||
func (c *Canvas) SetPainter(p gl.Painter) {
|
||
c.painter = p
|
||
}
|
||
|
||
// TypedShortcut handle the registered shortcut.
|
||
func (c *Canvas) TypedShortcut(shortcut fyne.Shortcut) {
|
||
c.shortcut.TypedShortcut(shortcut)
|
||
}
|
||
|
||
// Unfocus unfocuses all the objects in the canvas.
|
||
func (c *Canvas) Unfocus() {
|
||
mgr := c.focusManager()
|
||
if mgr == nil {
|
||
return
|
||
}
|
||
if mgr.Focus(nil) && c.OnUnfocus != nil {
|
||
c.OnUnfocus()
|
||
}
|
||
}
|
||
|
||
// WalkTrees walks over the trees.
|
||
func (c *Canvas) WalkTrees(
|
||
beforeChildren func(*RenderCacheNode, fyne.Position),
|
||
afterChildren func(*RenderCacheNode, fyne.Position),
|
||
) {
|
||
c.walkTree(c.contentTree, beforeChildren, afterChildren)
|
||
if c.mWindowHeadTree != nil && c.mWindowHeadTree.root.obj != nil {
|
||
c.walkTree(c.mWindowHeadTree, beforeChildren, afterChildren)
|
||
}
|
||
if c.menuTree != nil && c.menuTree.root.obj != nil {
|
||
c.walkTree(c.menuTree, beforeChildren, afterChildren)
|
||
}
|
||
for _, tree := range c.overlays.renderCaches {
|
||
if tree != nil {
|
||
c.walkTree(tree, beforeChildren, afterChildren)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (c *Canvas) focusManager() *app.FocusManager {
|
||
if focusMgr := c.overlays.TopFocusManager(); focusMgr != nil {
|
||
return focusMgr
|
||
}
|
||
c.RLock()
|
||
defer c.RUnlock()
|
||
if c.isMenuActive() {
|
||
return c.menuFocusMgr
|
||
}
|
||
return c.contentFocusMgr
|
||
}
|
||
|
||
func (c *Canvas) isMenuActive() bool {
|
||
if c.menuTree == nil || c.menuTree.root == nil || c.menuTree.root.obj == nil {
|
||
return false
|
||
}
|
||
menu := c.menuTree.root.obj
|
||
if am, ok := menu.(activatableMenu); ok {
|
||
return am.IsActive()
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (c *Canvas) walkTree(
|
||
tree *renderCacheTree,
|
||
beforeChildren func(*RenderCacheNode, fyne.Position),
|
||
afterChildren func(*RenderCacheNode, fyne.Position),
|
||
) {
|
||
tree.Lock()
|
||
defer tree.Unlock()
|
||
var node, parent, prev *RenderCacheNode
|
||
node = tree.root
|
||
|
||
bc := func(obj fyne.CanvasObject, pos fyne.Position, _ fyne.Position, _ fyne.Size) bool {
|
||
if node != nil && node.obj != obj {
|
||
if parent.firstChild == node {
|
||
parent.firstChild = nil
|
||
}
|
||
node = nil
|
||
}
|
||
if node == nil {
|
||
node = &RenderCacheNode{parent: parent, obj: obj}
|
||
if parent.firstChild == nil {
|
||
parent.firstChild = node
|
||
} else {
|
||
prev.nextSibling = node
|
||
}
|
||
}
|
||
if prev != nil && prev.parent != parent {
|
||
prev = nil
|
||
}
|
||
|
||
if beforeChildren != nil {
|
||
beforeChildren(node, pos)
|
||
}
|
||
|
||
parent = node
|
||
node = parent.firstChild
|
||
return false
|
||
}
|
||
ac := func(obj fyne.CanvasObject, pos fyne.Position, _ fyne.CanvasObject) {
|
||
node = parent
|
||
parent = node.parent
|
||
if prev != nil && prev.parent != parent {
|
||
prev.nextSibling = nil
|
||
}
|
||
|
||
if afterChildren != nil {
|
||
afterChildren(node, pos)
|
||
}
|
||
|
||
prev = node
|
||
node = node.nextSibling
|
||
}
|
||
driver.WalkVisibleObjectTree(tree.root.obj, bc, ac)
|
||
}
|
||
|
||
// RenderCacheNode represents a node in a render cache tree.
|
||
type RenderCacheNode struct {
|
||
// structural data
|
||
firstChild *RenderCacheNode
|
||
nextSibling *RenderCacheNode
|
||
obj fyne.CanvasObject
|
||
parent *RenderCacheNode
|
||
// cache data
|
||
minSize fyne.Size
|
||
// painterData is some data from the painter associated with the drawed node
|
||
// it may for instance point to a GL texture
|
||
// it should free all associated resources when released
|
||
// i.e. it should not simply be a texture reference integer
|
||
painterData interface{}
|
||
}
|
||
|
||
// Obj returns the node object.
|
||
func (r *RenderCacheNode) Obj() fyne.CanvasObject {
|
||
return r.obj
|
||
}
|
||
|
||
type activatableMenu interface {
|
||
IsActive() bool
|
||
}
|
||
|
||
type overlayStack struct {
|
||
internal.OverlayStack
|
||
|
||
propertyLock sync.RWMutex
|
||
renderCaches []*renderCacheTree
|
||
}
|
||
|
||
func (o *overlayStack) Add(overlay fyne.CanvasObject) {
|
||
if overlay == nil {
|
||
return
|
||
}
|
||
o.propertyLock.Lock()
|
||
defer o.propertyLock.Unlock()
|
||
o.add(overlay)
|
||
}
|
||
|
||
func (o *overlayStack) Remove(overlay fyne.CanvasObject) {
|
||
if overlay == nil || len(o.List()) == 0 {
|
||
return
|
||
}
|
||
o.propertyLock.Lock()
|
||
defer o.propertyLock.Unlock()
|
||
o.remove(overlay)
|
||
}
|
||
|
||
func (o *overlayStack) add(overlay fyne.CanvasObject) {
|
||
o.renderCaches = append(o.renderCaches, &renderCacheTree{root: &RenderCacheNode{obj: overlay}})
|
||
o.OverlayStack.Add(overlay)
|
||
}
|
||
|
||
func (o *overlayStack) remove(overlay fyne.CanvasObject) {
|
||
o.OverlayStack.Remove(overlay)
|
||
overlayCount := len(o.List())
|
||
o.renderCaches[overlayCount] = nil // release memory reference to removed element
|
||
o.renderCaches = o.renderCaches[:overlayCount]
|
||
}
|
||
|
||
type renderCacheTree struct {
|
||
sync.RWMutex
|
||
root *RenderCacheNode
|
||
}
|
||
|
||
func (c *Canvas) updateLayout(objToLayout fyne.CanvasObject) {
|
||
switch cont := objToLayout.(type) {
|
||
case *fyne.Container:
|
||
if cont.Layout != nil {
|
||
layout := cont.Layout
|
||
objects := cont.Objects
|
||
c.RUnlock()
|
||
layout.Layout(objects, cont.Size())
|
||
c.RLock()
|
||
}
|
||
case fyne.Widget:
|
||
renderer := cache.Renderer(cont)
|
||
c.RUnlock()
|
||
renderer.Layout(cont.Size())
|
||
c.RLock()
|
||
}
|
||
}
|