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

fyne-io / fyne / 18807176232

25 Oct 2025 06:34PM UTC coverage: 61.057% (-0.004%) from 61.061%
18807176232

Pull #5989

github

Jacalz
Fix TODO regarding comparable map key
Pull Request #5989: RFC: Proof of concept for upgrading Go to 1.24

155 of 188 new or added lines in 62 files covered. (82.45%)

27 existing lines in 6 files now uncovered.

25609 of 41943 relevant lines covered (61.06%)

692.99 hits per line

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

86.4
/internal/widget/scroller.go
1
package widget
2

3
import (
4
        "fyne.io/fyne/v2"
5
        "fyne.io/fyne/v2/canvas"
6
        "fyne.io/fyne/v2/driver/desktop"
7
        "fyne.io/fyne/v2/internal/cache"
8
        "fyne.io/fyne/v2/theme"
9
)
10

11
// ScrollDirection represents the directions in which a Scroll can scroll its child content.
12
type ScrollDirection = fyne.ScrollDirection
13

14
// Constants for valid values of ScrollDirection.
15
const (
16
        // ScrollBoth supports horizontal and vertical scrolling.
17
        ScrollBoth ScrollDirection = iota
18
        // ScrollHorizontalOnly specifies the scrolling should only happen left to right.
19
        ScrollHorizontalOnly
20
        // ScrollVerticalOnly specifies the scrolling should only happen top to bottom.
21
        ScrollVerticalOnly
22
        // ScrollNone turns off scrolling for this container.
23
        //
24
        // Since: 2.0
25
        ScrollNone
26
)
27

28
type scrollBarOrientation int
29

30
// We default to vertical as 0 due to that being the original orientation offered
31
const (
32
        scrollBarOrientationVertical   scrollBarOrientation = 0
33
        scrollBarOrientationHorizontal scrollBarOrientation = 1
34
        scrollContainerMinSize                              = float32(32) // TODO consider the smallest useful scroll view?
35

36
        // what fraction of the page to scroll when tapping on the scroll bar area
37
        pageScrollFraction = float32(0.95)
38
)
39

40
type scrollBarRenderer struct {
41
        BaseRenderer
42
        scrollBar  *scrollBar
43
        background *canvas.Rectangle
44
        minSize    fyne.Size
45
}
46

47
func (r *scrollBarRenderer) Layout(size fyne.Size) {
208✔
48
        r.background.Resize(size)
208✔
49
}
208✔
50

51
func (r *scrollBarRenderer) MinSize() fyne.Size {
×
52
        return r.minSize
×
53
}
×
54

55
func (r *scrollBarRenderer) Refresh() {
331✔
56
        th := theme.CurrentForWidget(r.scrollBar)
331✔
57
        v := fyne.CurrentApp().Settings().ThemeVariant()
331✔
58

331✔
59
        r.background.FillColor = th.Color(theme.ColorNameScrollBar, v)
331✔
60
        r.background.CornerRadius = th.Size(theme.SizeNameScrollBarRadius)
331✔
61
        r.background.Refresh()
331✔
62
}
331✔
63

64
var (
65
        _ desktop.Hoverable = (*scrollBar)(nil)
66
        _ fyne.Draggable    = (*scrollBar)(nil)
67
)
68

69
type scrollBar struct {
70
        Base
71
        area            *scrollBarArea
72
        draggedDistance float32
73
        dragStart       float32
74
        orientation     scrollBarOrientation
75
}
76

77
func (b *scrollBar) CreateRenderer() fyne.WidgetRenderer {
94✔
78
        th := theme.CurrentForWidget(b)
94✔
79
        v := fyne.CurrentApp().Settings().ThemeVariant()
94✔
80

94✔
81
        background := canvas.NewRectangle(th.Color(theme.ColorNameScrollBar, v))
94✔
82
        background.CornerRadius = th.Size(theme.SizeNameScrollBarRadius)
94✔
83
        r := &scrollBarRenderer{
94✔
84
                scrollBar:  b,
94✔
85
                background: background,
94✔
86
        }
94✔
87
        r.SetObjects([]fyne.CanvasObject{background})
94✔
88
        return r
94✔
89
}
94✔
90

