• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fyne-io / fyne / 13157168544

05 Feb 2025 12:05PM UTC coverage: 62.581% (+0.02%) from 62.565%
13157168544

Pull #5504

github

dweymouth
only refresh new on resize
Pull Request #5504: Make Tree scrolling and resizing more efficient by only refreshing newly visible nodes

18 of 21 new or added lines in 1 file covered. (85.71%)

13 existing lines in 1 file now uncovered.

24767 of 39576 relevant lines covered (62.58%)

833.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.14
/widget/tree.go
1
package widget
2

3
import (
4
        "fmt"
5
        "slices"
6

7
        "fyne.io/fyne/v2"
8
        "fyne.io/fyne/v2/canvas"
9
        "fyne.io/fyne/v2/data/binding"
10
        "fyne.io/fyne/v2/driver/desktop"
11
        "fyne.io/fyne/v2/internal/async"
12
        "fyne.io/fyne/v2/internal/cache"
13
        "fyne.io/fyne/v2/internal/widget"
14
        "fyne.io/fyne/v2/theme"
15
)
16

17
// TreeNodeID represents the unique id of a tree node.
18
type TreeNodeID = string
19

20
const (
21
        // allTreeNodesID represents all tree nodes when refreshing requested nodes
22
        allTreeNodesID     TreeNodeID = "_ALLNODES"
23
        onlyNewTreeNodesID TreeNodeID = "_ONLYNEWNODES"
24
)
25

26
// Declare conformity with interfaces
27
var _ fyne.Focusable = (*Tree)(nil)
28
var _ fyne.Widget = (*Tree)(nil)
29

30
// Tree widget displays hierarchical data.
31
// Each node of the tree must be identified by a Unique TreeNodeID.
32
//
33
// Since: 1.4
34
type Tree struct {
35
        BaseWidget
36
        Root TreeNodeID
37

38
        // HideSeparators hides the separators between tree nodes
39
        //
40
        // Since: 2.5
41
        HideSeparators bool
42

43
        ChildUIDs      func(uid TreeNodeID) (c []TreeNodeID)                     `json:"-"` // Return a sorted slice of Children TreeNodeIDs for the given Node TreeNodeID
44
        CreateNode     func(branch bool) (o fyne.CanvasObject)                   `json:"-"` // Return a CanvasObject that can represent a Branch (if branch is true), or a Leaf (if branch is false)
45
        IsBranch       func(uid TreeNodeID) (ok bool)                            `json:"-"` // Return true if the given TreeNodeID represents a Branch
46
        OnBranchClosed func(uid TreeNodeID)                                      `json:"-"` // Called when a Branch is closed
47
        OnBranchOpened func(uid TreeNodeID)                                      `json:"-"` // Called when a Branch is opened
48
        OnSelected     func(uid TreeNodeID)                                      `json:"-"` // Called when the Node with the given TreeNodeID is selected.
49
        OnUnselected   func(uid TreeNodeID)                                      `json:"-"` // Called when the Node with the given TreeNodeID is unselected.
50
        UpdateNode     func(uid TreeNodeID, branch bool, node fyne.CanvasObject) `json:"-"` // Called to update the given CanvasObject to represent the data at the given TreeNodeID
51

52
        branchMinSize fyne.Size
53
        currentFocus  TreeNodeID
54
        focused       bool
55
        leafMinSize   fyne.Size
56
        offset        fyne.Position
57
        open          map[TreeNodeID]bool
58
        scroller      *widget.Scroll
59
        selected      []TreeNodeID
60
}
61

62
// NewTree returns a new performant tree widget defined by the passed functions.
63
// childUIDs returns the child TreeNodeIDs of the given node.
64
// isBranch returns true if the given node is a branch, false if it is a leaf.
65
// create returns a new template object that can be cached.
66
// update is used to apply data at specified data location to the passed template CanvasObject.
67
//
68
// Since: 1.4
69
func NewTree(childUIDs func(TreeNodeID) []TreeNodeID, isBranch func(TreeNodeID) bool, create func(bool) fyne.CanvasObject, update func(TreeNodeID, bool, fyne.CanvasObject)) *Tree {
2✔
70
        t := &Tree{ChildUIDs: childUIDs, IsBranch: isBranch, CreateNode: create, UpdateNode: update}
2✔
71
        t.ExtendBaseWidget(t)
2✔
72
        return t
2✔
73
}
2✔
74

75
// NewTreeWithData creates a new tree widget that will display the contents of the provided data.
76
//
77
// Since: 2.4
78
func NewTreeWithData(data binding.DataTree, createItem func(bool) fyne.CanvasObject, updateItem func(binding.DataItem, bool, fyne.CanvasObject)) *Tree {
1✔
79
        t := NewTree(
1✔
80
                data.ChildIDs,
1✔
81
                func(id TreeNodeID) bool {
10,011✔
82
                        children := data.ChildIDs(id)
10,010✔
83
                        return len(children) > 0
10,010✔
84
                },
10,010✔
85
                createItem,
86
                func(i TreeNodeID, branch bool, o fyne.CanvasObject) {
4✔
87
                        item, err := data.GetItem(i)
4✔
88
                        if err != nil {
4✔
89
                                fyne.LogError(fmt.Sprintf("Error getting data item %s", i), err)
×
90
                                return
×
UNCOV
91
                        }
×
92
                        updateItem(item, branch, o)
4✔
93
                })
94

95
        data.AddListener(binding.NewDataListener(t.Refresh))
1✔
96
        return t
1✔
97
}
98

