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

fyne-io / fyne / 30240283751

27 Jul 2026 05:37AM UTC coverage: 59.896% (-0.02%) from 59.912%
30240283751

push

github

web-flow
Merge pull request #6440 from toaster/lint/trivial_checks

enable a couple of linter checks which only require trivial adjustments

589 of 969 new or added lines in 144 files covered. (60.78%)

14 existing lines in 10 files now uncovered.

27103 of 45250 relevant lines covered (59.9%)

815.37 hits per line

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

53.49
/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/internal/widget"
13
        "fyne.io/fyne/v2/layout"
14
        "fyne.io/fyne/v2/theme"
15
)
16

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

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

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

122
        // Since 2.8
123
        TextStyle fyne.TextStyle
124
        // Since 2.8
125
        SizeName     fyne.ThemeSizeName // The theme name of the text size to use, if blank will be the standard text size
126
        quotingLevel int
127
}
128

129
// Inline returns true as hyperlinks are inside other elements.
130
func (*HyperlinkSegment) Inline() bool {
15✔
131
        return true
15✔
132
}
15✔
133

134
// Textual returns the content of this segment rendered to plain text.
135
func (h *HyperlinkSegment) Textual() string {
32✔
136
        return h.Text
32✔
137
}
32✔
138

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

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

158
// Select tells the segment that the user is selecting the content between the two positions.
NEW
159
func (*HyperlinkSegment) Select(_, _ fyne.Position) {
×
160
        // no-op: this will be added when we progress to editor
×
161
}
×
162

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

169
// Unselect tells the segment that the user is has cancelled the previous selection.
NEW
170
func (*HyperlinkSegment) Unselect() {
×
171
        // no-op: this will be added when we progress to editor
×
172
}
×
173

174
// ImageSegment represents an image within a rich text widget.
175
//
176
// Since: 2.3
177
type ImageSegment struct {
178
        Source fyne.URI
179
        Title  string
180

181
        // Alignment specifies the horizontal alignment of this image segment
182
        // Since: 2.4
183
        Alignment fyne.TextAlign
184
}
185

186
// Inline returns false as images in rich text are blocks.
187
func (*ImageSegment) Inline() bool {
8✔
188
        return false
8✔
189
}
8✔
190

191
// Textual returns the content of this segment rendered to plain text.
192
func (i *ImageSegment) Textual() string {
×
193
        return "Image " + i.Title
×
194
}
×
195

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

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

4✔
206
        // one of the following will be used
4✔
207
        img.img.File = newer.File
4✔
208
        img.img.Resource = newer.Resource
4✔
209
        img.setAlign(i.Alignment)
4✔
210

4✔
211
        img.Refresh()
4✔
212
}
4✔
213

214
// Select tells the segment that the user is selecting the content between the two positions.
NEW
215
func (*ImageSegment) Select(_, _ fyne.Position) {
×
216
        // no-op: this will be added when we progress to editor
×
217
}
×
218

219
// SelectedText should return the text representation of any content currently selected through the Select call.
NEW
220
func (*ImageSegment) SelectedText() string {
×
221
        // no-op: images have no text rendering
×
222
        return ""
×
223
}
×
224

225
// Unselect tells the segment that the user is has cancelled the previous selection.
NEW
226
func (*ImageSegment) Unselect() {
×
227
        // no-op: this will be added when we progress to editor
×
228
}
×
229