91
func (b *scrollBar) Cursor() desktop.Cursor {
×
92
        return desktop.DefaultCursor
×
93
}
×
94

95
func (b *scrollBar) DragEnd() {
1✔
96
        b.area.isDragging = false
1✔
97

1✔
98
        if fyne.CurrentDevice().IsMobile() {
1✔
99
                b.area.MouseOut()
×
100
                return
×
101
        }
×
102
        b.area.Refresh()
1✔
103
}
104

105
func (b *scrollBar) Dragged(e *fyne.DragEvent) {
25✔
106
        if !b.area.isDragging {
36✔
107
                b.area.isDragging = true
11✔
108
                b.area.MouseIn(nil)
11✔
109

11✔
110
                switch b.orientation {
11✔
111
                case scrollBarOrientationHorizontal:
6✔
112
                        b.dragStart = b.Position().X
6✔
113
                case scrollBarOrientationVertical:
5✔
114
                        b.dragStart = b.Position().Y
5✔
115
                }
116
                b.draggedDistance = 0
11✔
117
        }
118

119
        switch b.orientation {
25✔
120
        case scrollBarOrientationHorizontal:
13✔
121
                b.draggedDistance += e.Dragged.DX
13✔
122
        case scrollBarOrientationVertical:
12✔
123
                b.draggedDistance += e.Dragged.DY
12✔
124
        }
125
        b.area.moveBar(b.draggedDistance+b.dragStart, b.Size())
25✔
126
}
127

128
func (b *scrollBar) MouseIn(e *desktop.MouseEvent) {
3✔
129
        b.area.MouseIn(e)
3✔
130
}
3✔
131

132
func (b *scrollBar) MouseMoved(*desktop.MouseEvent) {
×
133
}
×
134

135
func (b *scrollBar) MouseOut() {
5✔
136
        b.area.MouseOut()
5✔
137
}
5✔
138

139
func newScrollBar(area *scrollBarArea) *scrollBar {
94✔
140
        b := &scrollBar{area: area, orientation: area.orientation}
94✔
141
        b.ExtendBaseWidget(b)
94✔
142
        return b
94✔
143
}
94✔
144

145
func (a *scrollBarArea) isLarge() bool {
1,671✔
146
        return a.isMouseIn || a.isDragging
1,671✔
147
}
1,671✔
148

149
type scrollBarAreaRenderer struct {
150
        BaseRenderer
151
        area       *scrollBarArea
152
        bar        *scrollBar
153
        background *canvas.Rectangle
154
}
155

156
func (r *scrollBarAreaRenderer) Layout(size fyne.Size) {
569✔
157
        r.layoutWithTheme(theme.CurrentForWidget(r.area), size)
569✔
158
}
569✔
159

160
func (r *scrollBarAreaRenderer) layoutWithTheme(th fyne.Theme, size fyne.Size) {
894✔
161
        var barHeight, barWidth, barX, barY float32
894✔
162
        var bkgHeight, bkgWidth, bkgX, bkgY float32
894✔
163
        switch r.area.orientation {
894✔
164
        case scrollBarOrientationHorizontal:
451✔
165
                barWidth, barHeight, barX, barY = r.barSizeAndOffset(th, r.area.scroll.Offset.X, r.area.scroll.Content.Size().Width, r.area.scroll.Size().Width)
451✔
166
                r.area.barLeadingEdge = barX
451✔
167
                r.area.barTrailingEdge = barX + barWidth
451✔
168
                bkgWidth, bkgHeight, bkgX, bkgY = size.Width, barHeight, 0, barY
451✔
169
        default:
443✔
170
                barHeight, barWidth, barY, barX = r.barSizeAndOffset(th, r.area.scroll.Offset.Y, r.area.scroll.Content.Size().Height, r.area.scroll.Size().Height)
443✔
171
                r.area.barLeadingEdge = barY
443✔
172
                r.area.barTrailingEdge = barY + barHeight
443✔
173
                bkgWidth, bkgHeight, bkgX, bkgY = barWidth, size.Height, barX, 0
443✔
174
        }
175
        r.bar.Move(fyne.NewPos(barX, barY))
894✔
176
        r.bar.Resize(fyne.NewSize(barWidth, barHeight))
894✔
177
        r.background.Move(fyne.NewPos(bkgX, bkgY))
894✔
178
        r.background.Resize(fyne.NewSize(bkgWidth, bkgHeight))
894✔
179
}
180

