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

fyne-io / fyne / 26725334585

31 May 2026 09:43PM UTC coverage: 60.212% (-0.2%) from 60.377%
26725334585

Pull #6336

github

laszukdawid
Markdown: render GFM tables

Activate goldmark's Table extension and render tables through a new
TableSegment that lays its cells out as a grid, drawing grid lines and
honouring per-column alignment. Cell content is itself made of
RichTextSegments, so inline formatting (bold, code, links, strikethrough)
works inside cells. The grid is built with fyne.Container plus the public
stack/padded layouts, so the widget package gains no dependency on container.

Also make renderChildren nil-safe: the table extension's transformed nodes
can report a child count that does not match their sibling chain, which would
otherwise walk past the end of the list.
Pull Request #6336: Markdown: render GFM tables

36 of 179 new or added lines in 2 files covered. (20.11%)

92 existing lines in 2 files now uncovered.

26585 of 44152 relevant lines covered (60.21%)

674.98 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

289
// Textual returns no content for a list as the content is in sub-segments.
290
func (l *ListSegment) Textual() string {
×
291
        return ""
×
292
}
×
293

294
// Visual returns no additional elements for this segment.
295
func (l *ListSegment) Visual() fyne.CanvasObject {
×
296
        return nil
×
297
}
×
298

299
// Update doesn't need to change a list visual.
300
func (l *ListSegment) Update(fyne.CanvasObject) {
×
301
}
×
302

303
// Select does nothing for a list container.
304
func (l *ListSegment) Select(_, _ fyne.Position) {
×
305
}
×
306

307
// SelectedText returns the empty string for this list.
308
func (l *ListSegment) SelectedText() string {
×
309
        return ""
×
310
}
×
311

312
// Unselect does nothing for a list container.
313
func (l *ListSegment) Unselect() {
×
314
}
×
315

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

324
// Inline returns false as a paragraph should be in a block.
325
func (p *ParagraphSegment) Inline() bool {
29✔
326
        return false
29✔
327
}
29✔
328

329
// Segments returns the list of text elements in this paragraph.
330
func (p *ParagraphSegment) Segments() []RichTextSegment {
38✔
331
        return p.Texts
38✔
332
}
38✔
333

334
// Textual returns no content for a paragraph container.
335
func (p *ParagraphSegment) Textual() string {
×
336
        return ""
×
337
}
×
338

339
// Visual returns the no extra elements.
340
func (p *ParagraphSegment) Visual() fyne.CanvasObject {
×
341
        return nil
×
342
}
×
343

344
// Update doesn't need to change a paragraph container.
345
func (p *ParagraphSegment) Update(fyne.CanvasObject) {
×
346
}
×
347

348
// Select does nothing for a paragraph container.
349
func (p *ParagraphSegment) Select(_, _ fyne.Position) {
×
350
}
×
351

352
// SelectedText returns the empty string for this paragraph container.
353
func (p *ParagraphSegment) SelectedText() string {
×
354
        return ""
×
355
}
×
356

357
// Unselect does nothing for a paragraph container.
358
func (p *ParagraphSegment) Unselect() {
×
359
}
×
360

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

368
// Inline returns false as a separator should be full width.
369
func (s *SeparatorSegment) Inline() bool {
×
370
        return false
×
371
}
×
372

373
// Textual returns no content for a separator element.
374
func (s *SeparatorSegment) Textual() string {
×
375
        return ""
×
376
}
×
377

378
// Visual returns a new instance of a separator widget for this segment.
379
func (s *SeparatorSegment) Visual() fyne.CanvasObject {
×
380
        return NewSeparator()
×
381
}
×
382

383
// Update doesn't need to change a separator visual.
384
func (s *SeparatorSegment) Update(fyne.CanvasObject) {
×
385
}
×
386

387
// Select does nothing for a separator.
388
func (s *SeparatorSegment) Select(_, _ fyne.Position) {
×
389
}
×
390

391
// SelectedText returns the empty string for this separator.
392
func (s *SeparatorSegment) SelectedText() string {
×
393
        return "" // TODO maybe return "---\n"?
×
394
}
×
395

396
// Unselect does nothing for a separator.
397
func (s *SeparatorSegment) Unselect() {
×
398
}
×
399

