VideoTools/vendor/fyne.io/fyne/v2/widget/selectable.go
Stu Leak 68df790d27 Fix player frame generation and video playback
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
2026-01-07 22:20:00 -05:00

416 lines
11 KiB
Go

package widget
import (
"math"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/driver/mobile"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/theme"
)
type selectable struct {
BaseWidget
cursorRow, cursorColumn int
// selectRow and selectColumn represent the selection start location
// The selection will span from selectRow/Column to CursorRow/Column -- note that the cursor
// position may occur before or after the select start position in the text.
selectRow, selectColumn int
focussed, selecting, selectEnded, password bool
sizeName fyne.ThemeSizeName
style fyne.TextStyle
provider *RichText
theme fyne.Theme
focus fyne.Focusable
// doubleTappedAtUnixMillis stores the time the entry was last DoubleTapped
// used for deciding whether the next MouseDown/TouchDown is a triple-tap or not
doubleTappedAtUnixMillis int64
}
func (s *selectable) CreateRenderer() fyne.WidgetRenderer {
return &selectableRenderer{sel: s}
}
func (s *selectable) Cursor() desktop.Cursor {
return desktop.TextCursor
}
func (s *selectable) DoubleTapped(p *fyne.PointEvent) {
s.doubleTappedAtUnixMillis = time.Now().UnixMilli()
s.updateMousePointer(p.Position)
row := s.provider.row(s.cursorRow)
start, end := getTextWhitespaceRegion(row, s.cursorColumn, false)
if start == -1 || end == -1 {
return
}
s.selectRow = s.cursorRow
s.selectColumn = start
s.cursorColumn = end
s.selecting = true
s.grabFocus()
s.Refresh()
}
func (s *selectable) DragEnd() {
if s.cursorColumn == s.selectColumn && s.cursorRow == s.selectRow {
s.selecting = false
}
shouldRefresh := !s.selecting
if shouldRefresh {
s.Refresh()
}
s.selectEnded = true
}
func (s *selectable) Dragged(d *fyne.DragEvent) {
s.dragged(d)
}
func (s *selectable) dragged(d *fyne.DragEvent) {
if !s.selecting || s.selectEnded {
s.selectEnded = false
s.updateMousePointer(d.Position)
startPos := d.Position.Subtract(d.Dragged)
s.selectRow, s.selectColumn = s.getRowCol(startPos)
s.selecting = true
s.grabFocus()
}
s.updateMousePointer(d.Position)
s.Refresh()
}
func (s *selectable) MouseDown(m *desktop.MouseEvent) {
if isTripleTap(s.doubleTappedAtUnixMillis, time.Now().UnixMilli()) {
s.selectCurrentRow(false)
return
}
s.grabFocus()
if s.selecting && m.Button == desktop.MouseButtonPrimary {
s.selecting = false
}
}
func (s *selectable) MouseUp(ev *desktop.MouseEvent) {
if ev.Button == desktop.MouseButtonSecondary {
return
}
start, _ := s.selection()
if (start == -1 || (s.selectRow == s.cursorRow && s.selectColumn == s.cursorColumn)) && s.selecting {
s.selecting = false
}
s.Refresh()
}
// SelectedText returns the text currently selected in this Entry.
// If there is no selection it will return the empty string.
func (s *selectable) SelectedText() string {
if s == nil || !s.selecting {
return ""
}
start, stop := s.selection()
if start == stop {
return ""
}
r := ([]rune)(s.provider.String())
return string(r[start:stop])
}
func (s *selectable) Tapped(*fyne.PointEvent) {
if !fyne.CurrentDevice().IsMobile() {
return
}
if s.doubleTappedAtUnixMillis != 0 {
s.doubleTappedAtUnixMillis = 0
return // was a triple (TappedDouble plus Tapped)
}
s.selecting = false
s.Refresh()
}
func (s *selectable) TappedSecondary(ev *fyne.PointEvent) {
app := fyne.CurrentApp()
c := app.Driver().CanvasForObject(s.focus.(fyne.CanvasObject))
if c == nil {
return
}
m := fyne.NewMenu("",
fyne.NewMenuItem(lang.L("Copy"), func() {
app.Clipboard().SetContent(s.SelectedText())
}))
ShowPopUpMenuAtPosition(m, c, ev.AbsolutePosition)
}
func (s *selectable) TouchCancel(m *mobile.TouchEvent) {
s.TouchUp(m)
}
func (s *selectable) TouchDown(m *mobile.TouchEvent) {
if isTripleTap(s.doubleTappedAtUnixMillis, time.Now().UnixMilli()) {
s.selectCurrentRow(true)
return
}
}
func (s *selectable) TouchUp(*mobile.TouchEvent) {
}
func (s *selectable) TypedShortcut(sh fyne.Shortcut) {
switch sh.(type) {
case *fyne.ShortcutCopy:
fyne.CurrentApp().Clipboard().SetContent(s.SelectedText())
}
}
func (s *selectable) cursorColAt(text []rune, pos fyne.Position) int {
th := s.theme
textSize := th.Size(s.getSizeName())
innerPad := th.Size(theme.SizeNameInnerPadding)
for i := 0; i < len(text); i++ {
str := string(text[0:i])
wid := fyne.MeasureText(str, textSize, s.style).Width
charWid := fyne.MeasureText(string(text[i]), textSize, s.style).Width
if pos.X < innerPad+wid+(charWid/2) {
return i
}
}
return len(text)
}
func (s *selectable) getRowCol(p fyne.Position) (int, int) {
th := s.theme
textSize := th.Size(s.getSizeName())
innerPad := th.Size(theme.SizeNameInnerPadding)
rowHeight := s.provider.charMinSize(false, s.style, textSize).Height // TODO handle Password
row := int(math.Floor(float64(p.Y-innerPad+th.Size(theme.SizeNameLineSpacing)) / float64(rowHeight)))
col := 0
if row < 0 {
row = 0
} else if row >= s.provider.rows() {
row = s.provider.rows() - 1
col = s.provider.rowLength(row)
} else {
col = s.cursorColAt(s.provider.row(row), p)
}
return row, col
}
// Selects the row where the cursorColumn is currently positioned
func (s *selectable) selectCurrentRow(focus bool) {
s.grabFocus()
provider := s.provider
s.selectRow = s.cursorRow
s.selectColumn = 0
s.cursorColumn = provider.rowLength(s.cursorRow)
s.Refresh()
}
// selection returns the start and end text positions for the selected span of text
// Note: this functionality depends on the relationship between the selection start row/col and
// the current cursor row/column.
// eg: (whitespace for clarity, '_' denotes cursor)
//
// "T e s [t i]_n g" == 3, 5
// "T e s_[t i] n g" == 3, 5
// "T e_[s t i] n g" == 2, 5
func (s *selectable) selection() (int, int) {
noSelection := !s.selecting || (s.cursorRow == s.selectRow && s.cursorColumn == s.selectColumn)
if noSelection {
return -1, -1
}
// Find the selection start
rowA, colA := s.cursorRow, s.cursorColumn
rowB, colB := s.selectRow, s.selectColumn
// Reposition if the cursors row is more than select start row, or if the row is the same and
// the cursors col is more that the select start column
if rowA > s.selectRow || (rowA == s.selectRow && colA > s.selectColumn) {
rowA, colA = s.selectRow, s.selectColumn
rowB, colB = s.cursorRow, s.cursorColumn
}
return textPosFromRowCol(rowA, colA, s.provider), textPosFromRowCol(rowB, colB, s.provider)
}
// Obtains textual position from a given row and col
// expects a read or write lock to be held by the caller
func textPosFromRowCol(row, col int, prov *RichText) int {
b := prov.rowBoundary(row)
if b == nil {
return col
}
return b.begin + col
}
func (s *selectable) updateMousePointer(p fyne.Position) {
row, col := s.getRowCol(p)
s.cursorRow, s.cursorColumn = row, col
if !s.selecting {
s.selectRow = row
s.selectColumn = col
}
}
func (s *selectable) getSizeName() fyne.ThemeSizeName {
if s.sizeName != "" {
return s.sizeName
}
return theme.SizeNameText
}
type selectableRenderer struct {
sel *selectable
selections []fyne.CanvasObject
}
func (r *selectableRenderer) Destroy() {
}
func (r *selectableRenderer) Layout(fyne.Size) {
}
func (r *selectableRenderer) MinSize() fyne.Size {
return fyne.Size{}
}
func (r *selectableRenderer) Objects() []fyne.CanvasObject {
return r.selections
}
func (r *selectableRenderer) Refresh() {
r.buildSelection()
selections := r.selections
v := fyne.CurrentApp().Settings().ThemeVariant()
selectionColor := r.sel.theme.Color(theme.ColorNameSelection, v)
for _, selection := range selections {
rect := selection.(*canvas.Rectangle)
rect.FillColor = selectionColor
if r.sel.focussed {
rect.Show()
} else {
rect.Hide()
}
}
canvas.Refresh(r.sel.impl)
}
// This process builds a slice of rectangles:
// - one entry per row of text
// - ordered by row order as they occur in multiline text
// This process could be optimized in the scenario where the user is selecting upwards:
// If the upwards case instead produces an order-reversed slice then only the newest rectangle would
// require movement and resizing. The existing solution creates a new rectangle and then moves/resizes
// all rectangles to comply with the occurrence order as stated above.
func (r *selectableRenderer) buildSelection() {
th := r.sel.theme
v := fyne.CurrentApp().Settings().ThemeVariant()
textSize := th.Size(r.sel.getSizeName())
cursorRow, cursorCol := r.sel.cursorRow, r.sel.cursorColumn
selectRow, selectCol := -1, -1
if r.sel.selecting {
selectRow = r.sel.selectRow
selectCol = r.sel.selectColumn
}
if selectRow == -1 || (cursorRow == selectRow && cursorCol == selectCol) {
r.selections = r.selections[:0]
return
}
provider := r.sel.provider
innerPad := th.Size(theme.SizeNameInnerPadding)
// Convert column, row into x,y
getCoordinates := func(column int, row int) (float32, float32) {
sz := provider.lineSizeToColumn(column, row, textSize, innerPad)
return sz.Width, sz.Height*float32(row) - th.Size(theme.SizeNameInputBorder) + innerPad
}
lineHeight := r.sel.provider.charMinSize(r.sel.password, r.sel.style, textSize).Height
minmax := func(a, b int) (int, int) {
if a < b {
return a, b
}
return b, a
}
// The remainder of the function calculates the set of boxes and add them to r.selection
selectStartRow, selectEndRow := minmax(selectRow, cursorRow)
selectStartCol, selectEndCol := minmax(selectCol, cursorCol)
if selectRow < cursorRow {
selectStartCol, selectEndCol = selectCol, cursorCol
}
if selectRow > cursorRow {
selectStartCol, selectEndCol = cursorCol, selectCol
}
rowCount := selectEndRow - selectStartRow + 1
// trim r.selection to remove unwanted old rectangles
if len(r.selections) > rowCount {
r.selections = r.selections[:rowCount]
}
// build a rectangle for each row and add it to r.selection
for i := 0; i < rowCount; i++ {
if len(r.selections) <= i {
box := canvas.NewRectangle(th.Color(theme.ColorNameSelection, v))
r.selections = append(r.selections, box)
}
// determine starting/ending columns for this rectangle
row := selectStartRow + i
startCol, endCol := selectStartCol, selectEndCol
if selectStartRow < row {
startCol = 0
}
if selectEndRow > row {
endCol = provider.rowLength(row)
}
// translate columns and row into draw coordinates
x1, y1 := getCoordinates(startCol, row)
x2, _ := getCoordinates(endCol, row)
// resize and reposition each rectangle
r.selections[i].Resize(fyne.NewSize(x2-x1+1, lineHeight))
r.selections[i].Move(fyne.NewPos(x1-1, y1))
}
}
func (s *selectable) grabFocus() {
if c := fyne.CurrentApp().Driver().CanvasForObject(s.focus.(fyne.CanvasObject)); c != nil {
c.Focus(s.focus)
}
}
func isTripleTap(double, nowMilli int64) bool {
return nowMilli-double <= fyne.CurrentApp().Driver().DoubleTapDelay().Milliseconds()
}