181
func (r *scrollBarAreaRenderer) MinSize() fyne.Size {
352✔
182
        th := theme.CurrentForWidget(r.area)
352✔
183

352✔
184
        barSize := th.Size(theme.SizeNameScrollBar)
352✔
185
        min := barSize
352✔
186
        if !r.area.isLarge() {
638✔
187
                min = th.Size(theme.SizeNameScrollBarSmall) * 2
286✔
188
        }
286✔
189
        switch r.area.orientation {
352✔
190
        case scrollBarOrientationHorizontal:
176✔
191
                return fyne.NewSize(barSize, min)
176✔
192
        default:
176✔
193
                return fyne.NewSize(min, barSize)
176✔
194
        }
195
}
196

197
func (r *scrollBarAreaRenderer) Refresh() {
325✔
198
        th := theme.CurrentForWidget(r.area)
325✔
199
        r.bar.Refresh()
325✔
200
        r.background.FillColor = th.Color(theme.ColorNameScrollBarBackground, fyne.CurrentApp().Settings().ThemeVariant())
325✔
201
        r.background.Hidden = !r.area.isLarge()
325✔
202
        r.layoutWithTheme(th, r.area.Size())
325✔
203
        canvas.Refresh(r.bar)
325✔
204
        canvas.Refresh(r.background)
325✔
205
}
325✔
206

207
func (r *scrollBarAreaRenderer) barSizeAndOffset(th fyne.Theme, contentOffset, contentLength, scrollLength float32) (length, width, lengthOffset, widthOffset float32) {
894✔
208
        scrollBarSize := th.Size(theme.SizeNameScrollBar)
894✔
209
        if scrollLength < contentLength {
1,590✔
210
                portion := scrollLength / contentLength
696✔
211
                length = float32(int(scrollLength)) * portion
696✔
212
                length = max(length, scrollBarSize)
696✔
213
        } else {
894✔
214
                length = scrollLength
198✔
215
        }
198✔
216
        if contentOffset != 0 {
1,050✔
217
                lengthOffset = (scrollLength - length) * (contentOffset / (contentLength - scrollLength))
156✔
218
        }
156✔
219
        if r.area.isLarge() {
1,042✔
220
                width = scrollBarSize
148✔
221
        } else {
894✔
222
                widthOffset = th.Size(theme.SizeNameScrollBarSmall)
746✔
223
                width = widthOffset
746✔
224
        }
746✔
225
        return length, width, lengthOffset, widthOffset
894✔
226
}
227

228
var (
229
        _ desktop.Hoverable = (*scrollBarArea)(nil)
230
        _ fyne.Tappable     = (*scrollBarArea)(nil)
231
)
232

233
type scrollBarArea struct {
234
        Base
235

236
        isDragging  bool
237
        isMouseIn   bool
238
        scroll      *Scroll
239
        bar         *scrollBar
240
        orientation scrollBarOrientation
241

242
        // updated from renderer Layout
243
        // coordinates Y in vertical orientation, X in horizontal
244
        barLeadingEdge  float32
245
        barTrailingEdge float32
246
}
247

248
func (a *scrollBarArea) CreateRenderer() fyne.WidgetRenderer {
94✔
249
        th := theme.CurrentForWidget(a)
94✔
250
        v := fyne.CurrentApp().Settings().ThemeVariant()
94✔
251
        a.bar = newScrollBar(a)
94✔
252
        background := canvas.NewRectangle(th.Color(theme.ColorNameScrollBarBackground, v))
94✔
253
        background.Hidden = !a.isLarge()
94✔
254
        return &scrollBarAreaRenderer{BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{background, a.bar}), area: a, bar: a.bar, background: background}
94✔
255
}
94✔
256

