adam-gui/vendor/fyne.io/fyne/v2/widget/richtext_objects.go
2024-04-29 19:13:50 +02:00

538 lines
14 KiB
Go

package widget
import (
"image/color"
"net/url"
"strconv"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/internal/scale"
"fyne.io/fyne/v2/theme"
)
var (
// RichTextStyleBlockquote represents a quote presented in an indented block.
//
// Since: 2.1
RichTextStyleBlockquote = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: false,
SizeName: theme.SizeNameText,
TextStyle: fyne.TextStyle{Italic: true},
}
// RichTextStyleCodeBlock represents a code blog segment.
//
// Since: 2.1
RichTextStyleCodeBlock = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: false,
SizeName: theme.SizeNameText,
TextStyle: fyne.TextStyle{Monospace: true},
}
// RichTextStyleCodeInline represents an inline code segment.
//
// Since: 2.1
RichTextStyleCodeInline = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: true,
SizeName: theme.SizeNameText,
TextStyle: fyne.TextStyle{Monospace: true},
}
// RichTextStyleEmphasis represents regular text with emphasis.
//
// Since: 2.1
RichTextStyleEmphasis = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: true,
SizeName: theme.SizeNameText,
TextStyle: fyne.TextStyle{Italic: true},
}
// RichTextStyleHeading represents a heading text that stands on its own line.
//
// Since: 2.1
RichTextStyleHeading = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: false,
SizeName: theme.SizeNameHeadingText,
TextStyle: fyne.TextStyle{Bold: true},
}
// RichTextStyleInline represents standard text that can be surrounded by other elements.
//
// Since: 2.1
RichTextStyleInline = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: true,
SizeName: theme.SizeNameText,
}
// RichTextStyleParagraph represents standard text that should appear separate from other text.
//
// Since: 2.1
RichTextStyleParagraph = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: false,
SizeName: theme.SizeNameText,
}
// RichTextStylePassword represents standard sized text where the characters are obscured.
//
// Since: 2.1
RichTextStylePassword = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: true,
SizeName: theme.SizeNameText,
concealed: true,
}
// RichTextStyleStrong represents regular text with a strong emphasis.
//
// Since: 2.1
RichTextStyleStrong = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: true,
SizeName: theme.SizeNameText,
TextStyle: fyne.TextStyle{Bold: true},
}
// RichTextStyleSubHeading represents a sub-heading text that stands on its own line.
//
// Since: 2.1
RichTextStyleSubHeading = RichTextStyle{
ColorName: theme.ColorNameForeground,
Inline: false,
SizeName: theme.SizeNameSubHeadingText,
TextStyle: fyne.TextStyle{Bold: true},
}
)
// HyperlinkSegment represents a hyperlink within a rich text widget.
//
// Since: 2.1
type HyperlinkSegment struct {
Alignment fyne.TextAlign
Text string
URL *url.URL
// OnTapped overrides the default `fyne.OpenURL` call when the link is tapped
//
// Since: 2.4
OnTapped func()
}
// Inline returns true as hyperlinks are inside other elements.
func (h *HyperlinkSegment) Inline() bool {
return true
}
// Textual returns the content of this segment rendered to plain text.
func (h *HyperlinkSegment) Textual() string {
return h.Text
}
// Visual returns the hyperlink widget required to render this segment.
func (h *HyperlinkSegment) Visual() fyne.CanvasObject {
link := NewHyperlink(h.Text, h.URL)
link.Alignment = h.Alignment
link.OnTapped = h.OnTapped
return &fyne.Container{Layout: &unpadTextWidgetLayout{}, Objects: []fyne.CanvasObject{link}}
}
// Update applies the current state of this hyperlink segment to an existing visual.
func (h *HyperlinkSegment) Update(o fyne.CanvasObject) {
link := o.(*fyne.Container).Objects[0].(*Hyperlink)
link.Text = h.Text
link.URL = h.URL
link.Alignment = h.Alignment
link.OnTapped = h.OnTapped
link.Refresh()
}
// Select tells the segment that the user is selecting the content between the two positions.
func (h *HyperlinkSegment) Select(begin, end fyne.Position) {
// no-op: this will be added when we progress to editor
}
// SelectedText should return the text representation of any content currently selected through the Select call.
func (h *HyperlinkSegment) SelectedText() string {
// no-op: this will be added when we progress to editor
return ""
}
// Unselect tells the segment that the user is has cancelled the previous selection.
func (h *HyperlinkSegment) Unselect() {
// no-op: this will be added when we progress to editor
}
// ImageSegment represents an image within a rich text widget.
//
// Since: 2.3
type ImageSegment struct {
Source fyne.URI
Title string
// Alignment specifies the horizontal alignment of this image segment
// Since: 2.4
Alignment fyne.TextAlign
}
// Inline returns false as images in rich text are blocks.
func (i *ImageSegment) Inline() bool {
return false
}
// Textual returns the content of this segment rendered to plain text.
func (i *ImageSegment) Textual() string {
return "Image " + i.Title
}
// Visual returns the image widget required to render this segment.
func (i *ImageSegment) Visual() fyne.CanvasObject {
return newRichImage(i.Source, i.Alignment)
}
// Update applies the current state of this image segment to an existing visual.
func (i *ImageSegment) Update(o fyne.CanvasObject) {
newer := canvas.NewImageFromURI(i.Source)
img := o.(*richImage)
// one of the following will be used
img.img.File = newer.File
img.img.Resource = newer.Resource
img.setAlign(i.Alignment)
img.Refresh()
}
// Select tells the segment that the user is selecting the content between the two positions.
func (i *ImageSegment) Select(begin, end fyne.Position) {
// no-op: this will be added when we progress to editor
}
// SelectedText should return the text representation of any content currently selected through the Select call.
func (i *ImageSegment) SelectedText() string {
// no-op: images have no text rendering
return ""
}
// Unselect tells the segment that the user is has cancelled the previous selection.
func (i *ImageSegment) Unselect() {
// no-op: this will be added when we progress to editor
}
// ListSegment includes an itemised list with the content set using the Items field.
//
// Since: 2.1
type ListSegment struct {
Items []RichTextSegment
Ordered bool
}
// Inline returns false as a list should be in a block.
func (l *ListSegment) Inline() bool {
return false
}
// Segments returns the segments required to draw bullets before each item
func (l *ListSegment) Segments() []RichTextSegment {
out := make([]RichTextSegment, len(l.Items))
for i, in := range l.Items {
txt := "• "
if l.Ordered {
txt = strconv.Itoa(i+1) + "."
}
bullet := &TextSegment{Text: txt + " ", Style: RichTextStyleStrong}
out[i] = &ParagraphSegment{Texts: []RichTextSegment{
bullet,
in,
}}
}
return out
}
// Textual returns no content for a list as the content is in sub-segments.
func (l *ListSegment) Textual() string {
return ""
}
// Visual returns no additional elements for this segment.
func (l *ListSegment) Visual() fyne.CanvasObject {
return nil
}
// Update doesnt need to change a list visual.
func (l *ListSegment) Update(fyne.CanvasObject) {
}
// Select does nothing for a list container.
func (l *ListSegment) Select(_, _ fyne.Position) {
}
// SelectedText returns the empty string for this list.
func (l *ListSegment) SelectedText() string {
return ""
}
// Unselect does nothing for a list container.
func (l *ListSegment) Unselect() {
}
// ParagraphSegment wraps a number of text elements in a paragraph.
// It is similar to using a list of text elements when the final style is RichTextStyleParagraph.
//
// Since: 2.1
type ParagraphSegment struct {
Texts []RichTextSegment
}
// Inline returns false as a paragraph should be in a block.
func (p *ParagraphSegment) Inline() bool {
return false
}
// Segments returns the list of text elements in this paragraph.
func (p *ParagraphSegment) Segments() []RichTextSegment {
return p.Texts
}
// Textual returns no content for a paragraph container.
func (p *ParagraphSegment) Textual() string {
return ""
}
// Visual returns the no extra elements.
func (p *ParagraphSegment) Visual() fyne.CanvasObject {
return nil
}
// Update doesnt need to change a paragraph container.
func (p *ParagraphSegment) Update(fyne.CanvasObject) {
}
// Select does nothing for a paragraph container.
func (p *ParagraphSegment) Select(_, _ fyne.Position) {
}
// SelectedText returns the empty string for this paragraph container.
func (p *ParagraphSegment) SelectedText() string {
return ""
}
// Unselect does nothing for a paragraph container.
func (p *ParagraphSegment) Unselect() {
}
// SeparatorSegment includes a horizontal separator in a rich text widget.
//
// Since: 2.1
type SeparatorSegment struct {
_ bool // Without this a pointer to SeparatorSegment will always be the same.
}
// Inline returns false as a separator should be full width.
func (s *SeparatorSegment) Inline() bool {
return false
}
// Textual returns no content for a separator element.
func (s *SeparatorSegment) Textual() string {
return ""
}
// Visual returns the separator element for this segment.
func (s *SeparatorSegment) Visual() fyne.CanvasObject {
return NewSeparator()
}
// Update doesnt need to change a separator visual.
func (s *SeparatorSegment) Update(fyne.CanvasObject) {
}
// Select does nothing for a separator.
func (s *SeparatorSegment) Select(_, _ fyne.Position) {
}
// SelectedText returns the empty string for this separator.
func (s *SeparatorSegment) SelectedText() string {
return "" // TODO maybe return "---\n"?
}
// Unselect does nothing for a separator.
func (s *SeparatorSegment) Unselect() {
}
// RichTextStyle describes the details of a text object inside a RichText widget.
//
// Since: 2.1
type RichTextStyle struct {
Alignment fyne.TextAlign
ColorName fyne.ThemeColorName
Inline bool
SizeName fyne.ThemeSizeName
TextStyle fyne.TextStyle
// an internal detail where we obscure password fields
concealed bool
}
// RichTextSegment describes any element that can be rendered in a RichText widget.
//
// Since: 2.1
type RichTextSegment interface {
Inline() bool
Textual() string
Update(fyne.CanvasObject)
Visual() fyne.CanvasObject
Select(pos1, pos2 fyne.Position)
SelectedText() string
Unselect()
}
// TextSegment represents the styling for a segment of rich text.
//
// Since: 2.1
type TextSegment struct {
Style RichTextStyle
Text string
}
// Inline should return true if this text can be included within other elements, or false if it creates a new block.
func (t *TextSegment) Inline() bool {
return t.Style.Inline
}
// Textual returns the content of this segment rendered to plain text.
func (t *TextSegment) Textual() string {
return t.Text
}
// Visual returns the graphical elements required to render this segment.
func (t *TextSegment) Visual() fyne.CanvasObject {
obj := canvas.NewText(t.Text, t.color())
t.Update(obj)
return obj
}
// Update applies the current state of this text segment to an existing visual.
func (t *TextSegment) Update(o fyne.CanvasObject) {
obj := o.(*canvas.Text)
obj.Text = t.Text
obj.Color = t.color()
obj.Alignment = t.Style.Alignment
obj.TextStyle = t.Style.TextStyle
obj.TextSize = t.size()
obj.Refresh()
}
// Select tells the segment that the user is selecting the content between the two positions.
func (t *TextSegment) Select(begin, end fyne.Position) {
// no-op: this will be added when we progress to editor
}
// SelectedText should return the text representation of any content currently selected through the Select call.
func (t *TextSegment) SelectedText() string {
// no-op: this will be added when we progress to editor
return ""
}
// Unselect tells the segment that the user is has cancelled the previous selection.
func (t *TextSegment) Unselect() {
// no-op: this will be added when we progress to editor
}
func (t *TextSegment) color() color.Color {
if t.Style.ColorName != "" {
return fyne.CurrentApp().Settings().Theme().Color(t.Style.ColorName, fyne.CurrentApp().Settings().ThemeVariant())
}
return theme.ForegroundColor()
}
func (t *TextSegment) size() float32 {
if t.Style.SizeName != "" {
return fyne.CurrentApp().Settings().Theme().Size(t.Style.SizeName)
}
return theme.TextSize()
}
type richImage struct {
BaseWidget
align fyne.TextAlign
img *canvas.Image
oldMin fyne.Size
layout *fyne.Container
min fyne.Size
}
func newRichImage(u fyne.URI, align fyne.TextAlign) *richImage {
img := canvas.NewImageFromURI(u)
img.FillMode = canvas.ImageFillOriginal
i := &richImage{img: img, align: align}
i.ExtendBaseWidget(i)
return i
}
func (r *richImage) CreateRenderer() fyne.WidgetRenderer {
r.layout = &fyne.Container{Layout: &richImageLayout{r}, Objects: []fyne.CanvasObject{r.img}}
return NewSimpleRenderer(r.layout)
}
func (r *richImage) MinSize() fyne.Size {
orig := r.img.MinSize()
c := fyne.CurrentApp().Driver().CanvasForObject(r)
if c == nil {
return r.oldMin // not yet rendered
}
// unscale the image so it is not varying based on canvas
w := scale.ToScreenCoordinate(c, orig.Width)
h := scale.ToScreenCoordinate(c, orig.Height)
// we return size / 2 as this assumes a HiDPI / 2x image scaling
r.min = fyne.NewSize(float32(w)/2, float32(h)/2)
return r.min
}
func (r *richImage) setAlign(a fyne.TextAlign) {
if r.layout != nil {
r.layout.Refresh()
}
r.align = a
}
type richImageLayout struct {
r *richImage
}
func (r *richImageLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
r.r.img.Resize(r.r.min)
gap := float32(0)
switch r.r.align {
case fyne.TextAlignCenter:
gap = (s.Width - r.r.min.Width) / 2
case fyne.TextAlignTrailing:
gap = s.Width - r.r.min.Width
}
r.r.img.Move(fyne.NewPos(gap, 0))
}
func (r *richImageLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
return r.r.min
}
type unpadTextWidgetLayout struct {
}
func (u *unpadTextWidgetLayout) Layout(o []fyne.CanvasObject, s fyne.Size) {
pad := theme.InnerPadding() * -1
pad2 := pad * -2
o[0].Move(fyne.NewPos(pad, pad))
o[0].Resize(s.Add(fyne.NewSize(pad2, pad2)))
}
func (u *unpadTextWidgetLayout) MinSize(o []fyne.CanvasObject) fyne.Size {
pad := theme.InnerPadding() * 2
return o[0].MinSize().Subtract(fyne.NewSize(pad, pad))
}