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

fyne-io / fyne / 22076210847

16 Feb 2026 08:10PM UTC coverage: 61.098% (+0.04%) from 61.061%
22076210847

push

github

andydotxyz
Fix counting for nested ordered lists

6 of 6 new or added lines in 1 file covered. (100.0%)

137 existing lines in 5 files now uncovered.

25922 of 42427 relevant lines covered (61.1%)

692.18 hits per line

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

63.56
/widget/richtext_objects.go
1
package widget
2

3
import (
4
        "image/color"
5
        "net/url"
6
        "strconv"
7
        "strings"
8

9
        "fyne.io/fyne/v2"
10
        "fyne.io/fyne/v2/canvas"
11
        "fyne.io/fyne/v2/internal/scale"
12
        "fyne.io/fyne/v2/theme"
13
)
14

15
var (
16
        // RichTextStyleBlockquote represents a quote presented in an indented block.
17
        //
18
        // Since: 2.1
19
        RichTextStyleBlockquote = RichTextStyle{
20
                ColorName: theme.ColorNameForeground,
21
                Inline:    false,
22
                SizeName:  theme.SizeNameText,
23
                TextStyle: fyne.TextStyle{Italic: true},
24
        }
25
        // RichTextStyleCodeBlock represents a code blog segment.
26
        //
27
        // Since: 2.1
28
        RichTextStyleCodeBlock = RichTextStyle{
29
                ColorName: theme.ColorNameForeground,
30
                Inline:    false,
31
                SizeName:  theme.SizeNameText,
32
                TextStyle: fyne.TextStyle{Monospace: true},
33
        }
34
        // RichTextStyleCodeInline represents an inline code segment.
35
        //
36
        // Since: 2.1
37
        RichTextStyleCodeInline = RichTextStyle{
38
                ColorName: theme.ColorNameForeground,
39
                Inline:    true,
40
                SizeName:  theme.SizeNameText,
41
                TextStyle: fyne.TextStyle{Monospace: true},
42
        }
43
        // RichTextStyleEmphasis represents regular text with emphasis.
44
        //
45
        // Since: 2.1
46
        RichTextStyleEmphasis = RichTextStyle{
47
                ColorName: theme.ColorNameForeground,
48
                Inline:    true,
49
                SizeName:  theme.SizeNameText,
50
                TextStyle: fyne.TextStyle{Italic: true},
51
        }
52
        // RichTextStyleHeading represents a heading text that stands on its own line.
53
        //
54
        // Since: 2.1
55
        RichTextStyleHeading = RichTextStyle{
56
                ColorName: theme.ColorNameForeground,
57
                Inline:    false,
58
                SizeName:  theme.SizeNameHeadingText,
59
                TextStyle: fyne.TextStyle{Bold: true},
60
        }
61
        // RichTextStyleInline represents standard text that can be surrounded by other elements.
62
        //
63
        // Since: 2.1
64
        RichTextStyleInline = RichTextStyle{
65
                ColorName: theme.ColorNameForeground,
66
                Inline:    true,
67
                SizeName:  theme.SizeNameText,
68
        }
69
        // RichTextStyleParagraph represents standard text that should appear separate from other text.
70
        //
71
        // Since: 2.1
72
        RichTextStyleParagraph = RichTextStyle{
73
                ColorName: theme.ColorNameForeground,
74
                Inline:    false,
75
                SizeName:  theme.SizeNameText,
76
        }
77
        // RichTextStylePassword represents standard sized text where the characters are obscured.
78
        //
79
        // Since: 2.1
80
        RichTextStylePassword = RichTextStyle{
81
                ColorName: theme.ColorNameForeground,
82
                Inline:    true,
83
                SizeName:  theme.SizeNameText,
84
                concealed: true,
85
        }
86
        // RichTextStyleStrong represents regular text with a strong emphasis.
87
        //
88
        // Since: 2.1
89
        RichTextStyleStrong = RichTextStyle{
90
                ColorName: theme.ColorNameForeground,
91
                Inline:    true,
92
                SizeName:  theme.SizeNameText,
93
                TextStyle: fyne.TextStyle{Bold: true},
94
        }
95
        // RichTextStyleSubHeading represents a sub-heading text that stands on its own line.
96
        //
97
        // Since: 2.1
98
        RichTextStyleSubHeading = RichTextStyle{
99
                ColorName: theme.ColorNameForeground,
100
                Inline:    false,
101
                SizeName:  theme.SizeNameSubHeadingText,
102
                TextStyle: fyne.TextStyle{Bold: true},
103
        }
104
)
105