257
func (a *scrollBarArea) Tapped(e *fyne.PointEvent) {
6✔
258
        if isScrollerPageOnTap() {
6✔
259
                a.scrollFullPageOnTap(e)
×
260
                return
×
261
        }
×
262

263
        // scroll to tapped position
264
        barSize := a.bar.Size()
6✔
265
        switch a.orientation {
6✔
266
        case scrollBarOrientationHorizontal:
3✔
267
                if e.Position.X < a.barLeadingEdge || e.Position.X > a.barTrailingEdge {
5✔
268
                        a.moveBar(max(0, e.Position.X-barSize.Width/2), barSize)
2✔
269
                }
2✔
270
        case scrollBarOrientationVertical:
3✔
271
                if e.Position.Y < a.barLeadingEdge || e.Position.Y > a.barTrailingEdge {
5✔
272
                        a.moveBar(max(0, e.Position.Y-barSize.Height/2), a.bar.Size())
2✔
273
                }
2✔
274
        }
275
}
276

277
func (a *scrollBarArea) scrollFullPageOnTap(e *fyne.PointEvent) {
×
278
        // when tapping above/below or left/right of the bar, scroll the content
×
279
        // nearly a full page (pageScrollFraction) up/down or left/right, respectively
×
280
        newOffset := a.scroll.Offset
×
281
        switch a.orientation {
×
282
        case scrollBarOrientationHorizontal:
×
283
                if e.Position.X < a.barLeadingEdge {
×
NEW
284
                        newOffset.X = max(0, newOffset.X-a.scroll.Size().Width*pageScrollFraction)
×
285
                } else if e.Position.X > a.barTrailingEdge {
×
286
                        viewWid := a.scroll.Size().Width
×
NEW
287
                        newOffset.X = min(a.scroll.Content.Size().Width-viewWid, newOffset.X+viewWid*pageScrollFraction)
×
288
                }
×
289
        default:
×
290
                if e.Position.Y < a.barLeadingEdge {
×
NEW
291
                        newOffset.Y = max(0, newOffset.Y-a.scroll.Size().Height*pageScrollFraction)
×
292
                } else if e.Position.Y > a.barTrailingEdge {
×
293
                        viewHt := a.scroll.Size().Height
×
NEW
294
                        newOffset.Y = min(a.scroll.Content.Size().Height-viewHt, newOffset.Y+viewHt*pageScrollFraction)
×
295
                }
×
296
        }
297
        if newOffset == a.scroll.Offset {
×
298
                return
×
299
        }
×
300

301
        a.scroll.Offset = newOffset
×
302
        if f := a.scroll.OnScrolled; f != nil {
×
303
                f(a.scroll.Offset)
×
304
        }
×
305
        a.scroll.refreshWithoutOffsetUpdate()
×
306
}
307

308
func (a *scrollBarArea) MouseIn(*desktop.MouseEvent) {
16✔
309
        a.isMouseIn = true
16✔
310
        a.scroll.refreshBars()
16✔
311
}
16✔
312

313
func (a *scrollBarArea) MouseMoved(*desktop.MouseEvent) {
×
314
}
×
315

316
func (a *scrollBarArea) MouseOut() {
5✔
317
        a.isMouseIn = false
5✔
318
        if a.isDragging {
6✔
319
                return
1✔
320
        }
1✔
321

322
        a.scroll.refreshBars()
4✔
323
}
324

325
func (a *scrollBarArea) moveBar(offset float32, barSize fyne.Size) {
29✔
326
        oldX := a.scroll.Offset.X
29✔
327
        oldY := a.scroll.Offset.Y
29✔
328
        switch a.orientation {
29✔
329
        case scrollBarOrientationHorizontal:
15✔
330
                a.scroll.Offset.X = a.computeScrollOffset(barSize.Width, offset, a.scroll.Size().Width, a.scroll.Content.Size().Width)
15✔
331
        default:
14✔
332
                a.scroll.Offset.Y = a.computeScrollOffset(barSize.Height, offset, a.scroll.Size().Height, a.scroll.Content.Size().Height)
14✔
333
        }
334
        if f := a.scroll.OnScrolled; f != nil && (a.scroll.Offset.X != oldX || a.scroll.Offset.Y != oldY) {
29✔
335
                f(a.scroll.Offset)
×
336
        }
×
337
        a.scroll.refreshWithoutOffsetUpdate()
29✔
338
}
339