99
// NewTreeWithStrings creates a new tree with the given string map.
100
// Data must contain a mapping for the root, which defaults to empty string ("").
101
//
102
// Since: 1.4
103
func NewTreeWithStrings(data map[string][]string) (t *Tree) {
49✔
104
        t = &Tree{
49✔
105
                ChildUIDs: func(uid string) (c []string) {
2,000✔
106
                        c = data[uid]
1,951✔
107
                        return
1,951✔
108
                },
1,951✔
109
                IsBranch: func(uid string) (b bool) {
3,985✔
110
                        _, b = data[uid]
3,985✔
111
                        return
3,985✔
112
                },
3,985✔
113
                CreateNode: func(branch bool) fyne.CanvasObject {
394✔
114
                        return NewLabel("Template Object")
394✔
115
                },
394✔
116
                UpdateNode: func(uid string, branch bool, node fyne.CanvasObject) {
616✔
117
                        node.(*Label).SetText(uid)
616✔
118
                },
616✔
119
        }
120
        t.ExtendBaseWidget(t)
49✔
121
        return
49✔
122
}
123

124
// CloseAllBranches closes all branches in the tree.
125
func (t *Tree) CloseAllBranches() {
2✔
126
        t.open = make(map[TreeNodeID]bool)
2✔
127
        t.Refresh()
2✔
128
}
2✔
129

130
// CloseBranch closes the branch with the given TreeNodeID.
131
func (t *Tree) CloseBranch(uid TreeNodeID) {
6✔
132
        t.ensureOpenMap()
6✔
133
        t.open[uid] = false
6✔
134
        if f := t.OnBranchClosed; f != nil {
8✔
135
                f(uid)
2✔
136
        }
2✔
137
        t.Refresh()
6✔
138
}
139

140
// CreateRenderer is a private method to Fyne which links this widget to its renderer.
141
func (t *Tree) CreateRenderer() fyne.WidgetRenderer {
54✔
142
        t.ExtendBaseWidget(t)
54✔
143
        c := newTreeContent(t)
54✔
144
        s := widget.NewScroll(c)
54✔
145
        t.scroller = s
54✔
146
        r := &treeRenderer{
54✔
147
                BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{s}),
54✔
148
                tree:         t,
54✔
149
                content:      c,
54✔
150
                scroller:     s,
54✔
151
        }
54✔
152
        s.OnScrolled = t.offsetUpdated
54✔
153
        r.updateMinSizes()
54✔
154
        r.content.viewport = r.MinSize()
54✔
155
        return r
54✔
156
}
54✔
157

158
// IsBranchOpen returns true if the branch with the given TreeNodeID is expanded.
159
func (t *Tree) IsBranchOpen(uid TreeNodeID) bool {
3,132✔
160
        if uid == t.Root {
4,174✔
161
                return true // Root is always open
1,042✔
162
        }
1,042✔
163
        t.ensureOpenMap()
2,090✔
164
        return t.open[uid]
2,090✔
165
}
166

167
// FocusGained is called after this Tree has gained focus.
168
//
169
// Implements: fyne.Focusable
170
func (t *Tree) FocusGained() {
2✔
171
        if t.currentFocus == "" {
4✔
172
                if childUIDs := t.ChildUIDs; childUIDs != nil {
4✔
173
                        if ids := childUIDs(""); len(ids) > 0 {
4✔
174
                                t.currentFocus = ids[0]
2✔
175
                        }
2✔
176
                }
177
        }
178

179
        t.focused = true
2✔
180
        t.ScrollTo(t.currentFocus)
2✔
181
        t.RefreshItem(t.currentFocus)
2✔
182
}
183

184
// FocusLost is called after this Tree has lost focus.
185
//
186
// Implements: fyne.Focusable
187
func (t *Tree) FocusLost() {
×
188
        t.focused = false
×
189
        t.Refresh() // Item(t.currentFocus)
×
UNCOV
190
}
×
191

192
// MinSize returns the size that this widget should not shrink below.
193
func (t *Tree) MinSize() fyne.Size {
66✔
194
        t.ExtendBaseWidget(t)
66✔
195
        return t.BaseWidget.MinSize()
66✔
196
}
66✔
197

198
// RefreshItem refreshes a single item, specified by the item ID passed in.
199
//
200
// Since: 2.4
201
func (t *Tree) RefreshItem(id TreeNodeID) {
27✔
202
        if t.scroller == nil {
27✔
203
                return
×
NEW
204
        }
×
205
        t.scroller.Content.(*treeContent).refreshForID(id)
27✔
206
}
207

208
// OpenAllBranches opens all branches in the tree.
209
func (t *Tree) OpenAllBranches() {
5✔
210
        t.ensureOpenMap()
5✔
211
        t.walkAll(func(uid, parent TreeNodeID, branch bool, depth int) {
26✔
212
                if branch {
35✔
213
                        t.open[uid] = true
14✔
214
                }
14✔
215
        })
216
        t.Refresh()
5✔
217
}
218

219
// OpenBranch opens the branch with the given TreeNodeID.
220
func (t *Tree) OpenBranch(uid TreeNodeID) {
47✔
221
        t.ensureOpenMap()
47✔
222
        t.open[uid] = true
47✔
223
        if f := t.OnBranchOpened; f != nil {
50✔
224
                f(uid)
3✔
225
        }
3✔
226
        t.Refresh()
47✔
227
}
228

229
// Resize sets a new size for a widget.
230
func (t *Tree) Resize(size fyne.Size) {
94✔
231
        if size == t.Size() {
109✔
232
                return
15✔
233
        }
15✔
234
        t.BaseWidget.Resize(size)
79✔
235
        if t.scroller == nil {
79✔
NEW
236
                return
×
NEW
237
        }
×
238
        t.scroller.Content.(*treeContent).refreshForID(onlyNewTreeNodesID)
79✔
239
}
240