400
// TableSegment represents a table within a rich text widget.
401
//
402
// Since: 2.8
403
type TableSegment struct {
404
        // Headers holds the cells of the header row, or nil for a header-less table.
405
        Headers [][]RichTextSegment
406
        // Rows holds the body rows; each row is a slice of cells, each cell a slice of segments.
407
        Rows       [][][]RichTextSegment
408
        Alignments []fyne.TextAlign
409
}
410

411
// Inline returns false as a table is a full-width block element.
NEW
UNCOV
412
func (t *TableSegment) Inline() bool {
×
NEW
UNCOV
413
        return false
×
NEW
UNCOV
414
}
×
415

416
// Textual returns the table content as tab-separated, newline-delimited text.
NEW
UNCOV
417
func (t *TableSegment) Textual() string {
×
NEW
UNCOV
418
        var b strings.Builder
×
NEW
UNCOV
419
        writeRow := func(cells [][]RichTextSegment) {
×
NEW
UNCOV
420
                for i, cell := range cells {
×
NEW
UNCOV
421
                        if i > 0 {
×
NEW
UNCOV
422
                                b.WriteByte('\t')
×
NEW
UNCOV
423
                        }
×
NEW
UNCOV
424
                        for _, s := range cell {
×
NEW
UNCOV
425
                                b.WriteString(s.Textual())
×
NEW
UNCOV
426
                        }
×
427
                }
NEW
UNCOV
428
                b.WriteByte('\n')
×
429
        }
NEW
UNCOV
430
        if t.Headers != nil {
×
NEW
UNCOV
431
                writeRow(t.Headers)
×
NEW
UNCOV
432
        }
×
NEW
UNCOV
433
        for _, r := range t.Rows {
×
NEW
UNCOV
434
                writeRow(r)
×
NEW
UNCOV
435
        }
×
NEW
UNCOV
436
        return b.String()
×
437
}
438

NEW
UNCOV
439
func (t *TableSegment) columns() int {
×
NEW
UNCOV
440
        cols := len(t.Alignments)
×
NEW
UNCOV
441
        if len(t.Headers) > cols {
×
NEW
UNCOV
442
                cols = len(t.Headers)
×
NEW
UNCOV
443
        }
×
NEW
UNCOV
444
        for _, r := range t.Rows {
×
NEW
UNCOV
445
                if len(r) > cols {
×
NEW
UNCOV
446
                        cols = len(r)
×
NEW
UNCOV
447
                }
×
448
        }
NEW
UNCOV
449
        return cols
×
450
}
451

NEW
UNCOV
452
func (t *TableSegment) alignFor(col int) fyne.TextAlign {
×
NEW
UNCOV
453
        if col < len(t.Alignments) {
×
NEW
UNCOV
454
                return t.Alignments[col]
×
NEW
UNCOV
455
        }
×
NEW
UNCOV
456
        return fyne.TextAlignLeading
×
457
}
458

459
// Visual returns a new grid laying out the table cells.
NEW
UNCOV
460
func (t *TableSegment) Visual() fyne.CanvasObject {
×
NEW
UNCOV
461
        cols := t.columns()
×
NEW
UNCOV
462
        if cols == 0 {
×
NEW
UNCOV
463
                return NewRichText()
×
NEW
UNCOV
464
        }
×
465

NEW
UNCOV
466
        objects := make([]fyne.CanvasObject, 0, cols*(len(t.Rows)+1))
×
NEW
UNCOV
467
        appendRow := func(cells [][]RichTextSegment, header bool) {
×
NEW
UNCOV
468
                for c := 0; c < cols; c++ {
×
NEW
UNCOV
469
                        var segs []RichTextSegment
×
NEW
470
                        if c < len(cells) {
×
NEW
471
                                segs = cells[c]
×
NEW
472
                        }
×
NEW
UNCOV
473
                        objects = append(objects, newTableCell(segs, t.alignFor(c), header))
×
474
                }
475
        }
NEW
476
        if t.Headers != nil {
×
NEW
477
                appendRow(t.Headers, true)
×
NEW
478
        }
×
NEW
UNCOV
479
        for _, r := range t.Rows {
×
NEW
UNCOV
480
                appendRow(r, false)
×
NEW
481
        }
×
482

NEW
483
        grid := &fyne.Container{Layout: &tableSegmentLayout{cols: cols}, Objects: objects}
×
NEW
UNCOV
484
        border := canvas.NewRectangle(theme.Color(theme.ColorNameInputBorder))
×
NEW
UNCOV
485
        return &fyne.Container{Layout: layout.NewStackLayout(), Objects: []fyne.CanvasObject{border, grid}}
×
486
}
487

