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

611 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}