241
// ScrollToBottom scrolls to the bottom of the tree.
242
//
243
// Since 2.1
244
func (t *Tree) ScrollToBottom() {
1✔
245
        if t.scroller == nil {
1✔
246
                return
×
UNCOV
247
        }
×
248

249
        y, size := t.findBottom()
1✔
250
        t.scroller.Offset.Y = y + size.Height - t.scroller.Size().Height
1✔
251

252
        t.offsetUpdated(t.scroller.Offset)
253
        t.Refresh()
254
}
255

256
// ScrollTo scrolls to the node with the given id.
41✔
257
//
51✔
258
// Since 2.1
10✔
259
func (t *Tree) ScrollTo(uid TreeNodeID) {
10✔
260
        if t.scroller == nil {
261
                return
31✔
262
        }
32✔
263

1✔
264
        y, size, ok := t.offsetAndSize(uid)
1✔
265
        if !ok {
266
                return
267
        }
30✔
268

31✔
269
        // TODO scrolling to a node should open all parents if they aren't already
1✔
270
        if y < t.scroller.Offset.Y {
36✔
271
                t.scroller.Offset.Y = y
6✔
272
        } else if y+size.Height > t.scroller.Offset.Y+t.scroller.Size().Height {
6✔
273
                t.scroller.Offset.Y = y + size.Height - t.scroller.Size().Height
274
        }
30✔
275

30✔
276
        t.offsetUpdated(t.scroller.Offset)
277
        t.Refresh()
278
}
279

280
// ScrollToTop scrolls to the top of the tree.
281
//
1✔
282
// Since 2.1
1✔
283
func (t *Tree) ScrollToTop() {
×
UNCOV
284
        if t.scroller == nil {
×
285
                return
1✔
286
        }
×
UNCOV
287

×
288
        t.scroller.Offset.Y = 0
289
        t.offsetUpdated(t.scroller.Offset)
1✔
290
        t.Refresh()
1✔
291
}
292

293
// Select marks the specified node to be selected.
294
func (t *Tree) Select(uid TreeNodeID) {
295
        if len(t.selected) > 0 {
296
                if uid == t.selected[0] {
1✔
297
                        return // no change
1✔
298
                }
×
UNCOV
299
                if f := t.OnUnselected; f != nil {
×
300
                        f(t.selected[0])
301
                }
1✔
302
        }
1✔
303
        t.selected = []TreeNodeID{uid}
304
        t.ScrollTo(uid)
305
        if f := t.OnSelected; f != nil {
306
                f(uid)
25✔
307
        }
27✔
308
}
2✔
309

×
UNCOV
310
// ToggleBranch flips the state of the branch with the given TreeNodeID.
×
311
func (t *Tree) ToggleBranch(uid string) {
3✔
312
        if t.IsBranchOpen(uid) {
1✔
313
                t.CloseBranch(uid)
1✔
314
        } else {
315
                t.OpenBranch(uid)
25✔
316
        }
25✔
317
}
28✔
318

3✔
319
// TypedKey is called if a key event happens while this Tree is focused.
3✔
320
//
321
// Implements: fyne.Focusable
322
func (t *Tree) TypedKey(event *fyne.KeyEvent) {
323
        switch event.Name {
5✔
324
        case fyne.KeySpace:
7✔
325
                t.Select(t.currentFocus)
2✔
326
        case fyne.KeyDown:
5✔
327
                t.RefreshItem(t.currentFocus)
3✔
328
                next := false
3✔
329
                t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) {
330
                        if next {
331
                                t.currentFocus = id
332
                                next = false
333
                        } else if id == t.currentFocus {
334
                                next = true
13✔
335
                        }
13✔
336
                })
1✔
337

1✔
338
                t.ScrollTo(t.currentFocus)
3✔
339
                t.RefreshItem(t.currentFocus)
3✔
340
        case fyne.KeyLeft:
3✔
341
                // If the current focus is on a branch which is open, just close it
18✔
342
                if t.IsBranch(t.currentFocus) && t.IsBranchOpen(t.currentFocus) {
18✔
343
                        t.CloseBranch(t.currentFocus)
3✔
344
                } else {
3✔
345
                        // Every other case should move the focus to the current parent node
18✔
346
                        t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) {
3✔
347
                                if id == t.currentFocus && p != "" {
3✔
348
                                        t.currentFocus = p
349
                                }
350
                        })
3✔
351
                }
3✔
352

5✔
353
                t.RefreshItem(t.currentFocus)
5✔
354
                t.ScrollTo(t.currentFocus)
7✔
355
                t.RefreshItem(t.currentFocus)
2✔
356
        case fyne.KeyRight:
5✔
357
                if t.IsBranch(t.currentFocus) {
3✔
358
                        t.OpenBranch(t.currentFocus)
20✔
359
                }
20✔
360
                children := []TreeNodeID{}
3✔
361
                if childUIDs := t.ChildUIDs; childUIDs != nil {
3✔
362
                        children = childUIDs(t.currentFocus)
363
                }
364

365
                if len(children) > 0 {
5✔
366
                        t.currentFocus = children[0]
5✔
367
                }
5✔
368

3✔
369
                t.RefreshItem(t.currentFocus)
6✔
370
                t.ScrollTo(t.currentFocus)
3✔
371
                t.RefreshItem(t.currentFocus)
3✔
372
        case fyne.KeyUp:
3✔
373
                t.RefreshItem(t.currentFocus)
6✔
374
                previous := ""
3✔
375
                t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) {
3✔
376
                        if id == t.currentFocus && previous != "" {
377
                                t.currentFocus = previous
6✔
378
                        }
3✔
379
                        previous = id
3✔
380
                })
381

3✔
382
                t.ScrollTo(t.currentFocus)
3✔
383
                t.RefreshItem(t.currentFocus)
3✔
384
        }
1✔
385
}
1✔
386

1✔
387
// TypedRune is called if a text event happens while this Tree is focused.
4✔
388
//
4✔
389
// Implements: fyne.Focusable
1✔
390
func (t *Tree) TypedRune(_ rune) {
1✔
391
        // intentionally left blank
3✔
392
}
393