230
// ListSegment includes an itemised list with the content set using the Items field.
231
//
232
// Since: 2.1
233
type ListSegment struct {
234
        Items   []RichTextSegment
235
        Ordered bool
236

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

248
// SetStartNumber sets the starting number for an ordered list.
249
// Unordered lists are not affected.
250
//
251
// Since: 2.7
252
func (l *ListSegment) SetStartNumber(s int) {
4✔
253
        l.startIndex = s - 1
4✔
254
}
4✔
255

256
// StartNumber return the starting number for an ordered list.
257
//
258
// Since: 2.7
259
func (l *ListSegment) StartNumber() int {
14✔
260
        return l.startIndex + 1
14✔
261
}
14✔
262

263
// Inline returns false as a list should be in a block.
264
func (*ListSegment) Inline() bool {
1✔
265
        return false
1✔
266
}
1✔
267

268
// Segments returns the segments required to draw bullets before each item
269
func (l *ListSegment) Segments() []RichTextSegment {
11✔
270
        out := make([]RichTextSegment, len(l.Items))
11✔
271
        j := l.StartNumber()
11✔
272
        for i, in := range l.Items {
30✔
273
                var texts []RichTextSegment
19✔
274
                if _, ok := in.(*ListSegment); !ok {
37✔
275
                        txt := "• "
18✔
276
                        if l.Ordered {
32✔
277
                                txt = strconv.Itoa(j) + "."
14✔
278
                                j++
14✔
279
                        }
14✔
280
                        indentation := strings.Repeat(" ", l.indentationLevel*4)
18✔
281
                        style := RichTextStyleStrong
18✔
282
                        style.QuotingDepth = l.quotingLevel
18✔
283
                        bullet := &TextSegment{Text: indentation + txt + " ", Style: style}
18✔
284
                        texts = append(texts, bullet)
18✔
285
                        if _, ok := in.(*ParagraphSegment); !ok {
31✔
286
                                in = &ParagraphSegment{Texts: []RichTextSegment{in}}
13✔
287
                        }
13✔
288
                }
289
                texts = append(texts, in)
19✔
290
                out[i] = &ParagraphSegment{Texts: texts}
19✔
291
        }
292
        return out
11✔
293
}
294

295
// Textual returns no content for a list as the content is in sub-segments.
NEW
296
func (*ListSegment) Textual() string {
×
297
        return ""
×
298
}
×
299

300
// Visual returns no additional elements for this segment.
NEW
301
func (*ListSegment) Visual() fyne.CanvasObject {
×
302
        return nil
×
303
}
×
304

305
// Update doesn't need to change a list visual.
NEW
306
func (*ListSegment) Update(fyne.CanvasObject) {
×
307
}
×
308

309
// Select does nothing for a list container.
NEW
310
func (*ListSegment) Select(_, _ fyne.Position) {
×
311
}
×
312

313
// SelectedText returns the empty string for this list.
NEW
314
func (*ListSegment) SelectedText() string {
×
315
        return ""
×
316
}
×
317

318
// Unselect does nothing for a list container.
NEW
319
func (*ListSegment) Unselect() {
×
320
}
×
321

322
// ParagraphSegment wraps a number of text elements in a paragraph.
323
// It is similar to using a list of text elements when the final style is RichTextStyleParagraph.
324
//
325
// Since: 2.1
326
type ParagraphSegment struct {
327
        Texts []RichTextSegment
328
}
329

330
// Inline returns false as a paragraph should be in a block.
331
func (*ParagraphSegment) Inline() bool {
29✔
332
        return false
29✔
333
}
29✔
334

335
// Segments returns the list of text elements in this paragraph.
336
func (p *ParagraphSegment) Segments() []RichTextSegment {
38✔
337
        return p.Texts
38✔
338
}
38✔
339

340
// Textual returns no content for a paragraph container.
NEW
341
func (*ParagraphSegment) Textual() string {
×
342
        return ""
×
343
}
×
344

345
// Visual returns the no extra elements.
NEW
346
func (*ParagraphSegment) Visual() fyne.CanvasObject {
×
347
        return nil
×
348
}
×
349

350
// Update doesn't need to change a paragraph container.
NEW
351
func (*ParagraphSegment) Update(fyne.CanvasObject) {
×
352
}
×
353

354
// Select does nothing for a paragraph container.
NEW
355
func (*ParagraphSegment) Select(_, _ fyne.Position) {
×
356
}
×
357

358
// SelectedText returns the empty string for this paragraph container.
NEW
359
func (*ParagraphSegment) SelectedText() string {
×
360
        return ""
×
361
}
×
362

363
// Unselect does nothing for a paragraph container.
NEW
364
func (*ParagraphSegment) Unselect() {
×
365
}
×
366

367
// SeparatorSegment includes a horizontal separator in a rich text widget.
368
//
369
// Since: 2.1
370
type SeparatorSegment struct {
371
        _ bool // Without this a pointer to SeparatorSegment will always be the same.
372
}
373

374
// Inline returns false as a separator should be full width.
NEW
375
func (*SeparatorSegment) Inline() bool {
×
376
        return false
×
377
}
×
378

379
// Textual returns no content for a separator element.
NEW
380
func (*SeparatorSegment) Textual() string {
×
381
        return ""
×
382
}
×
383

384
// Visual returns a new instance of a separator widget for this segment.
NEW
385
func (*SeparatorSegment) Visual() fyne.CanvasObject {
×
386
        return NewSeparator()
×
387
}
×
388

389
// Update doesn't need to change a separator visual.
NEW
390
func (*SeparatorSegment) Update(fyne.CanvasObject) {
×
391
}
×
392

393
// Select does nothing for a separator.
NEW
394
func (*SeparatorSegment) Select(_, _ fyne.Position) {
×
395
}
×
396

397
// SelectedText returns the empty string for this separator.
NEW
398
func (*SeparatorSegment) SelectedText() string {
×
399
        return "" // TODO maybe return "---\n"?
×
400
}
×
401

402
// Unselect does nothing for a separator.
NEW
403
func (*SeparatorSegment) Unselect() {
×
404
}
×
405

406
// CodeBlockSegment represents a fenced or indented code block. It renders its
407
// content as monospace text on a panel, so the block stands apart from the
408
// surrounding prose.
409
//
410
// Since: 2.8
411
type CodeBlockSegment struct {
412
        Text         string
413
        quotingLevel int
414
}
415

416
// Inline returns false as a code block is a full-width block element.
417
func (*CodeBlockSegment) Inline() bool {
1✔
418
        return false
1✔
419
}
1✔
420

421
// Textual returns the raw content of this code block.
422
func (c *CodeBlockSegment) Textual() string {
×
423
        return c.Text
×
424
}
×
425

426
// Visual returns a new panel widget rendering this code block.
427
func (c *CodeBlockSegment) Visual() fyne.CanvasObject {
1✔
428
        return newRichCodeBlock(c.Text)
1✔
429
}
1✔
430

431
// Update applies the current content of this segment to an existing visual.
432
func (c *CodeBlockSegment) Update(o fyne.CanvasObject) {
×
433
        o.(*richCodeBlock).setText(c.Text)
×
434
}
×
435

436
// Select does nothing for a code block.
NEW
437
func (*CodeBlockSegment) Select(_, _ fyne.Position) {
×
438
}
×
439

440
// SelectedText returns the code block content.
441
func (c *CodeBlockSegment) SelectedText() string {
×
442
        return c.Text
×
443
}
×
444

445
// Unselect does nothing for a code block.
NEW
446
func (*CodeBlockSegment) Unselect() {
×
447
}
×
448

449
// richCodeBlock is the internal widget that draws a code block: monospace text
450
// on a rounded, bordered panel.
451
type richCodeBlock struct {
452
        BaseWidget
453
        text  string
454
        bg    *canvas.Rectangle
455
        label *Label
456
}
457

458
func newRichCodeBlock(text string) *richCodeBlock {
3✔
459
        c := &richCodeBlock{text: text}
3✔
460
        c.ExtendBaseWidget(c)
3✔
461
        return c
3✔
462
}
3✔
463

464
func (c *richCodeBlock) setText(text string) {
×
465
        c.text = text
×
466
        if c.label != nil {
×
467
                c.label.SetText(text)
×
468
        }
×
469
}
470

471
func (c *richCodeBlock) CreateRenderer() fyne.WidgetRenderer {
3✔
472
        c.bg = canvas.NewRectangle(theme.Color(theme.ColorNameInputBackground))
3✔
473
        c.bg.StrokeColor = theme.Color(theme.ColorNameInputBorder)
3✔
474
        c.bg.StrokeWidth = 1
3✔
475
        c.bg.CornerRadius = theme.Size(theme.SizeNameInputRadius)
3✔
476
        c.label = NewLabelWithStyle(c.text, fyne.TextAlignLeading, fyne.TextStyle{Monospace: true})
3✔
477
        scroll := widget.NewHScroll(c.label)
3✔
478
        cont := &fyne.Container{Layout: &richCodeBlockLayout{}, Objects: []fyne.CanvasObject{c.bg, scroll}}
3✔
479
        return NewSimpleRenderer(cont)
3✔
480
}
3✔
481

482
type richCodeBlockLayout struct{}
483

484
func (*richCodeBlockLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
2✔
485
        return objects[1].MinSize()
2✔
486
}
2✔
487

NEW
488
func (*richCodeBlockLayout) Layout(objects []fyne.CanvasObject, s fyne.Size) {
×
489
        for _, o := range objects {
×
490
                o.Move(fyne.NewPos(0, 0))
×
491
                o.Resize(s)
×
492
        }
×
493
}
494

495
// CheckBoxSegment represents checkbox (with text) in a rich text widget.
496
//
497
// Since: 2.8
498
type CheckBoxSegment struct {
499
        Checked bool
500
        Text    string
501
}
502

503
// Inline returns true as a CheckBoxSegment is usually part of a list item.
NEW
504
func (*CheckBoxSegment) Inline() bool {
×
505
        return true
×
506
}
×
507

508
// Textual returns the content of this segment rendered to plain text.
509
func (c *CheckBoxSegment) Textual() string {
×
510
        if c.Checked {
×
511
                return "[x] "
×
512
        }
×
513
        return "[ ] "
×
514
}
515

516
// Visual returns a new instance of a check widget for this segment.
517
func (c *CheckBoxSegment) Visual() fyne.CanvasObject {
×
518
        check := NewCheck(c.Text, nil)
×
519
        if c.Checked {
×
520
                check.SetChecked(true)
×
521
        }
×
522
        return &fyne.Container{Layout: &unpadTextWidgetLayout{parent: check}, Objects: []fyne.CanvasObject{check}}
×
523
}
524

525
// Update doesn't need to change a checkbox
NEW
526
func (*CheckBoxSegment) Update(fyne.CanvasObject) {
×
527
}
×
528

529
// Select does nothing for a checkbox.
NEW
530
func (*CheckBoxSegment) Select(_, _ fyne.Position) {
×
531
}
×
532

533
// SelectedText returns the empty string for a checkbox.
NEW
534
func (*CheckBoxSegment) SelectedText() string {
×
535
        return ""
×
536
}
×
537

538
// Unselect does nothing for a checkbox.
NEW
539
func (*CheckBoxSegment) Unselect() {
×
540
}
×
541

542
// TableSegment represents a table within a rich text widget.
543
//
544
// Since: 2.8
545
type TableSegment struct {
546
        // Headers holds the cells of the header row, or nil for a header-less table.
547
        Headers [][]RichTextSegment
548
        // Rows holds the body rows; each row is a slice of cells, each cell a slice of segments.
549
        Rows       [][][]RichTextSegment
550
        Alignments []fyne.TextAlign
551
}
552

553
// Inline returns false as a table is a full-width block element.
NEW
554
func (*TableSegment) Inline() bool {
×
555
        return false
×
556
}
×
557

558
// Textual returns the table content as tab-separated, newline-delimited text.
559
func (t *TableSegment) Textual() string {
×
560
        var b strings.Builder
×
561
        writeRow := func(cells [][]RichTextSegment) {
×
562
                for i, cell := range cells {
×
563
                        if i > 0 {
×
564
                                b.WriteByte('\t')
×
565
                        }
×
566
                        for _, s := range cell {
×
567
                                b.WriteString(s.Textual())
×
568
                        }
×
569
                }
570
                b.WriteByte('\n')
×
571
        }
572
        if t.Headers != nil {
×
573
                writeRow(t.Headers)
×
574
        }
×
575
        for _, r := range t.Rows {
×
576
                writeRow(r)
×
577
        }
×
578
        return b.String()
×
579
}
580

581
func (t *TableSegment) columns() int {
2✔
582
        cols := len(t.Alignments)
2✔
583
        if len(t.Headers) > cols {
2✔
584
                cols = len(t.Headers)
×
585
        }
×
586
        for _, r := range t.Rows {
4✔
587
                if len(r) > cols {
2✔
588
                        cols = len(r)
×
589
                }
×
590
        }
591
        return cols
2✔
592
}
593

594
func (t *TableSegment) alignFor(col int) fyne.TextAlign {
13✔
595
        if col < len(t.Alignments) {
25✔
596
                return t.Alignments[col]
12✔
597
        }
12✔
598
        return fyne.TextAlignLeading
1✔
599
}
600

601
// Visual returns a new grid laying out the table cells.
602
func (t *TableSegment) Visual() fyne.CanvasObject {
2✔
603
        cols := t.columns()
2✔
604
        if cols == 0 {
2✔
605
                return NewRichText()
×
606
        }
×
607

608
        objects := make([]fyne.CanvasObject, 0, cols*(len(t.Rows)+1))
2✔
609
        appendRow := func(cells [][]RichTextSegment, header bool) {
6✔
610
                for c := 0; c < cols; c++ {
12✔
611
                        var segs []RichTextSegment
8✔
612
                        if c < len(cells) {
16✔
613
                                segs = cells[c]
8✔
614
                        }
8✔
615
                        objects = append(objects, newTableCell(segs, t.alignFor(c), header))
8✔
616
                }
617
        }
618
        if t.Headers != nil {
4✔
619
                appendRow(t.Headers, true)
2✔
620
        }
2✔
621
        for _, r := range t.Rows {
4✔
622
                appendRow(r, false)
2✔
623
        }
2✔
624

625
        grid := &fyne.Container{Layout: &tableSegmentLayout{cols: cols}, Objects: objects}
2✔
626
        border := canvas.NewRectangle(theme.Color(theme.ColorNameInputBorder))
2✔
627
        return widget.NewHScroll(&fyne.Container{Layout: layout.NewStackLayout(), Objects: []fyne.CanvasObject{border, grid}})
2✔
628
}
629

630
// Update does nothing; a table visual is rebuilt rather than updated.
NEW
631
func (*TableSegment) Update(fyne.CanvasObject) {
×
632
}
×
633

634
// Select does nothing for a table.
NEW
635
func (*TableSegment) Select(_, _ fyne.Position) {
×
636
}
×
637

638
// SelectedText returns the table content as text.
639
func (t *TableSegment) SelectedText() string {
×
640
        return t.Textual()
×
641
}
×
642

643
// Unselect does nothing for a table.
NEW
644
func (*TableSegment) Unselect() {
×
645
}
×
646

647
// newTableCell builds a single table cell: padded rich-text content over a fill,
648
// so the grid-line colour drawn behind the grid shows through the gaps left by
649
// tableSegmentLayout.
650
func newTableCell(segs []RichTextSegment, align fyne.TextAlign, header bool) fyne.CanvasObject {
11✔
651
        fill := theme.Color(theme.ColorNameBackground)
11✔
652
        if header {
16✔
653
                fill = theme.Color(theme.ColorNameHeaderBackground)
5✔
654
        }
5✔
655
        bg := canvas.NewRectangle(fill)
11✔
656

11✔
657
        cell := make([]RichTextSegment, 0, len(segs))
11✔
658
        for _, s := range segs {
22✔
659
                switch seg := s.(type) {
11✔
660
                case *TextSegment:
10✔
661
                        seg.Style.Alignment = align
10✔
662
                        if header {
15✔
663
                                seg.Style.TextStyle.Bold = true
5✔
664
                        }
5✔
665
                case *HyperlinkSegment:
1✔
666
                        seg.Alignment = align
1✔
667
                }
668
                cell = append(cell, s)
11✔
669
        }
670
        if len(cell) == 0 {
11✔
671
                cell = append(cell, &TextSegment{Style: RichTextStyleInline, Text: " "})
×
672
        }
×
673

674
        text := NewRichText(cell...)
11✔
675
        text.Wrapping = fyne.TextWrapOff
11✔
676
        padded := &fyne.Container{Layout: layout.NewPaddedLayout(), Objects: []fyne.CanvasObject{text}}
11✔
677
        return &fyne.Container{Layout: layout.NewStackLayout(), Objects: []fyne.CanvasObject{bg, padded}}
11✔
678
}
679

680
// tableSegmentLayout arranges cells row-major. Columns are sized to their widest
681
// cell, any slack width is shared evenly so the table fills the available width,
682
// and a one-pixel gap is left around each cell so a background drawn behind the
683
// grid shows through as grid lines.
684
type tableSegmentLayout struct {
685
        cols int
686
}
687

688
func (l *tableSegmentLayout) measure(objects []fyne.CanvasObject) (colWidths, rowHeights []float32) {
×
689
        rows := (len(objects) + l.cols - 1) / l.cols
×
690
        colWidths = make([]float32, l.cols)
×
691
        rowHeights = make([]float32, rows)
×
692
        for i, o := range objects {
×
693
                r, c := i/l.cols, i%l.cols
×
694
                m := o.MinSize()
×
695
                if m.Width > colWidths[c] {
×
696
                        colWidths[c] = m.Width
×
697
                }
×
698
                if m.Height > rowHeights[r] {
×
699
                        rowHeights[r] = m.Height
×
700
                }
×
701
        }
702
        return colWidths, rowHeights
×
703
}
704

705
func (l *tableSegmentLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
×
706
        colWidths, rowHeights := l.measure(objects)
×
707
        gap := theme.Size(theme.SizeNameSeparatorThickness)
×
708
        w := gap
×
709
        for _, cw := range colWidths {
×
710
                w += cw + gap
×
711
        }
×
712
        h := gap
×
713
        for _, rh := range rowHeights {
×
714
                h += rh + gap
×
715
        }
×
716
        return fyne.NewSize(w, h)
×
717
}
718

719
func (l *tableSegmentLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
×
720
        colWidths, rowHeights := l.measure(objects)
×
721
        gap := theme.Size(theme.SizeNameSeparatorThickness)
×
722

×
723
        minWidth := gap
×
724
        for _, cw := range colWidths {
×
725
                minWidth += cw + gap
×
726
        }
×
727
        if extra := size.Width - minWidth; extra > 0 && l.cols > 0 {
×
728
                share := extra / float32(l.cols)
×
729
                for c := range colWidths {
×
730
                        colWidths[c] += share
×
731
                }
×
732
        }
733

734
        y := gap
×
735
        for r, rh := range rowHeights {
×
736
                x := gap
×
737
                for c := 0; c < l.cols; c++ {
×
738
                        idx := r*l.cols + c
×
739
                        if idx >= len(objects) {
×
740
                                break
×
741
                        }
742
                        objects[idx].Move(fyne.NewPos(x, y))
×
743
                        objects[idx].Resize(fyne.NewSize(colWidths[c], rh))
×
744
                        x += colWidths[c] + gap
×
745
                }
746
                y += rh + gap
×
747
        }
748
}
749

750
// RichTextStyle describes the details of a text object inside a RichText widget.
751
//
752
// Since: 2.1
753
type RichTextStyle struct {
754
        Alignment fyne.TextAlign
755
        ColorName fyne.ThemeColorName
756
        Inline    bool
757
        SizeName  fyne.ThemeSizeName // The theme name of the text size to use, if blank will be the standard text size
758
        TextStyle fyne.TextStyle
759
        // Since: 2.8
760
        QuotingDepth int
761

762
        // an internal detail where we obscure password fields
763
        concealed bool
764

765
        // an internal detail marking inline code, which renders on a background fill
766
        codeInline bool
767
}
768

769
// RichTextSegment describes any element that can be rendered in a RichText widget.
770
//
771
// Since: 2.1
772
type RichTextSegment interface {
773
        Inline() bool
774
        Textual() string
775
        Update(fyne.CanvasObject)
776
        Visual() fyne.CanvasObject
777

778
        Select(pos1, pos2 fyne.Position)
779
        SelectedText() string
780
        Unselect()
781
}
782

783
// TextSegment represents the styling for a segment of rich text.
784
//
785
// Since: 2.1
786
type TextSegment struct {
787
        Style RichTextStyle
788
        Text  string
789

790
        parent *RichText
791
}
792

793
// Inline should return true if this text can be included within other elements, or false if it creates a new block.
794
func (t *TextSegment) Inline() bool {
28,982✔
795
        return t.Style.Inline
28,982✔
796
}
28,982✔
797

798
// Textual returns the content of this segment rendered to plain text.
799
func (t *TextSegment) Textual() string {
48,690✔
800
        return t.Text
48,690✔
801
}
48,690✔
802

803
// Visual returns a new instance of a graphical element required to render this segment.
804
func (t *TextSegment) Visual() fyne.CanvasObject {
2,065✔
805
        text := canvas.NewText(t.Text, t.color())
2,065✔
806
        if t.Style.codeInline {
2,069✔
807
                bg := canvas.NewRectangle(theme.ColorForWidget(theme.ColorNameInputBackground, t.parent))
4✔
808
                c := &fyne.Container{Layout: &codeInlineLayout{}, Objects: []fyne.CanvasObject{bg, text}}
4✔
809
                t.Update(c)
4✔
810
                return c
4✔
811
        }
4✔
812

813
        t.Update(text)
2,061✔
814
        return text
2,061✔
815
}
816

817
// Update applies the current state of this text segment to an existing visual.
818
func (t *TextSegment) Update(o fyne.CanvasObject) {
8,950✔
819
        obj, ok := o.(*canvas.Text)
8,950✔
820
        if !ok { // inline code container: [background, text]
8,958✔
821
                c := o.(*fyne.Container)
8✔
822
                bg := c.Objects[0].(*canvas.Rectangle)
8✔
823
                bg.FillColor = theme.ColorForWidget(theme.ColorNameInputBackground, t.parent)
8✔
824
                bg.Refresh()
8✔
825
                obj = c.Objects[1].(*canvas.Text)
8✔
826
        }
8✔
827
        obj.Text = t.Text
8,950✔
828
        obj.Color = t.color()
8,950✔
829
        obj.Alignment = t.Style.Alignment
8,950✔
830
        obj.TextStyle = t.Style.TextStyle
8,950✔
831
        obj.TextSize = t.size()
8,950✔
832
        obj.Refresh()
8,950✔
833
}
834

835
// codeInlineLayout keeps the inline-code background tight to the text, so when
836
// the row layout stretches the container to fill trailing space the fill does
837
// not stretch with it.
838
type codeInlineLayout struct{}
839

840
func (codeInlineLayout) MinSize(o []fyne.CanvasObject) fyne.Size {
1✔
841
        return o[1].MinSize()
1✔
842
}
1✔
843

844
func (codeInlineLayout) Layout(o []fyne.CanvasObject, _ fyne.Size) {
1✔
845
        size := o[1].MinSize()
1✔
846
        for _, obj := range o {
3✔
847
                obj.Resize(size)
2✔
848
                obj.Move(fyne.NewPos(0, 0))
2✔
849
        }
2✔
850
}
851

852
// Select tells the segment that the user is selecting the content between the two positions.
NEW
853
func (*TextSegment) Select(_, _ fyne.Position) {
×
854
        // no-op: this will be added when we progress to editor
×
855
}
×
856

857
// SelectedText should return the text representation of any content currently selected through the Select call.
NEW
858
func (*TextSegment) SelectedText() string {
×
859
        // no-op: this will be added when we progress to editor
×
860
        return ""
×
861
}
×
862

863
// Unselect tells the segment that the user is has cancelled the previous selection.
NEW
864
func (*TextSegment) Unselect() {
×
865
        // no-op: this will be added when we progress to editor
×
866
}
×
867

868
func (t *TextSegment) color() color.Color {
11,015✔
869
        if t.Style.ColorName != "" {
21,949✔
870
                return theme.ColorForWidget(t.Style.ColorName, t.parent)
10,934✔
871
        }
10,934✔
872

873
        return theme.ColorForWidget(theme.ColorNameForeground, t.parent)
81✔
874
}
875

876
func (t *TextSegment) size() float32 {
28,400✔
877
        if t.Style.SizeName != "" {
56,696✔
878
                i := theme.SizeForWidget(t.Style.SizeName, t.parent)
28,296✔
879
                return i
28,296✔
880
        }
28,296✔
881

882
        i := theme.SizeForWidget(theme.SizeNameText, t.parent)
104✔
883
        return i
104✔
884
}
885

886
type richImage struct {
887
        BaseWidget
888
        align  fyne.TextAlign
889
        img    *canvas.Image
890
        oldMin fyne.Size
891
        layout *fyne.Container
892
        min    fyne.Size
893
}
894

895
func newRichImage(u fyne.URI, align fyne.TextAlign) *richImage {
1✔
896
        img := canvas.NewImageFromURI(u)
1✔
897
        img.FillMode = canvas.ImageFillOriginal
1✔
898
        i := &richImage{img: img, align: align}
1✔
899
        i.ExtendBaseWidget(i)
1✔
900
        return i
1✔
901
}
1✔
902

903
func (r *richImage) CreateRenderer() fyne.WidgetRenderer {
1✔
904
        r.layout = &fyne.Container{Layout: &richImageLayout{r}, Objects: []fyne.CanvasObject{r.img}}
1✔
905
        return NewSimpleRenderer(r.layout)
1✔
906
}
1✔
907

908
func (r *richImage) MinSize() fyne.Size {
8✔
909
        orig := r.img.MinSize()
8✔
910
        c := fyne.CurrentApp().Driver().CanvasForObject(r)
8✔
911
        if c == nil {
8✔
912
                return r.oldMin // not yet rendered
×
913
        }
×
914

915
        // unscale the image so it is not varying based on canvas
916
        w := scale.ToScreenCoordinate(c, orig.Width)
8✔
917
        h := scale.ToScreenCoordinate(c, orig.Height)
8✔
918
        // we return size / 2 as this assumes a HiDPI / 2x image scaling
8✔
919
        r.min = fyne.NewSize(float32(w)/2, float32(h)/2)
8✔
920
        return r.min
8✔
921
}
922

923
func (r *richImage) setAlign(a fyne.TextAlign) {
4✔
924
        if r.layout != nil {
7✔
925
                r.layout.Refresh()
3✔
926
        }
3✔
927
        r.align = a
4✔
928
}
929

930
type richImageLayout struct {
931
        r *richImage
932
}
933

934
func (r *richImageLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
9✔
935
        r.r.img.Resize(r.r.min)
9✔
936
        gap := float32(0)
9✔
937

9✔
938
        switch r.r.align {
9✔
939
        case fyne.TextAlignCenter:
2✔
940
                gap = (s.Width - r.r.min.Width) / 2
2✔
941
        case fyne.TextAlignTrailing:
1✔
942
                gap = s.Width - r.r.min.Width
1✔
943
        }
944

945
        r.r.img.Move(fyne.NewPos(gap, 0))
9✔
946
}
947

948
func (r *richImageLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
×
949
        return r.r.min
×
950
}
×
951

952
type unpadTextWidgetLayout struct {
953
        parent fyne.Widget
954
}
955

956
func (u *unpadTextWidgetLayout) Layout(o []fyne.CanvasObject, s fyne.Size) {
7✔
957
        innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent)
7✔
958
        pad := innerPad * -1
7✔
959
        pad2 := pad * -2
7✔
960

7✔
961
        o[0].Move(fyne.NewPos(pad, pad))
7✔
962
        o[0].Resize(s.Add(fyne.NewSize(pad2, pad2)))
7✔
963
}
7✔
964

965
func (u *unpadTextWidgetLayout) MinSize(o []fyne.CanvasObject) fyne.Size {
9✔
966
        innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent)
9✔
967
        pad := innerPad * 2
9✔
968
        return o[0].MinSize().Subtract(fyne.NewSize(pad, pad))
9✔
969
}
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc