adam-gui/vendor/fyne.io/fyne/v2/internal/driver/common/canvas.go

611 lines
15 KiB
Go
Raw Normal View History

2024-04-29 19:13:50 +02:00
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()
}
}