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

fyne-io / fyne / 25698801121

11 May 2026 09:34PM UTC coverage: 60.335% (+0.007%) from 60.328%
25698801121

Pull #6299

github

Vinayak9769
Enhance RichText layout by adding indentation support for wrapped continuation rows
Pull Request #6299: Fix indentation of wrapped list items in RichText

18 of 22 new or added lines in 1 file covered. (81.82%)

109 existing lines in 2 files now uncovered.

26455 of 43847 relevant lines covered (60.33%)

674.8 hits per line

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

63.71
/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:    true,
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:    true,
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
        // Since 2.8
120
        TextStyle fyne.TextStyle
121
        // Since 2.8
122
        SizeName fyne.ThemeSizeName // The theme name of the text size to use, if blank will be the standard text size
123
}
124

125
// Inline returns true as hyperlinks are inside other elements.
126
func (h *HyperlinkSegment) Inline() bool {
15✔
127
        return true
15✔
128
}
15✔
129

130
// Textual returns the content of this segment rendered to plain text.
131
func (h *HyperlinkSegment) Textual() string {
34✔
132
        return h.Text
34✔
133
}
34✔
134

135
// Visual returns a new instance of a hyperlink widget required to render this segment.
136
func (h *HyperlinkSegment) Visual() fyne.CanvasObject {
5✔
137
        link := NewHyperlink(h.Text, h.URL)
5✔
138
        link.Alignment = h.Alignment
5✔
139
        link.OnTapped = h.OnTapped
5✔
140
        return &fyne.Container{Layout: &unpadTextWidgetLayout{parent: link}, Objects: []fyne.CanvasObject{link}}
5✔
141
}
5✔
142

143
// Update applies the current state of this hyperlink segment to an existing visual.
144
func (h *HyperlinkSegment) Update(o fyne.CanvasObject) {
6✔
145
        link := o.(*fyne.Container).Objects[0].(*Hyperlink)
6✔
146
        link.URL = h.URL
6✔
147
        link.Alignment = h.Alignment
6✔
148
        link.SizeName = h.SizeName
6✔
149
        link.TextStyle = h.TextStyle
6✔
150
        link.OnTapped = h.OnTapped
6✔
151
        link.Refresh()
6✔
152
}
6✔
153

154
// Select tells the segment that the user is selecting the content between the two positions.
155
func (h *HyperlinkSegment) Select(begin, end fyne.Position) {
×
156
        // no-op: this will be added when we progress to editor
×
157
}
×
158

159
// SelectedText should return the text representation of any content currently selected through the Select call.
160
func (h *HyperlinkSegment) SelectedText() string {
×
161
        // no-op: this will be added when we progress to editor
×
162
        return ""
×
163
}
×
164

165
// Unselect tells the segment that the user is has cancelled the previous selection.
166
func (h *HyperlinkSegment) Unselect() {
×
167
        // no-op: this will be added when we progress to editor
×
168
}
×
169

170
// ImageSegment represents an image within a rich text widget.
171
//
172
// Since: 2.3
173
type ImageSegment struct {
174
        Source fyne.URI
175
        Title  string
176

177
        // Alignment specifies the horizontal alignment of this image segment
178
        // Since: 2.4
179
        Alignment fyne.TextAlign
180
}
181

182
// Inline returns false as images in rich text are blocks.
183
func (i *ImageSegment) Inline() bool {
8✔
184
        return false
8✔
185
}
8✔
186

187
// Textual returns the content of this segment rendered to plain text.
188
func (i *ImageSegment) Textual() string {
×
189
        return "Image " + i.Title
×
190
}
×
191

192
// Visual returns a new instance of an image widget required to render this segment.
193
func (i *ImageSegment) Visual() fyne.CanvasObject {
1✔
194
        return newRichImage(i.Source, i.Alignment)
1✔
195
}
1✔
196

197
// Update applies the current state of this image segment to an existing visual.
198
func (i *ImageSegment) Update(o fyne.CanvasObject) {
4✔
199
        newer := canvas.NewImageFromURI(i.Source)
4✔
200
        img := o.(*richImage)
4✔
201

4✔
202
        // one of the following will be used
4✔
203
        img.img.File = newer.File
4✔
204
        img.img.Resource = newer.Resource
4✔
205
        img.setAlign(i.Alignment)
4✔
206

4✔
207
        img.Refresh()
4✔
208
}
4✔
209

210
// Select tells the segment that the user is selecting the content between the two positions.
211
func (i *ImageSegment) Select(begin, end fyne.Position) {
×
212
        // no-op: this will be added when we progress to editor
×
213
}
×
214