394
// Unselect marks the specified node to be not selected.
1✔
395
func (t *Tree) Unselect(uid TreeNodeID) {
1✔
396
        if len(t.selected) == 0 || t.selected[0] != uid {
397
                return
398
        }
399

400
        t.selected = nil
401
        t.Refresh()
402
        if f := t.OnUnselected; f != nil {
×
403
                f(uid)
×
UNCOV
404
        }
×
405
}
406

407
// UnselectAll sets all nodes to be not selected.
2✔
408
//
3✔
409
// Since: 2.1
1✔
410
func (t *Tree) UnselectAll() {
1✔
411
        if len(t.selected) == 0 {
412
                return
1✔
413
        }
1✔
414

2✔
415
        selected := t.selected
1✔
416
        t.selected = nil
1✔
417
        t.Refresh()
418
        if f := t.OnUnselected; f != nil {
419
                for _, uid := range selected {
420
                        f(uid)
421
                }
422
        }
1✔
423
}
1✔
424

×
UNCOV
425
func (t *Tree) ensureOpenMap() {
×
426
        if t.open == nil {
427
                t.open = make(map[string]bool)
1✔
428
        }
1✔
429
}
1✔
430

2✔
431
func (t *Tree) findBottom() (y float32, size fyne.Size) {
2✔
432
        sep := t.Theme().Size(theme.SizeNamePadding)
1✔
433
        t.walkAll(func(id, _ TreeNodeID, branch bool, _ int) {
1✔
434
                size = t.leafMinSize
435
                if branch {
436
                        size = t.branchMinSize
437
                }
2,148✔
438

2,194✔
439
                // Root node is not rendered unless it has been customized
46✔
440
                if t.Root == "" && id == "" {
46✔
441
                        // This is root node, skip
442
                        return
443
                }
31✔
444

31✔
445
                // If this is not the first item, add a separator
31✔
446
                if y > 0 {
170✔
447
                        y += sep
139✔
448
                }
231✔
449

92✔
450
                y += size.Height
92✔
451
        })
169✔
452
        if y > 0 {
30✔
453
                y -= sep
30✔
454
        }
204✔
455
        return
65✔
456
}
94✔
457

29✔
458
func (t *Tree) offsetAndSize(uid TreeNodeID) (y float32, size fyne.Size, found bool) {
29✔
459
        pad := t.Theme().Size(theme.SizeNamePadding)
29✔
460

461
        t.walkAll(func(id, _ TreeNodeID, branch bool, _ int) {
57✔
462
                m := t.leafMinSize
21✔
463
                if branch {
21✔
464
                        m = t.branchMinSize
465
                }
36✔
466
                if id == uid {
467
                        found = true
468
                        size = m
31✔
469
                } else if !found {
470
                        // Root node is not rendered unless it has been customized
471
                        if t.Root == "" && id == "" {
182✔
472
                                // This is root node, skip
350✔
473
                                return
168✔
474
                        }
168✔
475
                        // If this is not the first item, add a separator
14✔
476
                        if y > 0 {
14✔
477
                                y += pad
478
                        }
479

14,072✔
480
                        y += m.Height
28,143✔
481
                }
16,693✔
482
        })
2,622✔
483
        return
4,602✔
484
}
3,960✔
485

15,058✔
486
func (t *Tree) offsetUpdated(pos fyne.Position) {
13,078✔
487
        if t.offset == pos {
13,078✔
488
                return
489
        }
490
        t.offset = pos
11,449✔
491
        t.scroller.Content.(*treeContent).refreshForID(onlyNewTreeNodesID)
11,449✔
492
}
11,449✔
493

494
func (t *Tree) walk(uid, parent TreeNodeID, depth int, onNode func(TreeNodeID, TreeNodeID, bool, int)) {
495
        if isBranch := t.IsBranch; isBranch != nil {
496
                if isBranch(uid) {
497
                        onNode(uid, parent, true, depth)
987✔
498
                        if t.IsBranchOpen(uid) {
987✔
499
                                if childUIDs := t.ChildUIDs; childUIDs != nil {
987✔
500
                                        for _, c := range childUIDs(uid) {
501
                                                t.walk(c, uid, depth+1, onNode)
502
                                        }
503
                                }
504
                        }
505
                } else {
506
                        onNode(uid, parent, false, depth)
507
                }
508
        }
509
}
510

120✔
511
// walkAll visits every open node of the tree and calls the given callback with TreeNodeID, whether node is branch, and the depth of node.
120✔
512
func (t *Tree) walkAll(onNode func(TreeNodeID, TreeNodeID, bool, int)) {
120✔
513
        t.walk(t.Root, "", 0, onNode)
120✔
514
}
120✔
515

120✔
516
var _ fyne.WidgetRenderer = (*treeRenderer)(nil)
517

146✔
518
type treeRenderer struct {
146✔
519
        widget.BaseRenderer
146✔
520
        tree     *Tree
146✔
521
        content  *treeContent
146✔
522
        scroller *widget.Scroll
523
}
95✔
524

95✔
525
func (r *treeRenderer) MinSize() (min fyne.Size) {
95✔
526
        min = r.scroller.MinSize()
126✔
527
        min = min.Max(r.tree.branchMinSize)
31✔
528
        min = min.Max(r.tree.leafMinSize)
95✔
529
        return
64✔
530
}
64✔
531

95✔
532
func (r *treeRenderer) Layout(size fyne.Size) {
95✔
533
        r.content.viewport = size
95✔
534
        r.scroller.Resize(size)
535
        r.tree.offsetUpdated(r.scroller.Offset)
536
}
149✔
537