106
// HyperlinkSegment represents a hyperlink within a rich text widget.
107
//
108
// Since: 2.1
109
type HyperlinkSegment struct {
110
        Alignment fyne.TextAlign
111
        Text      string
112
        URL       *url.URL
113

114
        // OnTapped overrides the default `fyne.OpenURL` call when the link is tapped
115
        //
116
        // Since: 2.4
117
        OnTapped func() `json:"-"`
118
}
119

120
// Inline returns true as hyperlinks are inside other elements.
121
func (h *HyperlinkSegment) Inline() bool {
11✔
122
        return true
11✔
123
}
11✔
124

125
// Textual returns the content of this segment rendered to plain text.
126
func (h *HyperlinkSegment) Textual() string {
3✔
127
        return h.Text
3✔
128
}
3✔
129

130
// Visual returns a new instance of a hyperlink widget required to render this segment.
131
func (h *HyperlinkSegment) Visual() fyne.CanvasObject {
2✔
132
        link := NewHyperlink(h.Text, h.URL)
2✔
133
        link.Alignment = h.Alignment
2✔
134
        link.OnTapped = h.OnTapped
2✔
135
        return &fyne.Container{Layout: &unpadTextWidgetLayout{parent: link}, Objects: []fyne.CanvasObject{link}}
2✔
136
}
2✔
137

138
// Update applies the current state of this hyperlink segment to an existing visual.
139
func (h *HyperlinkSegment) Update(o fyne.CanvasObject) {
3✔
140
        link := o.(*fyne.Container).Objects[0].(*Hyperlink)
3✔
141
        link.Text = h.Text
3✔
142
        link.URL = h.URL
3✔
143
        link.Alignment = h.Alignment
3✔
144
        link.OnTapped = h.OnTapped
3✔
145
        link.Refresh()
3✔
146
}
3✔
147

148
// Select tells the segment that the user is selecting the content between the two positions.
149
func (h *HyperlinkSegment) Select(begin, end fyne.Position) {
×
150
        // no-op: this will be added when we progress to editor
×
UNCOV
151
}
×
152

153
// SelectedText should return the text representation of any content currently selected through the Select call.
154
func (h *HyperlinkSegment) SelectedText() string {
×
155
        // no-op: this will be added when we progress to editor
×
156
        return ""
×
UNCOV
157
}
×
158

159
// Unselect tells the segment that the user is has cancelled the previous selection.
160
func (h *HyperlinkSegment) Unselect() {
×
161
        // no-op: this will be added when we progress to editor
×
UNCOV
162
}
×
163

164
// ImageSegment represents an image within a rich text widget.
165
//
166
// Since: 2.3
167
type ImageSegment struct {
168
        Source fyne.URI
169
        Title  string
170

171
        // Alignment specifies the horizontal alignment of this image segment
172
        // Since: 2.4
173
        Alignment fyne.TextAlign
174
}
175

176
// Inline returns false as images in rich text are blocks.
177
func (i *ImageSegment) Inline() bool {
9✔
178
        return false
9✔
179
}
9✔
180

181
// Textual returns the content of this segment rendered to plain text.
182
func (i *ImageSegment) Textual() string {
×
183
        return "Image " + i.Title
×
UNCOV
184
}
×
185

186
// Visual returns a new instance of an image widget required to render this segment.
187
func (i *ImageSegment) Visual() fyne.CanvasObject {
1✔
188
        return newRichImage(i.Source, i.Alignment)
1✔
189
}
1✔
190

191
// Update applies the current state of this image segment to an existing visual.
192
func (i *ImageSegment) Update(o fyne.CanvasObject) {
4✔
193
        newer := canvas.NewImageFromURI(i.Source)
4✔
194
        img := o.(*richImage)
4✔
195

4✔
196
        // one of the following will be used
4✔
197
        img.img.File = newer.File
4✔
198
        img.img.Resource = newer.Resource
4✔
199
        img.setAlign(i.Alignment)
4✔
200

4✔
201
        img.Refresh()
4✔
202
}
4✔
203

204
// Select tells the segment that the user is selecting the content between the two positions.
205
func (i *ImageSegment) Select(begin, end fyne.Position) {
×
206
        // no-op: this will be added when we progress to editor
×
UNCOV
207
}
×
208

209
// SelectedText should return the text representation of any content currently selected through the Select call.
210
func (i *ImageSegment) SelectedText() string {
×
211
        // no-op: images have no text rendering
×
212
        return ""
×
UNCOV
213
}
×
214

215
// Unselect tells the segment that the user is has cancelled the previous selection.
216
func (i *ImageSegment) Unselect() {
×
217
        // no-op: this will be added when we progress to editor
×
UNCOV
218
}
×
219

220
// ListSegment includes an itemised list with the content set using the Items field.
221
//
222
// Since: 2.1
223
type ListSegment struct {
224
        Items   []RichTextSegment
225
        Ordered bool
226

227
        // startIndex is the starting number - 1 (If it is ordered). Unordered lists
228
        // ignore startIndex.
229
        //
230
        // startIndex is set to start - 1 to allow the empty value of ListSegment to have a starting
231
        // number of 1, while also allowing the caller to override the starting
232
        // number to any int, including 0.
233
        startIndex       int
234
        indentationLevel int
235
}
236

237
// SetStartNumber sets the starting number for an ordered list.
238
// Unordered lists are not affected.
239
//
240
// Since: 2.7
241
func (l *ListSegment) SetStartNumber(s int) {
4✔
242
        l.startIndex = s - 1
4✔
243
}
4✔
244

245
// StartNumber return the starting number for an ordered list.
246
//
247
// Since: 2.7
248
func (l *ListSegment) StartNumber() int {
12✔
249
        return l.startIndex + 1
12✔
250
}
12✔
251

252
// Inline returns false as a list should be in a block.
253
func (l *ListSegment) Inline() bool {
1✔
254
        return false
1✔
255
}
1✔
256

257
// Segments returns the segments required to draw bullets before each item
258
func (l *ListSegment) Segments() []RichTextSegment {
9✔
259
        out := make([]RichTextSegment, len(l.Items))
9✔
260
        j := l.StartNumber()
9✔
261
        for i, in := range l.Items {
26✔
262
                var texts []RichTextSegment
17✔
263
                if _, ok := in.(*ListSegment); !ok {
33✔
264
                        txt := "• "
16✔
265
                        if l.Ordered {
30✔
266
                                txt = strconv.Itoa(j) + "."
14✔
267
                                j++
14✔
268
                        }
14✔
269
                        indentation := strings.Repeat(" ", l.indentationLevel*4)
16✔
270
                        bullet := &TextSegment{Text: indentation + txt + " ", Style: RichTextStyleStrong}
16✔
271
                        texts = append(texts, bullet)
16✔
272
                }
273
                texts = append(texts, in)
17✔
274
                out[i] = &ParagraphSegment{Texts: texts}
17✔
275
        }
276
        return out
9✔
277
}
278

279
// Textual returns no content for a list as the content is in sub-segments.
280
func (l *ListSegment) Textual() string {
×
281
        return ""
×
282
}
×
283

284
// Visual returns no additional elements for this segment.
285
func (l *ListSegment) Visual() fyne.CanvasObject {
×
286
        return nil
×
UNCOV
287
}
×
288

289
// Update doesn't need to change a list visual.
290
func (l *ListSegment) Update(fyne.CanvasObject) {
×
UNCOV
291
}
×
292

293
// Select does nothing for a list container.
294
func (l *ListSegment) Select(_, _ fyne.Position) {
×
295
}
×
296

297
// SelectedText returns the empty string for this list.
298
func (l *ListSegment) SelectedText() string {
×
299
        return ""
×
UNCOV
300
}
×
301

302
// Unselect does nothing for a list container.
UNCOV
303
func (l *ListSegment) Unselect() {
×
UNCOV
304
}
×
305

306
// ParagraphSegment wraps a number of text elements in a paragraph.
307
// It is similar to using a list of text elements when the final style is RichTextStyleParagraph.
308
//
309
// Since: 2.1
310
type ParagraphSegment struct {
311
        Texts []RichTextSegment
312
}
313

314
// Inline returns false as a paragraph should be in a block.
315
func (p *ParagraphSegment) Inline() bool {
14✔
316
        return false
14✔
317
}
14✔
318

319
// Segments returns the list of text elements in this paragraph.
320
func (p *ParagraphSegment) Segments() []RichTextSegment {
23✔
321
        return p.Texts
23✔
322
}
23✔
323

324
// Textual returns no content for a paragraph container.
325
func (p *ParagraphSegment) Textual() string {
×
326
        return ""
×
327
}
×
328

329
// Visual returns the no extra elements.
330
func (p *ParagraphSegment) Visual() fyne.CanvasObject {
×
331
        return nil
×
UNCOV
332
}
×
333

334
// Update doesn't need to change a paragraph container.
335
func (p *ParagraphSegment) Update(fyne.CanvasObject) {
×
UNCOV
336
}
×
337

338
// Select does nothing for a paragraph container.
339
func (p *ParagraphSegment) Select(_, _ fyne.Position) {
×
340
}
×
341

342
// SelectedText returns the empty string for this paragraph container.
343
func (p *ParagraphSegment) SelectedText() string {
×
344
        return ""
×
UNCOV
345
}
×
346

347
// Unselect does nothing for a paragraph container.
UNCOV
348
func (p *ParagraphSegment) Unselect() {
×
UNCOV
349
}
×
350

351
// SeparatorSegment includes a horizontal separator in a rich text widget.
352
//
353
// Since: 2.1
354
type SeparatorSegment struct {
355
        _ bool // Without this a pointer to SeparatorSegment will always be the same.
356
}
357

358
// Inline returns false as a separator should be full width.
359
func (s *SeparatorSegment) Inline() bool {
×
360
        return false
×
361
}
×
362

363
// Textual returns no content for a separator element.
364
func (s *SeparatorSegment) Textual() string {
×
365
        return ""
×
366
}
×
367

368
// Visual returns a new instance of a separator widget for this segment.
369
func (s *SeparatorSegment) Visual() fyne.CanvasObject {
×
370
        return NewSeparator()
×
UNCOV
371
}
×
372

373
// Update doesn't need to change a separator visual.
374
func (s *SeparatorSegment) Update(fyne.CanvasObject) {
×
UNCOV
375
}
×
376

377
// Select does nothing for a separator.
378
func (s *SeparatorSegment) Select(_, _ fyne.Position) {
×
379
}
×
380

381
// SelectedText returns the empty string for this separator.
382
func (s *SeparatorSegment) SelectedText() string {
×
383
        return "" // TODO maybe return "---\n"?
×
UNCOV
384
}
×
385

386
// Unselect does nothing for a separator.
UNCOV
387
func (s *SeparatorSegment) Unselect() {
×
UNCOV
388
}
×
389

390
// RichTextStyle describes the details of a text object inside a RichText widget.
391
//
392
// Since: 2.1
393
type RichTextStyle struct {
394
        Alignment fyne.TextAlign
395
        ColorName fyne.ThemeColorName
396
        Inline    bool
397
        SizeName  fyne.ThemeSizeName // The theme name of the text size to use, if blank will be the standard text size
398
        TextStyle fyne.TextStyle
399

400
        // an internal detail where we obscure password fields
401
        concealed bool
402
}
403

404
// RichTextSegment describes any element that can be rendered in a RichText widget.
405
//
406
// Since: 2.1
407
type RichTextSegment interface {
408
        Inline() bool
409
        Textual() string
410
        Update(fyne.CanvasObject)
411
        Visual() fyne.CanvasObject
412

413
        Select(pos1, pos2 fyne.Position)
414
        SelectedText() string
415
        Unselect()
416
}
417

418
// TextSegment represents the styling for a segment of rich text.
419
//
420
// Since: 2.1
421
type TextSegment struct {
422
        Style RichTextStyle
423
        Text  string
424

425
        parent *RichText
426
}
427

428
// Inline should return true if this text can be included within other elements, or false if it creates a new block.
429
func (t *TextSegment) Inline() bool {
34,895✔
430
        return t.Style.Inline
34,895✔
431
}
34,895✔
432

433
// Textual returns the content of this segment rendered to plain text.
434
func (t *TextSegment) Textual() string {
4,490✔
435
        return t.Text
4,490✔
436
}
4,490✔
437

438
// Visual returns a new instance of a graphical element required to render this segment.
439
func (t *TextSegment) Visual() fyne.CanvasObject {
1,991✔
440
        obj := canvas.NewText(t.Text, t.color())
1,991✔
441

1,991✔
442
        t.Update(obj)
1,991✔
443
        return obj
1,991✔
444
}
1,991✔
445

446
// Update applies the current state of this text segment to an existing visual.
447
func (t *TextSegment) Update(o fyne.CanvasObject) {
8,563✔
448
        obj := o.(*canvas.Text)
8,563✔
449
        obj.Text = t.Text
8,563✔
450
        obj.Color = t.color()
8,563✔
451
        obj.Alignment = t.Style.Alignment
8,563✔
452
        obj.TextStyle = t.Style.TextStyle
8,563✔
453
        obj.TextSize = t.size()
8,563✔
454
        obj.Refresh()
8,563✔
455
}
8,563✔
456

457
// Select tells the segment that the user is selecting the content between the two positions.
458
func (t *TextSegment) Select(begin, end fyne.Position) {
×
459
        // no-op: this will be added when we progress to editor
×
460
}
×
461

462
// SelectedText should return the text representation of any content currently selected through the Select call.
UNCOV
463
func (t *TextSegment) SelectedText() string {
×
464
        // no-op: this will be added when we progress to editor
×
465
        return ""
×
466
}
×
467

468
// Unselect tells the segment that the user is has cancelled the previous selection.
UNCOV
469
func (t *TextSegment) Unselect() {
×
UNCOV
470
        // no-op: this will be added when we progress to editor
×
UNCOV
471
}
×
472

473
func (t *TextSegment) color() color.Color {
10,554✔
474
        if t.Style.ColorName != "" {
21,020✔
475
                return theme.ColorForWidget(t.Style.ColorName, t.parent)
10,466✔
476
        }
10,466✔
477

478
        return theme.ColorForWidget(theme.ColorNameForeground, t.parent)
88✔
479
}
480

481
func (t *TextSegment) size() float32 {
46,609✔
482
        if t.Style.SizeName != "" {
93,099✔
483
                i := theme.SizeForWidget(t.Style.SizeName, t.parent)
46,490✔
484
                return i
46,490✔
485
        }
46,490✔
486

487
        i := theme.SizeForWidget(theme.SizeNameText, t.parent)
119✔
488
        return i
119✔
489
}
490

491
type richImage struct {
492
        BaseWidget
493
        align  fyne.TextAlign
494
        img    *canvas.Image
495
        oldMin fyne.Size
496
        layout *fyne.Container
497
        min    fyne.Size
498
}
499

500
func newRichImage(u fyne.URI, align fyne.TextAlign) *richImage {
1✔
501
        img := canvas.NewImageFromURI(u)
1✔
502
        img.FillMode = canvas.ImageFillOriginal
1✔
503
        i := &richImage{img: img, align: align}
1✔
504
        i.ExtendBaseWidget(i)
1✔
505
        return i
1✔
506
}
1✔
507

508
func (r *richImage) CreateRenderer() fyne.WidgetRenderer {
1✔
509
        r.layout = &fyne.Container{Layout: &richImageLayout{r}, Objects: []fyne.CanvasObject{r.img}}
1✔
510
        return NewSimpleRenderer(r.layout)
1✔
511
}
1✔
512

513
func (r *richImage) MinSize() fyne.Size {
9✔
514
        orig := r.img.MinSize()
9✔
515
        c := fyne.CurrentApp().Driver().CanvasForObject(r)
9✔
516
        if c == nil {
9✔
UNCOV
517
                return r.oldMin // not yet rendered
×
UNCOV
518
        }
×
519

520
        // unscale the image so it is not varying based on canvas
521
        w := scale.ToScreenCoordinate(c, orig.Width)
9✔
522
        h := scale.ToScreenCoordinate(c, orig.Height)
9✔
523
        // we return size / 2 as this assumes a HiDPI / 2x image scaling
9✔
524
        r.min = fyne.NewSize(float32(w)/2, float32(h)/2)
9✔
525
        return r.min
9✔
526
}
527

528
func (r *richImage) setAlign(a fyne.TextAlign) {
4✔
529
        if r.layout != nil {
7✔
530
                r.layout.Refresh()
3✔
531
        }
3✔
532
        r.align = a
4✔
533
}
534

535
type richImageLayout struct {
536
        r *richImage
537
}
538

539
func (r *richImageLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
9✔
540
        r.r.img.Resize(r.r.min)
9✔
541
        gap := float32(0)
9✔
542

9✔
543
        switch r.r.align {
9✔
544
        case fyne.TextAlignCenter:
2✔
545
                gap = (s.Width - r.r.min.Width) / 2
2✔
546
        case fyne.TextAlignTrailing:
1✔
547
                gap = s.Width - r.r.min.Width
1✔
548
        }
549

550
        r.r.img.Move(fyne.NewPos(gap, 0))
9✔
551
}
552

UNCOV
553
func (r *richImageLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
×
UNCOV
554
        return r.r.min
×
UNCOV
555
}
×
556

557
type unpadTextWidgetLayout struct {
558
        parent fyne.Widget
559
}
560

561
func (u *unpadTextWidgetLayout) Layout(o []fyne.CanvasObject, s fyne.Size) {
4✔
562
        innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent)
4✔
563
        pad := innerPad * -1
4✔
564
        pad2 := pad * -2
4✔
565

4✔
566
        o[0].Move(fyne.NewPos(pad, pad))
4✔
567
        o[0].Resize(s.Add(fyne.NewSize(pad2, pad2)))
4✔
568
}
4✔
569

570
func (u *unpadTextWidgetLayout) MinSize(o []fyne.CanvasObject) fyne.Size {
10✔
571
        innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent)
10✔
572
        pad := innerPad * 2
10✔
573
        return o[0].MinSize().Subtract(fyne.NewSize(pad, pad))
10✔
574
}
10✔
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