215
// SelectedText should return the text representation of any content currently selected through the Select call.
216
func (i *ImageSegment) SelectedText() string {
×
217
        // no-op: images have no text rendering
×
218
        return ""
×
219
}
×
220

221
// Unselect tells the segment that the user is has cancelled the previous selection.
222
func (i *ImageSegment) Unselect() {
×
223
        // no-op: this will be added when we progress to editor
×
224
}
×
225

226
// ListSegment includes an itemised list with the content set using the Items field.
227
//
228
// Since: 2.1
229
type ListSegment struct {
230
        Items   []RichTextSegment
231
        Ordered bool
232

233
        // startIndex is the starting number - 1 (If it is ordered). Unordered lists
234
        // ignore startIndex.
235
        //
236
        // startIndex is set to start - 1 to allow the empty value of ListSegment to have a starting
237
        // number of 1, while also allowing the caller to override the starting
238
        // number to any int, including 0.
239
        startIndex       int
240
        indentationLevel int
241
}
242

243
// SetStartNumber sets the starting number for an ordered list.
244
// Unordered lists are not affected.
245
//
246
// Since: 2.7
247
func (l *ListSegment) SetStartNumber(s int) {
4✔
248
        l.startIndex = s - 1
4✔
249
}
4✔
250

251
// StartNumber return the starting number for an ordered list.
252
//
253
// Since: 2.7
254
func (l *ListSegment) StartNumber() int {
12✔
255
        return l.startIndex + 1
12✔
256
}
12✔
257

258
// Inline returns false as a list should be in a block.
259
func (l *ListSegment) Inline() bool {
1✔
260
        return false
1✔
261
}
1✔
262

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

285
// Textual returns no content for a list as the content is in sub-segments.
UNCOV
286
func (l *ListSegment) Textual() string {
×
UNCOV
287
        return ""
×
UNCOV
288
}
×
289

290
// Visual returns no additional elements for this segment.
291
func (l *ListSegment) Visual() fyne.CanvasObject {
×
UNCOV
292
        return nil
×
UNCOV
293
}
×
294

295
// Update doesn't need to change a list visual.
296
func (l *ListSegment) Update(fyne.CanvasObject) {
×
UNCOV
297
}
×
298

299
// Select does nothing for a list container.
300
func (l *ListSegment) Select(_, _ fyne.Position) {
×
UNCOV
301
}
×
302

303
// SelectedText returns the empty string for this list.
304
func (l *ListSegment) SelectedText() string {
×
UNCOV
305
        return ""
×
UNCOV
306
}
×
307

308
// Unselect does nothing for a list container.
309
func (l *ListSegment) Unselect() {
×
UNCOV
310
}
×
311

312
// ParagraphSegment wraps a number of text elements in a paragraph.
313
// It is similar to using a list of text elements when the final style is RichTextStyleParagraph.
314
//
315
// Since: 2.1
316
type ParagraphSegment struct {
317
        Texts []RichTextSegment
318
}
319

320
// Inline returns false as a paragraph should be in a block.
321
func (p *ParagraphSegment) Inline() bool {
14✔
322
        return false
14✔
323
}
14✔
324

325
// Segments returns the list of text elements in this paragraph.
326
func (p *ParagraphSegment) Segments() []RichTextSegment {
23✔
327
        return p.Texts
23✔
328
}
23✔
329

330
// Textual returns no content for a paragraph container.
UNCOV
331
func (p *ParagraphSegment) Textual() string {
×
UNCOV
332
        return ""
×
UNCOV
333
}
×
334

335
// Visual returns the no extra elements.
336
func (p *ParagraphSegment) Visual() fyne.CanvasObject {
×
UNCOV
337
        return nil
×
UNCOV
338
}
×
339

340
// Update doesn't need to change a paragraph container.
341
func (p *ParagraphSegment) Update(fyne.CanvasObject) {
×
UNCOV
342
}
×
343

344
// Select does nothing for a paragraph container.
345
func (p *ParagraphSegment) Select(_, _ fyne.Position) {
×
UNCOV
346
}
×
347

348
// SelectedText returns the empty string for this paragraph container.
349
func (p *ParagraphSegment) SelectedText() string {
×
UNCOV
350
        return ""
×
UNCOV
351
}
×
352

353
// Unselect does nothing for a paragraph container.
354
func (p *ParagraphSegment) Unselect() {
×
UNCOV
355
}
×
356