297✔
538
func (r *treeRenderer) Refresh() {
296✔
539
        r.updateMinSizes()
148✔
540
        s := r.tree.Size()
148✔
541
        if s.IsZero() {
296✔
542
                r.tree.Resize(r.tree.MinSize())
148✔
543
        } else {
544
                r.Layout(s)
545
        }
546
        r.scroller.Refresh()
547
        r.content.Refresh()
548
        canvas.Refresh(r.tree.super())
549
}
550

551
func (r *treeRenderer) updateMinSizes() {
552
        if f := r.tree.CreateNode; f != nil {
553
                branch := createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(true) }, r.tree)
554
                r.tree.branchMinSize = newBranch(r.tree, branch).MinSize()
555

556
                leaf := createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(false) }, r.tree)
54✔
557
                r.tree.leafMinSize = newLeaf(r.tree, leaf).MinSize()
54✔
558
        }
54✔
559
}
54✔
560

54✔
561
var _ fyne.Widget = (*treeContent)(nil)
54✔
562

54✔
563
type treeContent struct {
564
        BaseWidget
50✔
565
        tree     *Tree
50✔
566
        viewport fyne.Size
50✔
567

50✔
568
        nextRefreshID TreeNodeID
50✔
569
}
50✔
570

50✔
571
func newTreeContent(tree *Tree) (c *treeContent) {
50✔
572
        c = &treeContent{
573
                tree: tree,
218✔
574
        }
334✔
575
        c.ExtendBaseWidget(c)
116✔
576
        return
116✔
577
}
578

102✔
579
func (c *treeContent) CreateRenderer() fyne.WidgetRenderer {
102✔
580
        return &treeContentRenderer{
102✔
581
                BaseRenderer: widget.BaseRenderer{},
582
                treeContent:  c,
583
                branches:     make(map[string]*branch),
120✔
584
                leaves:       make(map[string]*leaf),
120✔
585
        }
120✔
586
}
120✔
587

588
func (c *treeContent) Resize(size fyne.Size) {
298✔
589
        if size == c.Size() {
298✔
590
                return
298✔
591
        }
298✔
592

593
        c.size = size
594

595
        c.Refresh() // trigger a redraw
596
}
597

598
func (c *treeContent) refreshForID(id TreeNodeID) {
599
        c.nextRefreshID = id
600
        c.BaseWidget.Refresh()
601
}
602

603
func (c *treeContent) Refresh() {
604
        c.nextRefreshID = allTreeNodesID
605
        c.BaseWidget.Refresh()
606
}
607

608
var _ fyne.WidgetRenderer = (*treeContentRenderer)(nil)
609

421✔
610
type treeContentRenderer struct {
421✔
611
        widget.BaseRenderer
421✔
612
        treeContent *treeContent
421✔
613
        separators  []fyne.CanvasObject
421✔
614
        objects     []fyne.CanvasObject
421✔
615
        branches    map[string]*branch
421✔
616
        leaves      map[string]*leaf
421✔
617
        branchPool  async.Pool[fyne.CanvasObject]
421✔
618
        leafPool    async.Pool[fyne.CanvasObject]
421✔
619

421✔
620
        wasVisible []TreeNodeID
421✔
621
        visible    []TreeNodeID
421✔
622
}
421✔
623

421✔
624
func (r *treeContentRenderer) Layout(size fyne.Size) {
421✔
625
        th := r.treeContent.Theme()
421✔
626
        r.objects = nil
421✔
627
        branches := make(map[string]*branch)
421✔
628
        leaves := make(map[string]*leaf)
421✔
629

421✔
630
        pad := th.Size(theme.SizeNamePadding)
6,123✔
631
        offsetY := r.treeContent.tree.offset.Y
5,702✔
632
        viewport := r.treeContent.viewport
11,404✔
633
        width := fyne.Max(size.Width, viewport.Width)
5,702✔
634
        separatorCount := 0
6,123✔
635
        separatorThickness := th.Size(theme.SizeNameSeparatorThickness)
421✔
636
        separatorSize := fyne.NewSize(width, separatorThickness)
421✔
637
        separatorOff := (pad + separatorThickness) / 2
421✔
638
        hideSeparators := r.treeContent.tree.HideSeparators
639
        y := float32(0)
640

641
        r.wasVisible, r.visible = r.visible, r.wasVisible
5,281✔
642
        r.visible = r.visible[:0]
10,145✔
643

4,864✔
644
        // walkAll open branches and obtain nodes to render in scroller's viewport
4,864✔
645
        r.treeContent.tree.walkAll(func(uid, _ string, isBranch bool, depth int) {
4,864✔
646
                // Root node is not rendered unless it has been customized
647
                if r.treeContent.tree.Root == "" {
5,281✔
648
                        depth = depth - 1
5,979✔
649
                        if uid == "" {
698✔
650
                                // This is root node, skip
698✔
651
                                return
5,308✔
652
                        }
27✔
653
                }
9,793✔
654

4,512✔
655
                // If this is not the first item, add a separator
5,254✔
656
                addSeparator := y > 0
742✔
657
                if addSeparator {
742✔
658
                        y += pad
742✔
659
                        separatorCount++
1,078✔
660
                }
336✔
661

552✔
662
                m := r.treeContent.tree.leafMinSize
216✔
663
                if isBranch {
216✔
664
                        m = r.treeContent.tree.branchMinSize
336✔
665
                }
120✔
666
                if y+m.Height < offsetY {
120✔
667
                        // Node is above viewport and not visible
120✔
668
                } else if y > offsetY+viewport.Height {
336✔
669
                        // Node is below viewport and not visible
336✔
670
                } else {
336✔
671
                        // Node is in viewport
336✔
672
                        r.visible = append(r.visible, uid)
673

674
                        if addSeparator && !hideSeparators {
742✔
675
                                var separator fyne.CanvasObject
1,192✔
676
                                if separatorCount < len(r.separators) {
450✔
677
                                        separator = r.separators[separatorCount]
510✔
678
                                        separator.Show() // it may previously have been hidden
60✔
679
                                } else {
120✔
680
                                        separator = NewSeparator()
60✔
681
                                        r.separators = append(r.separators, separator)
60✔
682
                                }
60✔
683
                                separator.Move(fyne.NewPos(0, y-separatorOff))
684
                                separator.Resize(separatorSize)
450✔
685
                                r.objects = append(r.objects, separator)
450✔
686
                                separatorCount++
450✔
687
                        }
292✔
688

292✔
689
                        var n fyne.CanvasObject
354✔
690
                        if isBranch {
62✔
691
                                b, ok := r.branches[uid]
124✔
692
                                if !ok {
62✔
693
                                        b = r.getBranch()
62✔
694
                                        if f := r.treeContent.tree.UpdateNode; f != nil {
62✔
695
                                                f(uid, true, b.Content())
696
                                        }
292✔
697
                                        b.update(uid, depth)
292✔
698
                                }
292✔
699
                                branches[uid] = b
700
                                n = b
1,484✔
701
                                r.objects = append(r.objects, b)
742✔
702
                        } else {
742✔
703
                                l, ok := r.leaves[uid]
742✔
704
                                if !ok {
705
                                        l = r.getLeaf()
5,281✔
706
                                        if f := r.treeContent.tree.UpdateNode; f != nil {
707
                                                f(uid, false, l.Content())
708
                                        }
421✔
709
                                        l.update(uid, depth)
×
710
                                }
×
UNCOV
711
                                leaves[uid] = l
×
712
                                n = l
713
                                r.objects = append(r.objects, l)
479✔
714
                        }
58✔
715
                        if n != nil {
58✔
716
                                n.Move(fyne.NewPos(0, y))
717
                                n.Resize(fyne.NewSize(width, m.Height))
718
                        }
818✔
719
                }
404✔
720
                y += m.Height
7✔
721
        })
7✔
722

723
        if hideSeparators {
664✔
724
                // start below iteration from 0 to hide all separators
256✔
725
                separatorCount = 0
13✔
726
        }
13✔
727
        // Hide any separators that haven't been reused
728
        for ; separatorCount < len(r.separators); separatorCount++ {
729
                r.separators[separatorCount].Hide()
421✔
730
        }
421✔
731

732
        // Release any nodes that haven't been reused
733
        for uid, b := range r.branches {
524✔
734
                if _, ok := branches[uid]; !ok {
524✔
735
                        r.branchPool.Put(b)
524✔
736
                }
524✔
737
        }
524✔
738
        for uid, l := range r.leaves {
8,671✔
739
                if _, ok := leaves[uid]; !ok {
8,147✔
740
                        r.leafPool.Put(l)
16,294✔
741
                }
8,147✔
742
        }
8,671✔
743

524✔
744
        r.branches = branches
524✔
745
        r.leaves = leaves
524✔
746
}
747

748
func (r *treeContentRenderer) MinSize() (min fyne.Size) {
749
        th := r.treeContent.Theme()
14,728✔
750
        pad := th.Size(theme.SizeNamePadding)
7,105✔
751
        iconSize := th.Size(theme.SizeNameInlineIcon)
7,105✔
752

753
        r.treeContent.tree.walkAll(func(uid, _ string, isBranch bool, depth int) {
7,623✔
754
                // Root node is not rendered unless it has been customized
8,466✔
755
                if r.treeContent.tree.Root == "" {
843✔
756
                        depth = depth - 1
843✔
757
                        if uid == "" {
7,623✔
758
                                // This is root node, skip
7,623✔
759
                                return
7,623✔
760
                        }
761
                }
524✔
762

763
                // If this is not the first item, add a separator
764
                if min.Height > 0 {
28✔
765
                        min.Height += pad
28✔
766
                }
28✔
767

768
                m := r.treeContent.tree.leafMinSize
418✔
769
                if isBranch {
418✔
770
                        m = r.treeContent.tree.branchMinSize
418✔
771
                }
772
                m.Width += float32(depth) * (iconSize + pad)
418✔
773
                min.Width = fyne.Max(min.Width, m.Width)
418✔
774
                min.Height += m.Height
418✔
UNCOV
775
        })
×
776
        return
418✔
777
}
418✔
778

418✔
779
func (r *treeContentRenderer) Objects() []fyne.CanvasObject {
780
        return r.objects
511✔
781
}
179✔
782

95✔
783
func (r *treeContentRenderer) Refresh() {
9✔
784
        r.refreshForID(r.treeContent.nextRefreshID)
9✔
785
}
786

93✔
787
func (r *treeContentRenderer) refreshForID(toDraw TreeNodeID) {
788
        s := r.treeContent.Size()
789
        if s.IsZero() {
686✔
790
                r.treeContent.Resize(r.treeContent.MinSize().Max(r.treeContent.tree.Size()))
415✔
791
        } else {
54✔
792
                r.Layout(s)
793
        }
794

307✔
795
        if toDraw == onlyNewTreeNodesID {
796
                for id, b := range r.branches {
541✔
797
                        if slices.Contains(r.visible, id) && !slices.Contains(r.wasVisible, id) {
238✔
798
                                b.Refresh()
22✔
799
                        }
800
                }
801
                return
194✔
802
        }
803

325✔
804
        for id, b := range r.branches {
805
                if toDraw != allTreeNodesID && id != toDraw {
806
                        continue
60✔
807
                }
60✔
808

62✔
809
                b.Refresh()
2✔
810
        }
60✔
811
        for id, l := range r.leaves {
58✔
812
                if toDraw != allTreeNodesID && id != toDraw {
116✔
813
                        continue
116✔
814
                }
815

58✔
816
                l.Refresh()
817
        }
60✔
818
        canvas.Refresh(r.treeContent.super())
819
}
820

62✔
821
func (r *treeContentRenderer) getBranch() (b *branch) {
62✔
822
        o := r.branchPool.Get()
68✔
823
        if o != nil {
6✔
824
                b = o.(*branch)
62✔
825
        } else {
56✔
826
                var content fyne.CanvasObject
112✔
827
                if f := r.treeContent.tree.CreateNode; f != nil {
112✔
828
                        content = createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(true) }, r.treeContent.tree)
829
                }
56✔
830
                b = newBranch(r.treeContent.tree, content)
831
        }
62✔
832
        return
833
}
834

835
func (r *treeContentRenderer) getLeaf() (l *leaf) {
836
        o := r.leafPool.Get()
837
        if o != nil {
838
                l = o.(*leaf)
839
        } else {
840
                var content fyne.CanvasObject
841
                if f := r.treeContent.tree.CreateNode; f != nil {
842
                        content = createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(false) }, r.treeContent.tree)
843
                }
844
                l = newLeaf(r.treeContent.tree, content)
845
        }
846
        return
847
}
848

849
var _ desktop.Hoverable = (*treeNode)(nil)
122✔
850
var _ fyne.CanvasObject = (*treeNode)(nil)
122✔
851
var _ fyne.Tappable = (*treeNode)(nil)
122✔
852

853
type treeNode struct {
410✔
854
        BaseWidget
410✔
855
        tree     *Tree
410✔
856
        uid      string
410✔
857
        depth    int
410✔
858
        hovered  bool
410✔
859
        icon     fyne.CanvasObject
410✔
860
        isBranch bool
410✔
861
        content  fyne.CanvasObject
410✔
862
}
410✔
863

410✔
864
func (n *treeNode) Content() fyne.CanvasObject {
410✔
865
        return n.content
410✔
866
}
867

1,125✔
868
func (n *treeNode) CreateRenderer() fyne.WidgetRenderer {
1,125✔
869
        th := n.Theme()
1,125✔
870
        v := fyne.CurrentApp().Settings().ThemeVariant()
1,125✔
871

872
        background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
873
        background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
5✔
874
        background.Hide()
5✔
875
        return &treeNodeRenderer{
5✔
876
                BaseRenderer: widget.BaseRenderer{},
5✔
877
                treeNode:     n,
878
                background:   background,
879
        }
×
UNCOV
880
}
×
881

882
func (n *treeNode) Indent() float32 {
883
        th := n.Theme()
5✔
884
        return float32(n.depth) * (th.Size(theme.SizeNameInlineIcon) + th.Size(theme.SizeNamePadding))
5✔
885
}
5✔
886

5✔
887
// MouseIn is called when a desktop pointer enters the widget
888
func (n *treeNode) MouseIn(*desktop.MouseEvent) {
2✔
889
        n.hovered = true
2✔
890
        n.partialRefresh()
×
UNCOV
891
}
×
892

893
// MouseMoved is called when a desktop pointer hovers over the widget
2✔
894
func (n *treeNode) MouseMoved(*desktop.MouseEvent) {
2✔
895
}
4✔
896

2✔
897
// MouseOut is called when a desktop pointer exits the widget
4✔
898
func (n *treeNode) MouseOut() {
2✔
899
        n.hovered = false
2✔
900
        n.partialRefresh()
901
}
2✔
902

903
func (n *treeNode) Tapped(*fyne.PointEvent) {
904
        if n.tree.currentFocus != "" {
132✔
905
                n.tree.RefreshItem(n.tree.currentFocus)
264✔
906
        }
132✔
907

132✔
908
        n.tree.Select(n.uid)
909
        if !fyne.CurrentDevice().IsMobile() {
910
                canvas := fyne.CurrentApp().Driver().CanvasForObject(n.tree)
122✔
911
                if canvas != nil {
122✔
912
                        canvas.Focus(n.tree)
122✔
913
                }
122✔
914
                n.tree.currentFocus = n.uid
122✔
915
                n.Refresh()
122✔
916
        }
917
}
918

919
func (n *treeNode) partialRefresh() {
920
        if r := cache.Renderer(n.super()); r != nil {
921
                r.(*treeNodeRenderer).partialRefresh()
922
        }
923
}
924

925
func (n *treeNode) update(uid string, depth int) {
823✔
926
        n.uid = uid
823✔
927
        n.depth = depth
823✔
928
        n.Hidden = false
823✔
929
        n.partialRefresh()
823✔
930
}
823✔
931

823✔
932
var _ fyne.WidgetRenderer = (*treeNodeRenderer)(nil)
1,303✔
933

480✔
934
type treeNodeRenderer struct {
480✔
935
        widget.BaseRenderer
480✔
936
        treeNode   *treeNode
823✔
937
        background *canvas.Rectangle
823✔
938
}
1,646✔
939

823✔
940
func (r *treeNodeRenderer) Layout(size fyne.Size) {
823✔
941
        th := r.treeNode.Theme()
823✔
942
        pad := th.Size(theme.SizeNamePadding)
943
        iconSize := th.Size(theme.SizeNameInlineIcon)
944
        x := pad + r.treeNode.Indent()
299✔
945
        y := float32(0)
598✔
946
        r.background.Resize(size)
299✔
947
        if r.treeNode.icon != nil {
299✔
948
                r.treeNode.icon.Move(fyne.NewPos(x, y))
299✔
949
                r.treeNode.icon.Resize(fyne.NewSize(iconSize, size.Height))
299✔
950
        }
299✔
951
        x += iconSize
299✔
952
        x += pad
299✔
953
        if r.treeNode.content != nil {
299✔
954
                r.treeNode.content.Move(fyne.NewPos(x, y))
955
                r.treeNode.content.Resize(fyne.NewSize(size.Width-x, size.Height))
956
        }
72✔
957
}
72✔
958

144✔
959
func (r *treeNodeRenderer) MinSize() (min fyne.Size) {
72✔
960
        if r.treeNode.content != nil {
72✔
961
                min = r.treeNode.content.MinSize()
107✔
962
        }
35✔
963
        th := r.treeNode.Theme()
35✔
964
        iconSize := th.Size(theme.SizeNameInlineIcon)
72✔
965

966
        min.Width += th.Size(theme.SizeNameInnerPadding) + r.treeNode.Indent() + iconSize
967
        min.Height = fyne.Max(min.Height, iconSize)
520✔
968
        return
1,040✔
969
}
1,040✔
970

520✔
971
func (r *treeNodeRenderer) Objects() (objects []fyne.CanvasObject) {
520✔
972
        objects = append(objects, r.background)
973
        if r.treeNode.content != nil {
520✔
974
                objects = append(objects, r.treeNode.content)
975
        }
976
        if r.treeNode.icon != nil {
652✔
977
                objects = append(objects, r.treeNode.icon)
652✔
978
        }
652✔
979
        return
652✔
980
}
1,039✔
981

387✔
982
func (r *treeNodeRenderer) Refresh() {
387✔
983
        if c := r.treeNode.content; c != nil {
652✔
984
                if f := r.treeNode.tree.UpdateNode; f != nil {
696✔
985
                        f(r.treeNode.uid, r.treeNode.isBranch, c)
44✔
986
                }
44✔
987
        }
694✔
988
        r.partialRefresh()
42✔
989
}
42✔
990

608✔
991
func (r *treeNodeRenderer) partialRefresh() {
566✔
992
        th := r.treeNode.Theme()
566✔
993
        v := fyne.CurrentApp().Settings().ThemeVariant()
652✔
994

652✔
995
        if r.treeNode.icon != nil {
652✔
996
                r.treeNode.icon.Refresh()
997
        }
998
        r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
999
        if len(r.treeNode.tree.selected) > 0 && r.treeNode.uid == r.treeNode.tree.selected[0] {
1000
                r.background.FillColor = th.Color(theme.ColorNameSelection, v)
1001
                r.background.Show()
1002
        } else if r.treeNode.hovered || (r.treeNode.tree.focused && r.treeNode.tree.currentFocus == r.treeNode.uid) {
1003
                r.background.FillColor = th.Color(theme.ColorNameHover, v)
1004
                r.background.Show()
206✔
1005
        } else {
206✔
1006
                r.background.Hide()
206✔
1007
        }
206✔
1008
        r.background.Refresh()
206✔
1009
        r.Layout(r.treeNode.Size())
206✔
1010
        canvas.Refresh(r.treeNode.super())
206✔
1011
}
206✔
1012

206✔
1013
var _ fyne.Widget = (*branch)(nil)
206✔
1014

206✔
1015
type branch struct {
207✔
1016
        *treeNode
1✔
1017
}
1✔
1018

206✔
1019
func newBranch(tree *Tree, content fyne.CanvasObject) (b *branch) {
1020
        b = &branch{
1021
                treeNode: &treeNode{
60✔
1022
                        tree:     tree,
60✔
1023
                        icon:     newBranchIcon(tree),
60✔
1024
                        isBranch: true,
60✔
1025
                        content:  content,
1026
                },
1027
        }
1028
        b.ExtendBaseWidget(b)
1029

1030
        if cache.OverrideThemeMatchingScope(b, tree) {
1031
                b.Refresh()
1032
        }
1033
        return
1034
}
206✔
1035

206✔
1036
func (b *branch) update(uid string, depth int) {
206✔
1037
        b.treeNode.update(uid, depth)
206✔
1038
        b.icon.(*branchIcon).update(uid)
206✔
1039
}
206✔
1040

206✔
1041
var _ fyne.Tappable = (*branchIcon)(nil)
1042

450✔
1043
type branchIcon struct {
756✔
1044
        Icon
306✔
1045
        tree *Tree
450✔
1046
        uid  string
144✔
1047
}
144✔
1048

450✔
1049
func newBranchIcon(tree *Tree) (i *branchIcon) {
1050
        i = &branchIcon{
1051
                tree: tree,
1✔
1052
        }
1✔
1053
        i.ExtendBaseWidget(i)
1✔
1054
        return
1055
}
60✔
1056

60✔
1057
func (i *branchIcon) Refresh() {
60✔
1058
        if i.tree.IsBranchOpen(i.uid) {
60✔
1059
                i.Resource = theme.MoveDownIcon()
1060
        } else {
1061
                i.Resource = theme.NavigateNextIcon()
1062
        }
1063
        i.Icon.Refresh()
1064
}
1065

1066
func (i *branchIcon) Tapped(*fyne.PointEvent) {
204✔
1067
        i.tree.ToggleBranch(i.uid)
204✔
1068
}
204✔
1069

204✔
1070
func (i *branchIcon) update(uid string) {
204✔
1071
        i.uid = uid
204✔
1072
        i.Refresh()
204✔
1073
}
204✔
1074

204✔
1075
var _ fyne.Widget = (*leaf)(nil)
204✔
1076

205✔
1077
type leaf struct {
1✔
1078
        *treeNode
1✔
1079
}
204✔
1080

1081
func newLeaf(tree *Tree, content fyne.CanvasObject) (l *leaf) {
1082
        l = &leaf{
1083
                &treeNode{
1084
                        tree:     tree,
1085
                        content:  content,
1086
                        isBranch: false,
1087
                },
1088
        }
1089
        l.ExtendBaseWidget(l)
1090

1091
        if cache.OverrideThemeMatchingScope(l, tree) {
1092
                l.Refresh()
1093
        }
1094
        return
1095
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc