Major improvements to UnifiedPlayer: 1. GetFrameImage() now works when paused for responsive UI updates 2. Play() method properly starts FFmpeg process 3. Frame display loop runs continuously for smooth video display 4. Disabled audio temporarily to fix video playback fundamentals 5. Simplified FFmpeg command to focus on video stream only Player now: - Generates video frames correctly - Shows video when paused - Has responsive progress tracking - Starts playback properly Next steps: Re-enable audio playback once video is stable
568 lines
15 KiB
Go
568 lines
15 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() `json:"-"`
|
|
}
|
|
|
|
// 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 a new instance of a 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{parent: link}, 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 a new instance of an 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
|
|
|
|
// startIndex is the starting number - 1 (If it is ordered). Unordered lists
|
|
// ignore startIndex.
|
|
//
|
|
// startIndex is set to start - 1 to allow the empty value of ListSegment to have a starting
|
|
// number of 1, while also allowing the caller to override the starting
|
|
// number to any int, including 0.
|
|
startIndex int
|
|
}
|
|
|
|
// SetStartNumber sets the starting number for an ordered list.
|
|
// Unordered lists are not affected.
|
|
//
|
|
// Since: 2.7
|
|
func (l *ListSegment) SetStartNumber(s int) {
|
|
l.startIndex = s - 1
|
|
}
|
|
|
|
// StartNumber return the starting number for an ordered list.
|
|
//
|
|
// Since: 2.7
|
|
func (l *ListSegment) StartNumber() int {
|
|
return l.startIndex + 1
|
|
}
|
|
|
|
// 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+l.startIndex+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 doesn't 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 doesn't 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 a new instance of a separator widget for this segment.
|
|
func (s *SeparatorSegment) Visual() fyne.CanvasObject {
|
|
return NewSeparator()
|
|
}
|
|
|
|
// Update doesn't 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 // The theme name of the text size to use, if blank will be the standard text size
|
|
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
|
|
|
|
parent *RichText
|
|
}
|
|
|
|
// 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 a new instance of a graphical element 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 theme.ColorForWidget(t.Style.ColorName, t.parent)
|
|
}
|
|
|
|
return theme.ColorForWidget(theme.ColorNameForeground, t.parent)
|
|
}
|
|
|
|
func (t *TextSegment) size() float32 {
|
|
if t.Style.SizeName != "" {
|
|
i := theme.SizeForWidget(t.Style.SizeName, t.parent)
|
|
return i
|
|
}
|
|
|
|
i := theme.SizeForWidget(theme.SizeNameText, t.parent)
|
|
return i
|
|
}
|
|
|
|
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 {
|
|
parent fyne.Widget
|
|
}
|
|
|
|
func (u *unpadTextWidgetLayout) Layout(o []fyne.CanvasObject, s fyne.Size) {
|
|
innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent)
|
|
pad := innerPad * -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 {
|
|
innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent)
|
|
pad := innerPad * 2
|
|
return o[0].MinSize().Subtract(fyne.NewSize(pad, pad))
|
|
}
|