340
func (a *scrollBarArea) computeScrollOffset(length, offset, scrollLength, contentLength float32) float32 {
29✔
341
        maxOffset := scrollLength - length
29✔
342
        if offset < 0 {
33✔
343
                offset = 0
4✔
344
        } else if offset > maxOffset {
35✔
345
                offset = maxOffset
6✔
346
        }
6✔
347
        ratio := offset / maxOffset
29✔
348
        scrollOffset := ratio * (contentLength - scrollLength)
29✔
349
        return scrollOffset
29✔
350
}
351

352
func newScrollBarArea(scroll *Scroll, orientation scrollBarOrientation) *scrollBarArea {
100✔
353
        a := &scrollBarArea{scroll: scroll, orientation: orientation}
100✔
354
        a.ExtendBaseWidget(a)
100✔
355
        return a
100✔
356
}
100✔
357

358
type scrollContainerRenderer struct {
359
        BaseRenderer
360
        scroll                  *Scroll
361
        vertArea                *scrollBarArea
362
        horizArea               *scrollBarArea
363
        leftShadow, rightShadow *Shadow
364
        topShadow, bottomShadow *Shadow
365
        oldMinSize              fyne.Size
366
}
367

368
func (r *scrollContainerRenderer) layoutBars(size fyne.Size) {
180✔
369
        scrollerSize := r.scroll.Size()
180✔
370
        if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth {
356✔
371
                r.vertArea.Resize(fyne.NewSize(r.vertArea.MinSize().Width, size.Height))
176✔
372
                r.vertArea.Move(fyne.NewPos(scrollerSize.Width-r.vertArea.Size().Width, 0))
176✔
373
                r.topShadow.Resize(fyne.NewSize(size.Width, 0))
176✔
374
                r.bottomShadow.Resize(fyne.NewSize(size.Width, 0))
176✔
375
                r.bottomShadow.Move(fyne.NewPos(0, scrollerSize.Height))
176✔
376
        }
176✔
377

378
        if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth {
356✔
379
                r.horizArea.Resize(fyne.NewSize(size.Width, r.horizArea.MinSize().Height))
176✔
380
                r.horizArea.Move(fyne.NewPos(0, scrollerSize.Height-r.horizArea.Size().Height))
176✔
381
                r.leftShadow.Resize(fyne.NewSize(0, size.Height))
176✔
382
                r.rightShadow.Resize(fyne.NewSize(0, size.Height))
176✔
383
                r.rightShadow.Move(fyne.NewPos(scrollerSize.Width, 0))
176✔
384
        }
176✔
385

386
        r.updatePosition()
180✔
387
}
388

389
func (r *scrollContainerRenderer) Layout(size fyne.Size) {
96✔
390
        c := r.scroll.Content
96✔
391
        c.Resize(c.MinSize().Max(size))
96✔
392

96✔
393
        r.layoutBars(size)
96✔
394
}
96✔
395

396
func (r *scrollContainerRenderer) MinSize() fyne.Size {
6✔
397
        return r.scroll.MinSize()
6✔
398
}
6✔
399

400
func (r *scrollContainerRenderer) Refresh() {
128✔
401
        r.horizArea.Refresh()
128✔
402
        r.vertArea.Refresh()
128✔
403
        r.leftShadow.Refresh()
128✔
404
        r.topShadow.Refresh()
128✔
405
        r.rightShadow.Refresh()
128✔
406
        r.bottomShadow.Refresh()
128✔
407

128✔
408
        if len(r.BaseRenderer.Objects()) == 0 || r.BaseRenderer.Objects()[0] != r.scroll.Content {
129✔
409
                // push updated content object to baseRenderer
1✔
410
                r.BaseRenderer.Objects()[0] = r.scroll.Content
1✔
411
        }
1✔
412
        size := r.scroll.Size()
128✔
413
        newMin := r.scroll.Content.MinSize()
128✔
414
        if r.oldMinSize == newMin && r.oldMinSize == r.scroll.Content.Size() &&
128✔
415
                (size.Width <= r.oldMinSize.Width && size.Height <= r.oldMinSize.Height) {
212✔
416
                r.layoutBars(size)
84✔
417
                return
84✔
418
        }
84✔
419

420
        r.oldMinSize = newMin
44✔
421
        r.Layout(size)
44✔
422
}
423