357
// SeparatorSegment includes a horizontal separator in a rich text widget.
358
//
359
// Since: 2.1
360
type SeparatorSegment struct {
361
        _ bool // Without this a pointer to SeparatorSegment will always be the same.
362
}
363

364
// Inline returns false as a separator should be full width.
UNCOV
365
func (s *SeparatorSegment) Inline() bool {
×
UNCOV
366
        return false
×
UNCOV
367
}
×
368

369
// Textual returns no content for a separator element.
370
func (s *SeparatorSegment) Textual() string {
×
UNCOV
371
        return ""
×
UNCOV
372
}
×
373

374
// Visual returns a new instance of a separator widget for this segment.
375
func (s *SeparatorSegment) Visual() fyne.CanvasObject {
×
UNCOV
376
        return NewSeparator()
×
UNCOV
377
}
×
378

379
// Update doesn't need to change a separator visual.
380
func (s *SeparatorSegment) Update(fyne.CanvasObject) {
×
UNCOV
381
}
×
382

383
// Select does nothing for a separator.
384
func (s *SeparatorSegment) Select(_, _ fyne.Position) {
×
UNCOV
385
}
×
386

387
// SelectedText returns the empty string for this separator.
388
func (s *SeparatorSegment) SelectedText() string {
×
UNCOV
389
        return "" // TODO maybe return "---\n"?
×
UNCOV
390
}
×
391

392
// Unselect does nothing for a separator.
393
func (s *SeparatorSegment) Unselect() {
×
UNCOV
394
}
×
395

396
// RichTextStyle describes the details of a text object inside a RichText widget.
397
//
398
// Since: 2.1
399
type RichTextStyle struct {
400
        Alignment fyne.TextAlign
401
        ColorName fyne.ThemeColorName
402
        Inline    bool
403
        SizeName  fyne.ThemeSizeName // The theme name of the text size to use, if blank will be the standard text size
404
        TextStyle fyne.TextStyle
405

406
        // an internal detail where we obscure password fields
407
        concealed bool
408
}
409

410
// RichTextSegment describes any element that can be rendered in a RichText widget.
411
//
412
// Since: 2.1
413
type RichTextSegment interface {
414
        Inline() bool
415
        Textual() string
416
        Update(fyne.CanvasObject)
417
        Visual() fyne.CanvasObject
418

419
        Select(pos1, pos2 fyne.Position)
420
        SelectedText() string
421
        Unselect()
422
}
423

424
// TextSegment represents the styling for a segment of rich text.
425
//
426
// Since: 2.1
427
type TextSegment struct {
428
        Style RichTextStyle
429
        Text  string
430

431
        parent *RichText
432
}
433

434
// Inline should return true if this text can be included within other elements, or false if it creates a new block.
435
func (t *TextSegment) Inline() bool {
28,189✔
436
        return t.Style.Inline
28,189✔
437
}
28,189✔
438

439
// Textual returns the content of this segment rendered to plain text.
440
func (t *TextSegment) Textual() string {
47,393✔
441
        return t.Text
47,393✔
442
}
47,393✔
443

444
// Visual returns a new instance of a graphical element required to render this segment.
445
func (t *TextSegment) Visual() fyne.CanvasObject {
1,997✔
446
        obj := canvas.NewText(t.Text, t.color())
1,997✔
447

1,997✔
448
        t.Update(obj)
1,997✔
449
        return obj
1,997✔
450
}
1,997✔
451

452
// Update applies the current state of this text segment to an existing visual.
453
func (t *TextSegment) Update(o fyne.CanvasObject) {
8,734✔
454
        obj := o.(*canvas.Text)
8,734✔
455
        obj.Text = t.Text
8,734✔
456
        obj.Color = t.color()
8,734✔
457
        obj.Alignment = t.Style.Alignment
8,734✔
458
        obj.TextStyle = t.Style.TextStyle
8,734✔
459
        obj.TextSize = t.size()
8,734✔
460
        obj.Refresh()
8,734✔
461
}
8,734✔
462

463
// Select tells the segment that the user is selecting the content between the two positions.
UNCOV
464
func (t *TextSegment) Select(begin, end fyne.Position) {
×
UNCOV
465
        // no-op: this will be added when we progress to editor
×
UNCOV
466
}
×
467

468
// SelectedText should return the text representation of any content currently selected through the Select call.
469
func (t *TextSegment) SelectedText() string {
×
UNCOV
470
        // no-op: this will be added when we progress to editor
×
UNCOV
471
        return ""
×
472
}
×
473

474
// Unselect tells the segment that the user is has cancelled the previous selection.
475
func (t *TextSegment) Unselect() {
×
UNCOV
476
        // no-op: this will be added when we progress to editor
×
UNCOV
477
}
×
478

479
func (t *TextSegment) color() color.Color {
10,731✔
480
        if t.Style.ColorName != "" {
21,408✔
481
                return theme.ColorForWidget(t.Style.ColorName, t.parent)
10,677✔
482
        }
10,677✔
483

484
        return theme.ColorForWidget(theme.ColorNameForeground, t.parent)
54✔
485
}
486

487
func (t *TextSegment) size() float32 {
27,643✔
488
        if t.Style.SizeName != "" {
55,208✔
489
                i := theme.SizeForWidget(t.Style.SizeName, t.parent)
27,565✔
490
                return i
27,565✔
491
        }
27,565✔
492

493
        i := theme.SizeForWidget(theme.SizeNameText, t.parent)
78✔
494
        return i
78✔
495
}
496

497
type richImage struct {
498
        BaseWidget
499
        align  fyne.TextAlign
500
        img    *canvas.Image
501
        oldMin fyne.Size
502
        layout *fyne.Container
503
        min    fyne.Size
504
}
505

506
func newRichImage(u fyne.URI, align fyne.TextAlign) *richImage {
1✔
507
        img := canvas.NewImageFromURI(u)
1✔
508
        img.FillMode = canvas.ImageFillOriginal
1✔
509
        i := &richImage{img: img, align: align}
1✔
510
        i.ExtendBaseWidget(i)
1✔
511
        return i
1✔
512
}
1✔
513

514
func (r *richImage) CreateRenderer() fyne.WidgetRenderer {
1✔
515
        r.layout = &fyne.Container{Layout: &richImageLayout{r}, Objects: []fyne.CanvasObject{r.img}}
1✔
516
        return NewSimpleRenderer(r.layout)
1✔
517
}
1✔
518

519
func (r *richImage) MinSize() fyne.Size {
8✔
520
        orig := r.img.MinSize()
8✔
521
        c := fyne.CurrentApp().Driver().CanvasForObject(r)
8✔
522
        if c == nil {
8✔
UNCOV
523
                return r.oldMin // not yet rendered
×
UNCOV
524
        }
×
525

526
        // unscale the image so it is not varying based on canvas
527
        w := scale.ToScreenCoordinate(c, orig.Width)
8✔
528
        h := scale.ToScreenCoordinate(c, orig.Height)
8✔
529
        // we return size / 2 as this assumes a HiDPI / 2x image scaling
8✔
530
        r.min = fyne.NewSize(float32(w)/2, float32(h)/2)
8✔
531
        return r.min
8✔
532
}
533

534
func (r *richImage) setAlign(a fyne.TextAlign) {
4✔
535
        if r.layout != nil {
7✔
536
                r.layout.Refresh()
3✔
537
        }
3✔
538
        r.align = a
4✔
539
}
540

541
type richImageLayout struct {
542
        r *richImage
543
}
544

545
func (r *richImageLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
9✔
546
        r.r.img.Resize(r.r.min)
9✔
547
        gap := float32(0)
9✔
548

9✔
549
        switch r.r.align {
9✔
550
        case fyne.TextAlignCenter:
2✔
551
                gap = (s.Width - r.r.min.Width) / 2
2✔
552
        case fyne.TextAlignTrailing:
1✔
553
                gap = s.Width - r.r.min.Width
1✔
554
        }
555

556
        r.r.img.Move(fyne.NewPos(gap, 0))
9✔
557
}
558

UNCOV
559
func (r *richImageLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
×
UNCOV
560
        return r.r.min
×
UNCOV
561
}
×
562

563
type unpadTextWidgetLayout struct {
564
        parent fyne.Widget
565
}
566

567
func (u *unpadTextWidgetLayout) Layout(o []fyne.CanvasObject, s fyne.Size) {
7✔
568
        innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent)
7✔
569
        pad := innerPad * -1
7✔
570
        pad2 := pad * -2
7✔
571

7✔
572
        o[0].Move(fyne.NewPos(pad, pad))
7✔
573
        o[0].Resize(s.Add(fyne.NewSize(pad2, pad2)))
7✔
574
}
7✔
575

576
func (u *unpadTextWidgetLayout) MinSize(o []fyne.CanvasObject) fyne.Size {
9✔
577
        innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent)
9✔
578
        pad := innerPad * 2
9✔
579
        return o[0].MinSize().Subtract(fyne.NewSize(pad, pad))
9✔
580
}
9✔
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