package widgets import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) type MultiHyperlink struct { widget.BaseWidget Segments []MultiHyperlinkSegment OnTapped func(string) minSegWidthCached float32 minHeightCached float32 separatorWCached float32 // TODO: Once https://github.com/fyne-io/fyne/issues/4336 is resolved, // we can switch to the much cleaner RichText implementation //provider *widget.RichText content *fyne.Container } type MultiHyperlinkSegment struct { Text string LinkID string } func NewMultiHyperlink() *MultiHyperlink { c := &MultiHyperlink{ //provider: widget.NewRichText(), content: container.NewWithoutLayout(), } c.ExtendBaseWidget(c) //c.provider.Truncation = fyne.TextTruncateEllipsis return c } func (m *MultiHyperlink) BuildSegments(texts, links []string) { l := len(links) m.Segments = nil for i, text := range texts { link := "" if l > i { link = links[i] } m.Segments = append(m.Segments, MultiHyperlinkSegment{Text: text, LinkID: link}) } } func (c *MultiHyperlink) getMinSegWidth() float32 { if c.minSegWidthCached == 0 { c.minSegWidthCached = fyne.MeasureText(", W", theme.TextSize(), fyne.TextStyle{}).Width } return c.minSegWidthCached } func (c *MultiHyperlink) getSeparatorWidth() float32 { if c.separatorWCached == 0 { c.separatorWCached = fyne.MeasureText(",", theme.TextSize(), fyne.TextStyle{}).Width } return c.separatorWCached } func (c *MultiHyperlink) layoutObjects() { if len(c.Segments) == 0 { c.content.RemoveAll() return } x := float32(0) width := c.Size().Width l := len(c.content.Objects) var i int // at end of loop should be index of last seg that was laid out for display var seg MultiHyperlinkSegment for i, seg = range c.Segments { if i > 0 { // check if we have enough room to show another segment if x+c.getMinSegWidth() > width { i -= 1 // we are not showing this segment break } } appendingSegments := 2*i >= l var obj fyne.CanvasObject if !appendingSegments { obj = c.content.Objects[2*i] } else if i > 0 { c.content.Objects = append(c.content.Objects, c.newSeparatorLabel()) } if seg.LinkID == "" { obj = c.updateOrReplaceLabel(obj, seg.Text) } else { obj = c.updateOrReplaceHyperlink(obj, seg.Text, seg.LinkID) } if appendingSegments { c.content.Objects = append(c.content.Objects, obj) } else { c.content.Objects[2*i] = obj } if i > 0 { // move and resize separator obj = c.content.Objects[2*i-1] ms := obj.MinSize() obj.Resize(ms) obj.Move(fyne.NewPos(x-ms.Width+c.getSeparatorWidth()+1, 0)) // this is really ugly x -= theme.Padding() * 2 } // move and resize text object // extra +3 to textW gives it just enough space to not trigger ellipsis truncation textW := fyne.MeasureText(seg.Text, theme.TextSize(), fyne.TextStyle{}).Width + theme.Padding()*2 + theme.InnerPadding() + 3 obj = c.content.Objects[2*i] ms := obj.MinSize() w := fyne.Min(width-x, textW) obj.Resize(fyne.NewSize(w, ms.Height)) obj.Move(fyne.NewPos(x, 0)) x += w } i += 1 c.content.Objects = c.content.Objects[:2*i-1] } func (c *MultiHyperlink) updateOrReplaceLabel(obj fyne.CanvasObject, text string) fyne.CanvasObject { if obj != nil { if label, ok := obj.(*widget.Label); ok { label.Text = text return label } } l := widget.NewLabel(text) l.Truncation = fyne.TextTruncateEllipsis return l } func (c *MultiHyperlink) updateOrReplaceHyperlink(obj fyne.CanvasObject, text, link string) fyne.CanvasObject { if obj != nil { if l, ok := obj.(*widget.Hyperlink); ok { l.Text = text l.OnTapped = func() { c.onSegmentTapped(link) } return l } } l := widget.NewHyperlink(text, nil) l.Truncation = fyne.TextTruncateEllipsis l.OnTapped = func() { c.onSegmentTapped(link) } return l } func (c *MultiHyperlink) newSeparatorLabel() *widget.Label { return widget.NewLabel(", ") } /*** * RichText implementation func (c *MultiHyperlink) syncSegments() { l := len(c.provider.Segments) for i, seg := range c.Segments { appendingSegments := 2*i >= l // true if we need to extend the RichText provider with new segments var rtSeg widget.RichTextSegment if !appendingSegments { rtSeg = c.provider.Segments[2*i] } else if i > 0 { // append new separator segment c.provider.Segments = append(c.provider.Segments, c.newSeparatorSegment()) } if seg.LinkID == "" { rtSeg = c.updateOrReplaceTextSegment(rtSeg, seg.Text) } else { rtSeg = c.updateOrReplaceHyperlinkSegment(rtSeg, seg.Text, seg.LinkID) } if appendingSegments { c.provider.Segments = append(c.provider.Segments, rtSeg) } else { c.provider.Segments[2*i] = rtSeg } } // discard extra segments if shortening the multihyperlink for i := 2 * len(c.Segments); i < l; i++ { c.provider.Segments[i] = nil } c.provider.Segments = c.provider.Segments[:2*len(c.Segments)-1] } func (c *MultiHyperlink) newSeparatorSegment() widget.RichTextSegment { return &widget.TextSegment{Text: ", ", Style: widget.RichTextStyle{Inline: true}} } func (c *MultiHyperlink) updateOrReplaceTextSegment(seg widget.RichTextSegment, text string) widget.RichTextSegment { if seg != nil { if ts, ok := seg.(*widget.TextSegment); ok { ts.Text = text return seg } } return &widget.TextSegment{Text: text, Style: widget.RichTextStyle{Inline: true}} } func (c *MultiHyperlink) updateOrReplaceHyperlinkSegment(seg widget.RichTextSegment, text, linkID string) widget.RichTextSegment { if seg != nil { if ts, ok := seg.(*widget.HyperlinkSegment); ok { ts.Text = text // TODO: OnTapped return seg } } return &widget.HyperlinkSegment{Text: text} // TODO: OnTapped } */ func (c *MultiHyperlink) onSegmentTapped(linkID string) { if c.OnTapped != nil { c.OnTapped(linkID) } } func (c *MultiHyperlink) MinSize() fyne.Size { if c.minHeightCached == 0 { c.minHeightCached = widget.NewLabel("").MinSize().Height } return fyne.NewSize(1, c.minHeightCached) } func (c *MultiHyperlink) Resize(size fyne.Size) { c.BaseWidget.Resize(size) c.layoutObjects() } func (c *MultiHyperlink) Refresh() { //c.syncSegments() c.layoutObjects() c.BaseWidget.Refresh() } func (c *MultiHyperlink) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(c.content) //return widget.NewSimpleRenderer(c.provider) }