424
func (r *scrollContainerRenderer) handleAreaVisibility(contentSize, scrollSize float32, area *scrollBarArea) {
442✔
425
        if contentSize <= scrollSize {
584✔
426
                area.Hide()
142✔
427
        } else if r.scroll.Visible() {
742✔
428
                area.Show()
300✔
429
        }
300✔
430
}
431

432
func (r *scrollContainerRenderer) handleShadowVisibility(offset, contentSize, scrollSize float32, shadowStart fyne.CanvasObject, shadowEnd fyne.CanvasObject) {
442✔
433
        if !r.scroll.Visible() {
442✔
434
                return
×
435
        }
×
436
        if offset > 0 {
518✔
437
                shadowStart.Show()
76✔
438
        } else {
442✔
439
                shadowStart.Hide()
366✔
440
        }
366✔
441
        if offset < contentSize-scrollSize {
713✔
442
                shadowEnd.Show()
271✔
443
        } else {
442✔
444
                shadowEnd.Hide()
171✔
445
        }
171✔
446
}
447

448
func (r *scrollContainerRenderer) updatePosition() {
230✔
449
        if r.scroll.Content == nil {
230✔
450
                return
×
451
        }
×
452
        scrollSize := r.scroll.Size()
230✔
453
        contentSize := r.scroll.Content.Size()
230✔
454

230✔
455
        r.scroll.Content.Move(fyne.NewPos(-r.scroll.Offset.X, -r.scroll.Offset.Y))
230✔
456

230✔
457
        if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth {
451✔
458
                r.handleAreaVisibility(contentSize.Height, scrollSize.Height, r.vertArea)
221✔
459
                r.handleShadowVisibility(r.scroll.Offset.Y, contentSize.Height, scrollSize.Height, r.topShadow, r.bottomShadow)
221✔
460
                cache.Renderer(r.vertArea).Layout(scrollSize)
221✔
461
        } else {
230✔
462
                r.vertArea.Hide()
9✔
463
                r.topShadow.Hide()
9✔
464
                r.bottomShadow.Hide()
9✔
465
        }
9✔
466
        if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth {
451✔
467
                r.handleAreaVisibility(contentSize.Width, scrollSize.Width, r.horizArea)
221✔
468
                r.handleShadowVisibility(r.scroll.Offset.X, contentSize.Width, scrollSize.Width, r.leftShadow, r.rightShadow)
221✔
469
                cache.Renderer(r.horizArea).Layout(scrollSize)
221✔
470
        } else {
230✔
471
                r.horizArea.Hide()
9✔
472
                r.leftShadow.Hide()
9✔
473
                r.rightShadow.Hide()
9✔
474
        }
9✔
475

476
        if r.scroll.Direction != ScrollHorizontalOnly {
454✔
477
                canvas.Refresh(r.vertArea) // this is required to force the canvas to update, we have no "Redraw()"
224✔
478
        } else {
230✔
479
                canvas.Refresh(r.horizArea) // this is required like above but if we are horizontal
6✔
480
        }
6✔
481
}
482

483
// Scroll defines a container that is smaller than the Content.
484
// The Offset is used to determine the position of the child widgets within the container.
485
type Scroll struct {
486
        Base
487
        minSize   fyne.Size
488
        Direction ScrollDirection
489
        Content   fyne.CanvasObject
490
        Offset    fyne.Position
491
        // OnScrolled can be set to be notified when the Scroll has changed position.
492
        // You should not update the Scroll.Offset from this method.
493
        //
494
        // Since: 2.0
495
        OnScrolled func(fyne.Position) `json:"-"`
496
}
497

498
// CreateRenderer is a private method to Fyne which links this widget to its renderer
499
func (s *Scroll) CreateRenderer() fyne.WidgetRenderer {
50✔
500
        scr := &scrollContainerRenderer{
50✔
501
                BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{s.Content}),
50✔
502
                scroll:       s,
50✔
503
        }
50✔
504
        scr.vertArea = newScrollBarArea(s, scrollBarOrientationVertical)
50✔
505
        scr.topShadow = NewShadow(ShadowBottom, SubmergedContentLevel)
50✔
506
        scr.bottomShadow = NewShadow(ShadowTop, SubmergedContentLevel)
50✔
507
        scr.horizArea = newScrollBarArea(s, scrollBarOrientationHorizontal)
50✔
508
        scr.leftShadow = NewShadow(ShadowRight, SubmergedContentLevel)
50✔
509
        scr.rightShadow = NewShadow(ShadowLeft, SubmergedContentLevel)
50✔
510
        scr.SetObjects(append(scr.Objects(), scr.topShadow, scr.bottomShadow, scr.leftShadow, scr.rightShadow,
50✔
511
                scr.vertArea, scr.horizArea))
50✔
512
        scr.updatePosition()
50✔
513

50✔
514
        return scr
50✔
515
}
50✔
516

517
// ScrollToBottom will scroll content to container bottom - to show latest info which end user just added
518
func (s *Scroll) ScrollToBottom() {
1✔
519
        s.scrollBy(0, -1*(s.Content.MinSize().Height-s.Size().Height-s.Offset.Y))
1✔
520
        s.refreshBars()
1✔
521
}
1✔
522

523
// ScrollToTop will scroll content to container top
524
func (s *Scroll) ScrollToTop() {
1✔
525
        s.ScrollToOffset(fyne.Position{})
1✔
526
        s.refreshBars()
1✔
527
}
1✔
528

529
// DragEnd will stop scrolling on mobile has stopped
530
func (s *Scroll) DragEnd() {
×
531
}
×
532

533
// Dragged will scroll on any drag - bar or otherwise - for mobile
534
func (s *Scroll) Dragged(e *fyne.DragEvent) {
×
535
        if !fyne.CurrentDevice().IsMobile() {
×
536
                return
×
537
        }
×
538

539
        if s.updateOffset(e.Dragged.DX, e.Dragged.DY) {
×
540
                s.refreshWithoutOffsetUpdate()
×
541
        }
×
542
}
543

544
// MinSize returns the smallest size this widget can shrink to
545
func (s *Scroll) MinSize() fyne.Size {
20✔
546
        min := fyne.NewSize(scrollContainerMinSize, scrollContainerMinSize).Max(s.minSize)
20✔
547
        switch s.Direction {
20✔
548
        case ScrollHorizontalOnly:
5✔
549
                min.Height = max(min.Height, s.Content.MinSize().Height)
5✔
550
        case ScrollVerticalOnly:
5✔
551
                min.Width = max(min.Width, s.Content.MinSize().Width)
5✔
552
        case ScrollNone:
×
553
                return s.Content.MinSize()
×
554
        }
555
        return min
20✔
556
}
557

558
// SetMinSize specifies a minimum size for this scroll container.
559
// If the specified size is larger than the content size then scrolling will not be enabled
560
// This can be helpful to appear larger than default if the layout is collapsing this widget.
561
func (s *Scroll) SetMinSize(size fyne.Size) {
7✔
562
        s.minSize = size
7✔
563
}
7✔
564

565
// Refresh causes this widget to be redrawn in it's current state
566
func (s *Scroll) Refresh() {
8✔
567
        s.refreshBars()
8✔
568

8✔
569
        if s.Content != nil {
16✔
570
                s.Content.Refresh()
8✔
571
        }
8✔
572
}
573

574
// Resize is called when this scroller should change size. We refresh to ensure the scroll bars are updated.
575
func (s *Scroll) Resize(sz fyne.Size) {
47✔
576
        if sz == s.Size() {
48✔
577
                return
1✔
578
        }
1✔
579

580
        s.Base.Resize(sz)
46✔
581
        s.refreshBars()
46✔
582
}
583

584
// ScrollToOffset will update the location of the content of this scroll container.
585
//
586
// Since: 2.6
587
func (s *Scroll) ScrollToOffset(p fyne.Position) {
2✔
588
        if s.Offset == p {
2✔
589
                return
×
590
        }
×
591

592
        s.Offset = p
2✔
593
        s.refreshBars()
2✔
594
}
595

596
func (s *Scroll) refreshWithoutOffsetUpdate() {
126✔
597
        s.Base.Refresh()
126✔
598
}
126✔
599