488
// Update does nothing; a table visual is rebuilt rather than updated.
NEW
UNCOV
489
func (t *TableSegment) Update(fyne.CanvasObject) {
×
NEW
UNCOV
490
}
×
491

492
// Select does nothing for a table.
NEW
UNCOV
493
func (t *TableSegment) Select(_, _ fyne.Position) {
×
NEW
UNCOV
494
}
×
495

496
// SelectedText returns the table content as text.
NEW
UNCOV
497
func (t *TableSegment) SelectedText() string {
×
NEW
UNCOV
498
        return t.Textual()
×
NEW
UNCOV
499
}
×
500

501
// Unselect does nothing for a table.
NEW
UNCOV
502
func (t *TableSegment) Unselect() {
×
NEW
UNCOV
503
}
×
504

505
// newTableCell builds a single table cell: padded rich-text content over a fill,
506
// so the grid-line colour drawn behind the grid shows through the gaps left by
507
// tableSegmentLayout.
NEW
UNCOV
508
func newTableCell(segs []RichTextSegment, align fyne.TextAlign, header bool) fyne.CanvasObject {
×
NEW
UNCOV
509
        fill := theme.Color(theme.ColorNameBackground)
×
NEW
UNCOV
510
        if header {
×
NEW
UNCOV
511
                fill = theme.Color(theme.ColorNameInputBackground)
×
NEW
UNCOV
512
        }
×
NEW
UNCOV
513
        bg := canvas.NewRectangle(fill)
×
NEW
UNCOV
514

×
NEW
UNCOV
515
        cell := make([]RichTextSegment, 0, len(segs))
×
NEW
UNCOV
516
        for _, s := range segs {
×
NEW
UNCOV
517
                switch seg := s.(type) {
×
NEW
UNCOV
518
                case *TextSegment:
×
NEW
UNCOV
519
                        seg.Style.Alignment = align
×
NEW
UNCOV
520
                        if header {
×
NEW
UNCOV
521
                                seg.Style.TextStyle.Bold = true
×
NEW
UNCOV
522
                        }
×
NEW
UNCOV
523
                case *HyperlinkSegment:
×
NEW
UNCOV
524
                        seg.Alignment = align
×
525
                }
NEW
UNCOV
526
                cell = append(cell, s)
×
527
        }
NEW
UNCOV
528
        if len(cell) == 0 {
×
NEW
529
                cell = append(cell, &TextSegment{Style: RichTextStyleInline, Text: " "})
×
NEW
530
        }
×
531

NEW
UNCOV
532
        text := NewRichText(cell...)
×
NEW
UNCOV
533
        text.Wrapping = fyne.TextWrapOff
×
NEW
UNCOV
534
        padded := &fyne.Container{Layout: layout.NewPaddedLayout(), Objects: []fyne.CanvasObject{text}}
×
NEW
UNCOV
535
        return &fyne.Container{Layout: layout.NewStackLayout(), Objects: []fyne.CanvasObject{bg, padded}}
×
536
}
537

538
// tableSegmentLayout arranges cells row-major. Columns are sized to their widest
539
// cell, any slack width is shared evenly so the table fills the available width,
540
// and a one-pixel gap is left around each cell so a background drawn behind the
541
// grid shows through as grid lines.
542
type tableSegmentLayout struct {
543
        cols int
544
}
545

546
const tableSegmentGap float32 = 1
547

NEW
548
func (l *tableSegmentLayout) measure(objects []fyne.CanvasObject) (colWidths, rowHeights []float32) {
×
NEW
549
        rows := (len(objects) + l.cols - 1) / l.cols
×
NEW
550
        colWidths = make([]float32, l.cols)
×
NEW
551
        rowHeights = make([]float32, rows)
×
NEW
552
        for i, o := range objects {
×
NEW
553
                r, c := i/l.cols, i%l.cols
×
NEW
554
                m := o.MinSize()
×
NEW
555
                if m.Width > colWidths[c] {
×
NEW
556
                        colWidths[c] = m.Width
×
NEW
557
                }
×
NEW
558
                if m.Height > rowHeights[r] {
×
NEW
559
                        rowHeights[r] = m.Height
×
NEW
560
                }
×
561
        }
NEW
562
        return colWidths, rowHeights
×
563
}
564

NEW
565
func (l *tableSegmentLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
×
NEW
566
        colWidths, rowHeights := l.measure(objects)
×
NEW
567
        w := tableSegmentGap
×
NEW
568
        for _, cw := range colWidths {
×
NEW
569
                w += cw + tableSegmentGap
×
NEW
570
        }
×
NEW
571
        h := tableSegmentGap
×
NEW
572
        for _, rh := range rowHeights {
×
NEW
573
                h += rh + tableSegmentGap
×
NEW
574
        }
×
NEW
575
        return fyne.NewSize(w, h)
×
576
}
577

NEW
578
func (l *tableSegmentLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
×
NEW
579
        colWidths, rowHeights := l.measure(objects)
×
NEW
580

×
NEW
581
        minWidth := tableSegmentGap
×
NEW
582
        for _, cw := range colWidths {
×
NEW
583
                minWidth += cw + tableSegmentGap
×
NEW
584
        }
×
NEW
585
        if extra := size.Width - minWidth; extra > 0 && l.cols > 0 {
×
NEW
586
                share := extra / float32(l.cols)
×
NEW
587
                for c := range colWidths {
×
NEW
588
                        colWidths[c] += share
×
NEW
589
                }
×
590
        }
591

NEW
592
        y := tableSegmentGap
×
NEW
593
        for r, rh := range rowHeights {
×
NEW
594
                x := tableSegmentGap
×
NEW
595
                for c := 0; c < l.cols; c++ {
×
NEW
596
                        idx := r*l.cols + c
×
NEW
597
                        if idx >= len(objects) {
×
NEW
598
                                break
×
599
                        }
NEW
600
                        objects[idx].Move(fyne.NewPos(x, y))
×
NEW
601
                        objects[idx].Resize(fyne.NewSize(colWidths[c], rh))
×
NEW
602
                        x += colWidths[c] + tableSegmentGap
×
603
                }
NEW
604
                y += rh + tableSegmentGap
×
605
        }
606
}
607

608
// RichTextStyle describes the details of a text object inside a RichText widget.
609
//
610
// Since: 2.1
611
type RichTextStyle struct {
612
        Alignment fyne.TextAlign
613
        ColorName fyne.ThemeColorName
614
        Inline    bool
615
        SizeName  fyne.ThemeSizeName // The theme name of the text size to use, if blank will be the standard text size
616
        TextStyle fyne.TextStyle
617
        // Since: 2.8
618
        QuotingDepth int
619

620
        // an internal detail where we obscure password fields
621
        concealed bool
622
}
623

624
// RichTextSegment describes any element that can be rendered in a RichText widget.
625
//
626
// Since: 2.1
627
type RichTextSegment interface {
628
        Inline() bool
629
        Textual() string
630
        Update(fyne.CanvasObject)
631
        Visual() fyne.CanvasObject
632

633
        Select(pos1, pos2 fyne.Position)
634
        SelectedText() string
635
        Unselect()
636
}
637

638
// TextSegment represents the styling for a segment of rich text.
639
//
640
// Since: 2.1
641
type TextSegment struct {
642
        Style RichTextStyle
643
        Text  string
644

645
        parent *RichText
646
}
647

648
// Inline should return true if this text can be included within other elements, or false if it creates a new block.
649
func (t *TextSegment) Inline() bool {
28,226✔
650
        return t.Style.Inline
28,226✔
651
}
28,226✔
652

653
// Textual returns the content of this segment rendered to plain text.
654
func (t *TextSegment) Textual() string {
47,421✔
655
        return t.Text
47,421✔
656
}
47,421✔
657

658
// Visual returns a new instance of a graphical element required to render this segment.
659
func (t *TextSegment) Visual() fyne.CanvasObject {
2,015✔
660
        obj := canvas.NewText(t.Text, t.color())
2,015✔
661

2,015✔
662
        t.Update(obj)
2,015✔
663
        return obj
2,015✔
664
}
2,015✔
665

666
// Update applies the current state of this text segment to an existing visual.
667
func (t *TextSegment) Update(o fyne.CanvasObject) {
8,773✔
668
        obj := o.(*canvas.Text)
8,773✔
669
        obj.Text = t.Text
8,773✔
670
        obj.Color = t.color()
8,773✔
671
        obj.Alignment = t.Style.Alignment
8,773✔
672
        obj.TextStyle = t.Style.TextStyle
8,773✔
673
        obj.TextSize = t.size()
8,773✔
674
        obj.Refresh()
8,773✔
675
}
8,773✔
676

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

682
// SelectedText should return the text representation of any content currently selected through the Select call.
683
func (t *TextSegment) SelectedText() string {
×
684
        // no-op: this will be added when we progress to editor
×
685
        return ""
×
686
}
×
687

688
// Unselect tells the segment that the user is has cancelled the previous selection.
689
func (t *TextSegment) Unselect() {
×
690
        // no-op: this will be added when we progress to editor
×
691
}
×
692

693
func (t *TextSegment) color() color.Color {
10,788✔
694
        if t.Style.ColorName != "" {
21,495✔
695
                return theme.ColorForWidget(t.Style.ColorName, t.parent)
10,707✔
696
        }
10,707✔
697

698
        return theme.ColorForWidget(theme.ColorNameForeground, t.parent)
81✔
699
}
700

701
func (t *TextSegment) size() float32 {
27,691✔
702
        if t.Style.SizeName != "" {
55,284✔
703
                i := theme.SizeForWidget(t.Style.SizeName, t.parent)
27,593✔
704
                return i
27,593✔
705
        }
27,593✔
706

707
        i := theme.SizeForWidget(theme.SizeNameText, t.parent)
98✔
708
        return i
98✔
709
}
710

711
type richImage struct {
712
        BaseWidget
713
        align  fyne.TextAlign
714
        img    *canvas.Image
715
        oldMin fyne.Size
716
        layout *fyne.Container
717
        min    fyne.Size
718
}
719

720
func newRichImage(u fyne.URI, align fyne.TextAlign) *richImage {
1✔
721
        img := canvas.NewImageFromURI(u)
1✔
722
        img.FillMode = canvas.ImageFillOriginal
1✔
723
        i := &richImage{img: img, align: align}
1✔
724
        i.ExtendBaseWidget(i)
1✔
725
        return i
1✔
726
}
1✔
727

728
func (r *richImage) CreateRenderer() fyne.WidgetRenderer {
1✔
729
        r.layout = &fyne.Container{Layout: &richImageLayout{r}, Objects: []fyne.CanvasObject{r.img}}
1✔
730
        return NewSimpleRenderer(r.layout)
1✔
731
}
1✔
732

733
func (r *richImage) MinSize() fyne.Size {
8✔
734
        orig := r.img.MinSize()
8✔
735
        c := fyne.CurrentApp().Driver().CanvasForObject(r)
8✔
736
        if c == nil {
8✔
737
                return r.oldMin // not yet rendered
×
738
        }
×
739

740
        // unscale the image so it is not varying based on canvas
741
        w := scale.ToScreenCoordinate(c, orig.Width)
8✔
742
        h := scale.ToScreenCoordinate(c, orig.Height)
8✔
743
        // we return size / 2 as this assumes a HiDPI / 2x image scaling
8✔
744
        r.min = fyne.NewSize(float32(w)/2, float32(h)/2)
8✔
745
        return r.min
8✔
746
}
747

748
func (r *richImage) setAlign(a fyne.TextAlign) {
4✔
749
        if r.layout != nil {
7✔
750
                r.layout.Refresh()
3✔
751
        }
3✔
752
        r.align = a
4✔
753
}
754

755
type richImageLayout struct {
756
        r *richImage
757
}
758

759
func (r *richImageLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
9✔
760
        r.r.img.Resize(r.r.min)
9✔
761
        gap := float32(0)
9✔
762

9✔
763
        switch r.r.align {
9✔
764
        case fyne.TextAlignCenter:
2✔
765
                gap = (s.Width - r.r.min.Width) / 2
2✔
766
        case fyne.TextAlignTrailing:
1✔
767
                gap = s.Width - r.r.min.Width
1✔
768
        }
769

770
        r.r.img.Move(fyne.NewPos(gap, 0))
9✔
771
}
772

773
func (r *richImageLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
×
774
        return r.r.min
×
775
}
×
776

777
type unpadTextWidgetLayout struct {
778
        parent fyne.Widget
779
}
780

781
func (u *unpadTextWidgetLayout) Layout(o []fyne.CanvasObject, s fyne.Size) {
7✔
782
        innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent)
7✔
783
        pad := innerPad * -1
7✔
784
        pad2 := pad * -2
7✔
785

7✔
786
        o[0].Move(fyne.NewPos(pad, pad))
7✔
787
        o[0].Resize(s.Add(fyne.NewSize(pad2, pad2)))
7✔
788
}
7✔
789

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