600
// Scrolled is called when an input device triggers a scroll event
601
func (s *Scroll) Scrolled(ev *fyne.ScrollEvent) {
21✔
602
        if s.Direction != ScrollNone {
41✔
603
                s.scrollBy(ev.Scrolled.DX, ev.Scrolled.DY)
20✔
604
        }
20✔
605
}
606

607
func (s *Scroll) refreshBars() {
78✔
608
        s.updateOffset(0, 0)
78✔
609
        s.refreshWithoutOffsetUpdate()
78✔
610
}
78✔
611

612
func (s *Scroll) scrollBy(dx, dy float32) {
21✔
613
        min := s.Content.MinSize()
21✔
614
        size := s.Size()
21✔
615
        if size.Width < min.Width && size.Height >= min.Height && dx == 0 {
22✔
616
                dx, dy = dy, dx
1✔
617
        }
1✔
618
        if s.updateOffset(dx, dy) {
40✔
619
                s.refreshWithoutOffsetUpdate()
19✔
620
        }
19✔
621
}
622

623
func (s *Scroll) updateOffset(deltaX, deltaY float32) bool {
99✔
624
        size := s.Size()
99✔
625
        contentSize := s.Content.Size()
99✔
626
        if contentSize.Width <= size.Width && contentSize.Height <= size.Height {
109✔
627
                if s.Offset.X != 0 || s.Offset.Y != 0 {
12✔
628
                        s.Offset.X = 0
2✔
629
                        s.Offset.Y = 0
2✔
630
                        return true
2✔
631
                }
2✔
632
                return false
8✔
633
        }
634
        oldX := s.Offset.X
89✔
635
        oldY := s.Offset.Y
89✔
636
        min := s.Content.MinSize()
89✔
637
        s.Offset.X = computeOffset(s.Offset.X, -deltaX, size.Width, min.Width)
89✔
638
        s.Offset.Y = computeOffset(s.Offset.Y, -deltaY, size.Height, min.Height)
89✔
639

89✔
640
        moved := s.Offset.X != oldX || s.Offset.Y != oldY
89✔
641
        if f := s.OnScrolled; f != nil && moved {
90✔
642
                f(s.Offset)
1✔
643
        }
1✔
644
        return moved
89✔
645
}
646

647
func computeOffset(start, delta, outerWidth, innerWidth float32) float32 {
178✔
648
        offset := start + delta
178✔
649
        if offset+outerWidth >= innerWidth {
211✔
650
                offset = innerWidth - outerWidth
33✔
651
        }
33✔
652

653
        return max(offset, 0)
178✔
654
}
655

656
// NewScroll creates a scrollable parent wrapping the specified content.
657
// Note that this may cause the MinSize to be smaller than that of the passed object.
658
func NewScroll(content fyne.CanvasObject) *Scroll {
43✔
659
        s := newScrollContainerWithDirection(ScrollBoth, content)
43✔
660
        s.ExtendBaseWidget(s)
43✔
661
        return s
43✔
662
}
43✔
663

664
// NewHScroll create a scrollable parent wrapping the specified content.
665
// Note that this may cause the MinSize.Width to be smaller than that of the passed object.
666
func NewHScroll(content fyne.CanvasObject) *Scroll {
6✔
667
        s := newScrollContainerWithDirection(ScrollHorizontalOnly, content)
6✔
668
        s.ExtendBaseWidget(s)
6✔
669
        return s
6✔
670
}
6✔
671

672
// NewVScroll create a scrollable parent wrapping the specified content.
673
// Note that this may cause the MinSize.Height to be smaller than that of the passed object.
674
func NewVScroll(content fyne.CanvasObject) *Scroll {
6✔
675
        s := newScrollContainerWithDirection(ScrollVerticalOnly, content)
6✔
676
        s.ExtendBaseWidget(s)
6✔
677
        return s
6✔
678
}
6✔
679

680
func newScrollContainerWithDirection(direction ScrollDirection, content fyne.CanvasObject) *Scroll {
55✔
681
        s := &Scroll{
55✔
682
                Direction: direction,
55✔
683
                Content:   content,
55✔
684
        }
55✔
685
        s.ExtendBaseWidget(s)
55✔
686
        return s
55✔
687
}
55✔
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

© 2025 Coveralls, Inc