VideoTools/vendor/fyne.io/fyne/v2/widget/table.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

1741 lines
44 KiB
Go

package widget
import (
"math"
"strconv"
"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/internal/async"
"fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/theme"
)
const noCellMatch = math.MaxInt
var (
// allTableCellsID represents all table cells when refreshing requested cells
allTableCellsID = TableCellID{-1, -1}
// onlyNewTableCellsID represents newly visible table cells when refreshing requested cells
onlyNewTableCellsID = TableCellID{-2, -2}
)
// Declare conformity with interfaces
var (
_ desktop.Cursorable = (*Table)(nil)
_ fyne.Draggable = (*Table)(nil)
_ fyne.Focusable = (*Table)(nil)
_ desktop.Hoverable = (*Table)(nil)
_ fyne.Tappable = (*Table)(nil)
_ fyne.Widget = (*Table)(nil)
)
// TableCellID is a type that represents a cell's position in a table based on its row and column location.
type TableCellID struct {
Row int
Col int
}
// Table widget is a grid of items that can be scrolled and a cell selected.
// Its performance is provided by caching cell templates created with CreateCell and re-using them with UpdateCell.
// The size of the content rows/columns is returned by the Length callback.
//
// Since: 1.4
type Table struct {
BaseWidget
Length func() (rows int, cols int) `json:"-"`
CreateCell func() fyne.CanvasObject `json:"-"`
UpdateCell func(id TableCellID, template fyne.CanvasObject) `json:"-"`
OnSelected func(id TableCellID) `json:"-"`
OnUnselected func(id TableCellID) `json:"-"`
// ShowHeaderRow specifies that a row should be added to the table with header content.
// This will default to an A-Z style content, unless overridden with `CreateHeader` and `UpdateHeader` calls.
//
// Since: 2.4
ShowHeaderRow bool
// ShowHeaderColumn specifies that a column should be added to the table with header content.
// This will default to an 1-10 style numeric content, unless overridden with `CreateHeader` and `UpdateHeader` calls.
//
// Since: 2.4
ShowHeaderColumn bool
// CreateHeader is an optional function that allows overriding of the default header widget.
// Developers must also override `UpdateHeader`.
//
// Since: 2.4
CreateHeader func() fyne.CanvasObject `json:"-"`
// UpdateHeader is used with `CreateHeader` to support custom header content.
// The `id` parameter will have `-1` value to indicate a header, and `> 0` where the column or row refer to data.
//
// Since: 2.4
UpdateHeader func(id TableCellID, template fyne.CanvasObject) `json:"-"`
// StickyRowCount specifies how many data rows should not scroll when the content moves.
// If `ShowHeaderRow` us `true` then the stuck row will appear immediately underneath.
//
// Since: 2.4
StickyRowCount int
// StickyColumnCount specifies how many data columns should not scroll when the content moves.
// If `ShowHeaderColumn` us `true` then the stuck column will appear immediately next to the header.
//
// Since: 2.4
StickyColumnCount int
// HideSeparators hides the separator lines between the table cells
//
// Since: 2.5
HideSeparators bool
currentFocus TableCellID
focused bool
selectedCell, hoveredCell *TableCellID
cells *tableCells
columnWidths, rowHeights map[int]float32
moveCallback func()
offset fyne.Position
content *widget.Scroll
cellSize, headerSize fyne.Size
stuckXOff, stuckYOff, stuckWidth, stuckHeight, dragStartSize float32
top, left, corner, dividerLayer *clip
hoverHeaderRow, hoverHeaderCol, dragCol, dragRow int
dragStartPos fyne.Position
}
// NewTable returns a new performant table widget defined by the passed functions.
// The first returns the data size in rows and columns, second parameter is a function that returns cell
// template objects that can be cached and the third is used to apply data at specified data location to the
// passed template CanvasObject.
//
// Since: 1.4
func NewTable(length func() (rows int, cols int), create func() fyne.CanvasObject, update func(TableCellID, fyne.CanvasObject)) *Table {
t := &Table{Length: length, CreateCell: create, UpdateCell: update}
t.ExtendBaseWidget(t)
return t
}
// NewTableWithHeaders returns a new performant table widget defined by the passed functions including sticky headers.
// The first returns the data size in rows and columns, second parameter is a function that returns cell
// template objects that can be cached and the third is used to apply data at specified data location to the
// passed template CanvasObject.
// The row and column headers will stick to the leading and top edges of the table and contain "1-10" and "A-Z" formatted labels.
//
// Since: 2.4
func NewTableWithHeaders(length func() (rows int, cols int), create func() fyne.CanvasObject, update func(TableCellID, fyne.CanvasObject)) *Table {
t := NewTable(length, create, update)
t.ShowHeaderRow = true
t.ShowHeaderColumn = true
return t
}
// CreateRenderer returns a new renderer for the table.
func (t *Table) CreateRenderer() fyne.WidgetRenderer {
t.ExtendBaseWidget(t)
t.headerSize = t.createHeader().MinSize()
if t.columnWidths != nil {
if v, ok := t.columnWidths[-1]; ok {
t.headerSize.Width = v
}
}
if t.rowHeights != nil {
if v, ok := t.rowHeights[-1]; ok {
t.headerSize.Height = v
}
}
t.cellSize = t.templateSize()
t.cells = newTableCells(t)
t.content = widget.NewScroll(t.cells)
t.top = newClip(t, &fyne.Container{})
t.left = newClip(t, &fyne.Container{})
t.corner = newClip(t, &fyne.Container{})
t.dividerLayer = newClip(t, &fyne.Container{})
t.dragCol = noCellMatch
t.dragRow = noCellMatch
r := &tableRenderer{t: t}
r.SetObjects([]fyne.CanvasObject{t.top, t.left, t.corner, t.dividerLayer, t.content})
t.content.OnScrolled = func(pos fyne.Position) {
t.offset = pos
t.cells.refreshForID(onlyNewTableCellsID)
}
r.Layout(t.Size())
return r
}
func (t *Table) Cursor() desktop.Cursor {
if t.hoverHeaderRow != noCellMatch {
return desktop.VResizeCursor
} else if t.hoverHeaderCol != noCellMatch {
return desktop.HResizeCursor
}
return desktop.DefaultCursor
}
func (t *Table) Dragged(e *fyne.DragEvent) {
min := t.cellSize
col := t.dragCol
row := t.dragRow
startPos := t.dragStartPos
startSize := t.dragStartSize
if col != noCellMatch {
newSize := startSize + (e.Position.X - startPos.X)
if newSize < min.Width {
newSize = min.Width
}
t.SetColumnWidth(t.dragCol, newSize)
}
if row != noCellMatch {
newSize := startSize + (e.Position.Y - startPos.Y)
if newSize < min.Height {
newSize = min.Height
}
t.SetRowHeight(t.dragRow, newSize)
}
}
func (t *Table) DragEnd() {
t.dragCol = noCellMatch
t.dragRow = noCellMatch
}
// FocusGained is called after this table has gained focus.
func (t *Table) FocusGained() {
t.focused = true
t.RefreshItem(t.currentFocus)
}
// FocusLost is called after this Table has lost focus.
func (t *Table) FocusLost() {
t.focused = false
t.Refresh() // Item(t.currentFocus)
}
func (t *Table) MouseIn(ev *desktop.MouseEvent) {
t.hoverAt(ev.Position)
}
// MouseDown response to desktop mouse event
func (t *Table) MouseDown(e *desktop.MouseEvent) {
t.tapped(e.Position)
}
func (t *Table) MouseMoved(ev *desktop.MouseEvent) {
t.hoverAt(ev.Position)
}
func (t *Table) MouseOut() {
t.hoverOut()
}
// MouseUp response to desktop mouse event
func (t *Table) MouseUp(*desktop.MouseEvent) {
}
// RefreshItem refreshes a single item, specified by the item ID passed in.
//
// Since: 2.4
func (t *Table) RefreshItem(id TableCellID) {
if t.cells == nil {
return
}
t.cells.refreshForID(id)
}
// Select will mark the specified cell as selected.
func (t *Table) Select(id TableCellID) {
if t.Length == nil {
return
}
rows, cols := t.Length()
if id.Row < 0 || id.Row >= rows || id.Col < 0 || id.Col >= cols {
return
}
if t.selectedCell != nil && *t.selectedCell == id {
return
}
if f := t.OnUnselected; f != nil && t.selectedCell != nil {
f(*t.selectedCell)
}
t.selectedCell = &id
t.currentFocus = id
t.ScrollTo(id)
if f := t.OnSelected; f != nil {
f(id)
}
}
// SetColumnWidth supports changing the width of the specified column. Columns normally take the width of the template
// cell returned from the CreateCell callback. The width parameter uses the same units as a fyne.Size type and refers
// to the internal content width not including the divider size.
//
// Since: 1.4.1
func (t *Table) SetColumnWidth(id int, width float32) {
if id < 0 {
if t.headerSize.Width == width {
return
}
t.headerSize.Width = width
}
if t.columnWidths == nil {
t.columnWidths = make(map[int]float32)
}
if set, ok := t.columnWidths[id]; ok && set == width {
return
}
t.columnWidths[id] = width
t.Refresh()
}
// SetRowHeight supports changing the height of the specified row. Rows normally take the height of the template
// cell returned from the CreateCell callback. The height parameter uses the same units as a fyne.Size type and refers
// to the internal content height not including the divider size.
//
// Since: 2.3
func (t *Table) SetRowHeight(id int, height float32) {
if id < 0 {
if t.headerSize.Height == height {
return
}
t.headerSize.Height = height
}
if t.rowHeights == nil {
t.rowHeights = make(map[int]float32)
}
if set, ok := t.rowHeights[id]; ok && set == height {
return
}
t.rowHeights[id] = height
t.Refresh()
}
// TouchDown response to mobile touch event
func (t *Table) TouchDown(e *mobile.TouchEvent) {
t.tapped(e.Position)
}
// TouchUp response to mobile touch event
func (t *Table) TouchUp(*mobile.TouchEvent) {
}
// TouchCancel response to mobile touch event
func (t *Table) TouchCancel(*mobile.TouchEvent) {
}
// TypedKey is called if a key event happens while this Table is focused.
func (t *Table) TypedKey(event *fyne.KeyEvent) {
switch event.Name {
case fyne.KeySpace:
t.Select(t.currentFocus)
case fyne.KeyDown:
if f := t.Length; f != nil {
rows, _ := f()
if t.currentFocus.Row >= rows-1 {
return
}
}
t.RefreshItem(t.currentFocus)
t.currentFocus.Row++
t.ScrollTo(t.currentFocus)
t.RefreshItem(t.currentFocus)
case fyne.KeyLeft:
if t.currentFocus.Col <= 0 {
return
}
t.RefreshItem(t.currentFocus)
t.currentFocus.Col--
t.ScrollTo(t.currentFocus)
t.RefreshItem(t.currentFocus)
case fyne.KeyRight:
if f := t.Length; f != nil {
_, cols := f()
if t.currentFocus.Col >= cols-1 {
return
}
}
t.RefreshItem(t.currentFocus)
t.currentFocus.Col++
t.ScrollTo(t.currentFocus)
t.RefreshItem(t.currentFocus)
case fyne.KeyUp:
if t.currentFocus.Row <= 0 {
return
}
t.RefreshItem(t.currentFocus)
t.currentFocus.Row--
t.ScrollTo(t.currentFocus)
t.RefreshItem(t.currentFocus)
}
}
// TypedRune is called if a text event happens while this Table is focused.
func (t *Table) TypedRune(_ rune) {
// intentionally left blank
}
// Unselect will mark the cell provided by id as unselected.
func (t *Table) Unselect(id TableCellID) {
if t.selectedCell == nil || id != *t.selectedCell {
return
}
t.selectedCell = nil
if t.moveCallback != nil {
t.moveCallback()
}
if f := t.OnUnselected; f != nil {
f(id)
}
}
// UnselectAll will mark all cells as unselected.
//
// Since: 2.1
func (t *Table) UnselectAll() {
if t.selectedCell == nil {
return
}
selected := *t.selectedCell
t.selectedCell = nil
if t.moveCallback != nil {
t.moveCallback()
}
if f := t.OnUnselected; f != nil {
f(selected)
}
}
// ScrollTo will scroll to the given cell without changing the selection.
// Attempting to scroll beyond the limits of the table will scroll to
// the edge of the table instead.
//
// Since: 2.1
func (t *Table) ScrollTo(id TableCellID) {
if t.Length == nil {
return
}
if t.content == nil {
return
}
rows, cols := t.Length()
if id.Row >= rows {
id.Row = rows - 1
}
if id.Col >= cols {
id.Col = cols - 1
}
scrollPos := t.offset
cellX, cellWidth := t.findX(id.Col)
stickCols := t.StickyColumnCount
if stickCols > 0 {
cellX -= t.stuckXOff + t.stuckWidth
}
if t.ShowHeaderColumn {
cellX += t.headerSize.Width
stickCols--
}
if stickCols == 0 || id.Col > stickCols {
if cellX < scrollPos.X {
scrollPos.X = cellX
} else if cellX+cellWidth > scrollPos.X+t.content.Size().Width {
scrollPos.X = cellX + cellWidth - t.content.Size().Width
}
}
cellY, cellHeight := t.findY(id.Row)
stickRows := t.StickyRowCount
if stickRows > 0 {
cellY -= t.stuckYOff + t.stuckHeight
}
if t.ShowHeaderRow {
cellY += t.headerSize.Height
stickRows--
}
if stickRows == 0 || id.Row >= stickRows {
if cellY < scrollPos.Y {
scrollPos.Y = cellY
} else if cellY+cellHeight > scrollPos.Y+t.content.Size().Height {
scrollPos.Y = cellY + cellHeight - t.content.Size().Height
}
}
t.offset = scrollPos
t.content.Offset = scrollPos
t.content.Refresh()
t.finishScroll()
}
// ScrollToBottom scrolls to the last row in the table
//
// Since: 2.1
func (t *Table) ScrollToBottom() {
if t.Length == nil || t.content == nil {
return
}
rows, _ := t.Length()
cellY, cellHeight := t.findY(rows - 1)
y := cellY + cellHeight - t.content.Size().Height
if y <= 0 {
return
}
t.content.Offset.Y = y
t.offset.Y = y
t.finishScroll()
}
// ScrollToLeading scrolls horizontally to the leading edge of the table
//
// Since: 2.1
func (t *Table) ScrollToLeading() {
if t.content == nil {
return
}
t.content.Offset.X = 0
t.offset.X = 0
t.finishScroll()
}
// ScrollToOffset scrolls the table to a specific position
//
// Since: 2.6
func (t *Table) ScrollToOffset(off fyne.Position) {
if t.content == nil {
return
}
t.content.ScrollToOffset(off)
t.offset = t.content.Offset
t.finishScroll()
}
// ScrollToTop scrolls to the first row in the table
//
// Since: 2.1
func (t *Table) ScrollToTop() {
if t.content == nil {
return
}
t.content.Offset.Y = 0
t.offset.Y = 0
t.finishScroll()
}
// ScrollToTrailing scrolls horizontally to the trailing edge of the table
//
// Since: 2.1
func (t *Table) ScrollToTrailing() {
if t.content == nil || t.Length == nil {
return
}
_, cols := t.Length()
cellX, cellWidth := t.findX(cols - 1)
scrollX := cellX + cellWidth - t.content.Size().Width
if scrollX <= 0 {
return
}
t.content.Offset.X = scrollX
t.offset.X = scrollX
t.finishScroll()
}
func (t *Table) Tapped(e *fyne.PointEvent) {
if e.Position.X < 0 || e.Position.X >= t.Size().Width || e.Position.Y < 0 || e.Position.Y >= t.Size().Height {
t.selectedCell = nil
t.Refresh()
return
}
col := t.columnAt(e.Position)
if col == noCellMatch || col < 0 {
return // out of col range
}
row := t.rowAt(e.Position)
if row == noCellMatch || row < 0 {
return // out of row range
}
t.Select(TableCellID{row, col})
if !fyne.CurrentDevice().IsMobile() {
t.RefreshItem(t.currentFocus)
canvas := fyne.CurrentApp().Driver().CanvasForObject(t)
if canvas != nil {
canvas.Focus(t.impl.(fyne.Focusable))
}
t.RefreshItem(t.currentFocus)
}
}
// columnAt returns a positive integer (or 0) for the column that is found at the `pos` X position.
// If the position is between cells the method will return a negative integer representing the next column,
// i.e. -1 means the gap between 0 and 1.
func (t *Table) columnAt(pos fyne.Position) int {
dataCols := 0
if f := t.Length; f != nil {
_, dataCols = t.Length()
}
visibleColWidths, offX, minCol, maxCol := t.visibleColumnWidths(t.cellSize.Width, dataCols)
i := minCol
end := maxCol
if pos.X < t.stuckXOff+t.stuckWidth {
offX = t.stuckXOff
end = t.StickyColumnCount
i = 0
} else {
pos.X += t.content.Offset.X
offX += t.stuckXOff
}
padding := t.Theme().Size(theme.SizeNamePadding)
for x := offX; i < end; x += visibleColWidths[i-1] + padding {
if pos.X < x {
return -i // the space between i-1 and i
} else if pos.X < x+visibleColWidths[i] {
return i
}
i++
}
return noCellMatch
}
func (t *Table) createHeader() fyne.CanvasObject {
if f := t.CreateHeader; f != nil {
return f()
}
l := NewLabel("00")
l.TextStyle.Bold = true
l.Alignment = fyne.TextAlignCenter
return l
}
func (t *Table) findX(col int) (cellX float32, cellWidth float32) {
cellSize := t.templateSize()
padding := t.Theme().Size(theme.SizeNamePadding)
for i := 0; i <= col; i++ {
if cellWidth > 0 {
cellX += cellWidth + padding
}
width := cellSize.Width
if w, ok := t.columnWidths[i]; ok {
width = w
}
cellWidth = width
}
return cellX, cellWidth
}
func (t *Table) findY(row int) (cellY float32, cellHeight float32) {
cellSize := t.templateSize()
padding := t.Theme().Size(theme.SizeNamePadding)
for i := 0; i <= row; i++ {
if cellHeight > 0 {
cellY += cellHeight + padding
}
height := cellSize.Height
if h, ok := t.rowHeights[i]; ok {
height = h
}
cellHeight = height
}
return cellY, cellHeight
}
func (t *Table) finishScroll() {
if t.moveCallback != nil {
t.moveCallback()
}
t.cells.Refresh()
}
func (t *Table) hoverAt(pos fyne.Position) {
col := t.columnAt(pos)
row := t.rowAt(pos)
t.hoveredCell = &TableCellID{row, col}
overHeaderRow := t.ShowHeaderRow && pos.Y < t.headerSize.Height
overHeaderCol := t.ShowHeaderColumn && pos.X < t.headerSize.Width
if overHeaderRow && !overHeaderCol {
if col >= 0 {
t.hoverHeaderCol = noCellMatch
} else {
t.hoverHeaderCol = -col - 1
}
} else {
t.hoverHeaderCol = noCellMatch
}
if overHeaderCol && !overHeaderRow {
if row >= 0 {
t.hoverHeaderRow = noCellMatch
} else {
t.hoverHeaderRow = -row - 1
}
} else {
t.hoverHeaderRow = noCellMatch
}
rows, cols := 0, 0
if f := t.Length; f != nil {
rows, cols = t.Length()
}
if t.hoveredCell.Col >= cols || t.hoveredCell.Row >= rows || t.hoveredCell.Col < 0 || t.hoveredCell.Row < 0 {
t.hoverOut()
return
}
if t.moveCallback != nil {
t.moveCallback()
}
}
func (t *Table) hoverOut() {
t.hoveredCell = nil
if t.moveCallback != nil {
t.moveCallback()
}
}
// rowAt returns a positive integer (or 0) for the row that is found at the `pos` Y position.
// If the position is between cells the method will return a negative integer representing the next row,
// i.e. -1 means the gap between rows 0 and 1.
func (t *Table) rowAt(pos fyne.Position) int {
dataRows := 0
if f := t.Length; f != nil {
dataRows, _ = t.Length()
}
visibleRowHeights, offY, minRow, maxRow := t.visibleRowHeights(t.cellSize.Height, dataRows)
i := minRow
end := maxRow
if pos.Y < t.stuckYOff+t.stuckHeight {
offY = t.stuckYOff
end = t.StickyRowCount
i = 0
} else {
pos.Y += t.content.Offset.Y
offY += t.stuckYOff
}
padding := t.Theme().Size(theme.SizeNamePadding)
for y := offY; i < end; y += visibleRowHeights[i-1] + padding {
if pos.Y < y {
return -i // the space between i-1 and i
} else if pos.Y >= y && pos.Y < y+visibleRowHeights[i] {
return i
}
i++
}
return noCellMatch
}
func (t *Table) tapped(pos fyne.Position) {
if t.dragCol == noCellMatch && t.dragRow == noCellMatch {
t.dragStartPos = pos
if t.hoverHeaderRow != noCellMatch {
t.dragCol = noCellMatch
t.dragRow = t.hoverHeaderRow
size, ok := t.rowHeights[t.hoverHeaderRow]
if !ok {
size = t.cellSize.Height
}
t.dragStartSize = size
} else if t.hoverHeaderCol != noCellMatch {
t.dragCol = t.hoverHeaderCol
t.dragRow = noCellMatch
size, ok := t.columnWidths[t.hoverHeaderCol]
if !ok {
size = t.cellSize.Width
}
t.dragStartSize = size
}
}
}
func (t *Table) templateSize() fyne.Size {
if f := t.CreateCell; f != nil {
template := createItemAndApplyThemeScope(f, t) // don't use cache, we need new template
if !t.ShowHeaderRow && !t.ShowHeaderColumn {
return template.MinSize()
}
return template.MinSize().Max(t.createHeader().MinSize())
}
fyne.LogError("Missing CreateCell callback required for Table", nil)
return fyne.Size{}
}
func (t *Table) updateHeader(id TableCellID, o fyne.CanvasObject) {
if f := t.UpdateHeader; f != nil {
f(id, o)
return
}
l := o.(*Label)
if id.Row < 0 {
ids := []rune{'A' + rune(id.Col%26)}
pre := (id.Col - id.Col%26) / 26
for pre > 0 {
ids = append([]rune{'A' - 1 + rune(pre%26)}, ids...)
pre = (pre - pre%26) / 26
}
l.SetText(string(ids))
} else if id.Col < 0 {
l.SetText(strconv.Itoa(id.Row + 1))
} else {
l.SetText("")
}
}
func (t *Table) stickyColumnWidths(colWidth float32, cols int) (visible []float32) {
if cols == 0 {
return []float32{}
}
max := t.StickyColumnCount
if max > cols {
max = cols
}
visible = make([]float32, max)
if len(t.columnWidths) == 0 {
for i := 0; i < max; i++ {
visible[i] = colWidth
}
return visible
}
for i := 0; i < max; i++ {
height := colWidth
if h, ok := t.columnWidths[i]; ok {
height = h
}
visible[i] = height
}
return visible
}
func (t *Table) visibleColumnWidths(colWidth float32, cols int) (visible map[int]float32, offX float32, minCol, maxCol int) {
maxCol = cols
colOffset, headWidth := float32(0), float32(0)
isVisible := false
visible = make(map[int]float32)
if t.content.Size().Width <= 0 {
return visible, offX, minCol, maxCol
}
padding := t.Theme().Size(theme.SizeNamePadding)
stick := t.StickyColumnCount
size := t.Size()
if len(t.columnWidths) == 0 {
paddedWidth := colWidth + padding
offX = float32(math.Floor(float64(t.offset.X/paddedWidth))) * paddedWidth
minCol = int(math.Floor(float64(offX / paddedWidth)))
maxCol = int(math.Ceil(float64((t.offset.X + size.Width) / paddedWidth)))
if minCol > cols-1 {
minCol = cols - 1
}
if minCol < 0 {
minCol = 0
}
if maxCol > cols {
maxCol = cols
}
visible = make(map[int]float32, maxCol-minCol+stick)
for i := minCol; i < maxCol; i++ {
visible[i] = colWidth
}
for i := 0; i < stick; i++ {
visible[i] = colWidth
}
return visible, offX, minCol, maxCol
}
for i := 0; i < cols; i++ {
width := colWidth
if w, ok := t.columnWidths[i]; ok {
width = w
}
if colOffset <= t.offset.X-width-padding {
// before visible content
} else if colOffset <= headWidth || colOffset <= t.offset.X {
minCol = i
offX = colOffset
isVisible = true
}
if colOffset < t.offset.X+size.Width {
maxCol = i + 1
} else {
break
}
colOffset += width + padding
if isVisible || i < stick {
visible[i] = width
}
}
return visible, offX, minCol, maxCol
}
func (t *Table) stickyRowHeights(rowHeight float32, rows int) (visible []float32) {
if rows == 0 {
return []float32{}
}
max := t.StickyRowCount
if max > rows {
max = rows
}
visible = make([]float32, max)
if len(t.rowHeights) == 0 {
for i := 0; i < max; i++ {
visible[i] = rowHeight
}
return visible
}
for i := 0; i < max; i++ {
height := rowHeight
if h, ok := t.rowHeights[i]; ok {
height = h
}
visible[i] = height
}
return visible
}
func (t *Table) visibleRowHeights(rowHeight float32, rows int) (visible map[int]float32, offY float32, minRow, maxRow int) {
maxRow = rows
rowOffset, headHeight := float32(0), float32(0)
isVisible := false
visible = make(map[int]float32)
if t.content.Size().Height <= 0 {
return visible, offY, minRow, maxRow
}
padding := t.Theme().Size(theme.SizeNamePadding)
stick := t.StickyRowCount
size := t.Size()
if len(t.rowHeights) == 0 {
paddedHeight := rowHeight + padding
offY = float32(math.Floor(float64(t.offset.Y/paddedHeight))) * paddedHeight
minRow = int(math.Floor(float64(offY / paddedHeight)))
maxRow = int(math.Ceil(float64((t.offset.Y + size.Height) / paddedHeight)))
if minRow > rows-1 {
minRow = rows - 1
}
if minRow < 0 {
minRow = 0
}
if maxRow > rows {
maxRow = rows
}
visible = make(map[int]float32, maxRow-minRow+stick)
for i := minRow; i < maxRow; i++ {
visible[i] = rowHeight
}
for i := 0; i < stick; i++ {
visible[i] = rowHeight
}
return visible, offY, minRow, maxRow
}
for i := 0; i < rows; i++ {
height := rowHeight
if h, ok := t.rowHeights[i]; ok {
height = h
}
if rowOffset <= t.offset.Y-height-padding {
// before visible content
} else if rowOffset <= headHeight || rowOffset <= t.offset.Y {
minRow = i
offY = rowOffset
isVisible = true
}
if rowOffset < t.offset.Y+size.Height {
maxRow = i + 1
} else {
break
}
rowOffset += height + padding
if isVisible || i < stick {
visible[i] = height
}
}
return visible, offY, minRow, maxRow
}
// Declare conformity with WidgetRenderer interface.
var _ fyne.WidgetRenderer = (*tableRenderer)(nil)
type tableRenderer struct {
widget.BaseRenderer
t *Table
}
func (t *tableRenderer) Layout(s fyne.Size) {
th := t.t.Theme()
t.calculateHeaderSizes(th)
off := fyne.NewPos(t.t.stuckWidth, t.t.stuckHeight)
if t.t.ShowHeaderRow {
off.Y += t.t.headerSize.Height
}
if t.t.ShowHeaderColumn {
off.X += t.t.headerSize.Width
}
t.t.content.Move(off)
t.t.content.Resize(s.SubtractWidthHeight(off.X, off.Y))
t.t.top.Move(fyne.NewPos(off.X, 0))
t.t.top.Resize(fyne.NewSize(s.Width-off.X, off.Y))
t.t.left.Move(fyne.NewPos(0, off.Y))
t.t.left.Resize(fyne.NewSize(off.X, s.Height-off.Y))
t.t.corner.Resize(fyne.NewSize(off.X, off.Y))
t.t.dividerLayer.Resize(s)
if t.t.HideSeparators {
t.t.dividerLayer.Hide()
} else {
t.t.dividerLayer.Show()
}
}
func (t *tableRenderer) MinSize() fyne.Size {
sep := t.t.Theme().Size(theme.SizeNamePadding)
min := t.t.content.MinSize().Max(t.t.cellSize)
if t.t.ShowHeaderRow {
min.Height += t.t.headerSize.Height + sep
}
if t.t.ShowHeaderColumn {
min.Width += t.t.headerSize.Width + sep
}
if t.t.StickyRowCount > 0 {
for i := 0; i < t.t.StickyRowCount; i++ {
height := t.t.cellSize.Height
if h, ok := t.t.rowHeights[i]; ok {
height = h
}
min.Height += height + sep
}
}
if t.t.StickyColumnCount > 0 {
for i := 0; i < t.t.StickyColumnCount; i++ {
width := t.t.cellSize.Width
if w, ok := t.t.columnWidths[i]; ok {
width = w
}
min.Width += width + sep
}
}
return min
}
func (t *tableRenderer) Refresh() {
th := t.t.Theme()
t.t.headerSize = t.t.createHeader().MinSize()
if t.t.columnWidths != nil {
if v, ok := t.t.columnWidths[-1]; ok {
t.t.headerSize.Width = v
}
}
if t.t.rowHeights != nil {
if v, ok := t.t.rowHeights[-1]; ok {
t.t.headerSize.Height = v
}
}
t.t.cellSize = t.t.templateSize()
t.calculateHeaderSizes(th)
t.Layout(t.t.Size())
t.t.cells.Refresh()
}
func (t *tableRenderer) calculateHeaderSizes(th fyne.Theme) {
t.t.stuckXOff = 0
t.t.stuckYOff = 0
if t.t.ShowHeaderRow {
t.t.stuckYOff = t.t.headerSize.Height
}
if t.t.ShowHeaderColumn {
t.t.stuckXOff = t.t.headerSize.Width
}
separatorThickness := th.Size(theme.SizeNamePadding)
stickyColWidths := t.t.stickyColumnWidths(t.t.cellSize.Width, t.t.StickyColumnCount)
stickyRowHeights := t.t.stickyRowHeights(t.t.cellSize.Height, t.t.StickyRowCount)
var stuckHeight float32
for _, rowHeight := range stickyRowHeights {
stuckHeight += rowHeight + separatorThickness
}
t.t.stuckHeight = stuckHeight
var stuckWidth float32
for _, colWidth := range stickyColWidths {
stuckWidth += colWidth + separatorThickness
}
t.t.stuckWidth = stuckWidth
}
// Declare conformity with Widget interface.
var _ fyne.Widget = (*tableCells)(nil)
type tableCells struct {
BaseWidget
t *Table
nextRefreshCellsID TableCellID
}
func newTableCells(t *Table) *tableCells {
c := &tableCells{t: t, nextRefreshCellsID: allTableCellsID}
c.ExtendBaseWidget(c)
return c
}
func (c *tableCells) CreateRenderer() fyne.WidgetRenderer {
th := c.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
marker := canvas.NewRectangle(th.Color(theme.ColorNameSelection, v))
marker.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
hover := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
hover.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
r := &tableCellsRenderer{
cells: c,
visible: make(map[TableCellID]fyne.CanvasObject), headers: make(map[TableCellID]fyne.CanvasObject),
headRowBG: canvas.NewRectangle(th.Color(theme.ColorNameHeaderBackground, v)), headColBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)),
headRowStickyBG: canvas.NewRectangle(th.Color(theme.ColorNameHeaderBackground, v)), headColStickyBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)),
marker: marker, hover: hover,
}
c.t.moveCallback = r.moveIndicators
return r
}
func (c *tableCells) Resize(s fyne.Size) {
c.BaseWidget.Resize(s)
c.refreshForID(onlyNewTableCellsID) // trigger a redraw
}
func (c *tableCells) refreshForID(id TableCellID) {
c.nextRefreshCellsID = id
c.BaseWidget.Refresh()
}
func (c *tableCells) Refresh() {
c.nextRefreshCellsID = allTableCellsID
c.BaseWidget.Refresh()
}
// Declare conformity with WidgetRenderer interface.
var _ fyne.WidgetRenderer = (*tableCellsRenderer)(nil)
type tableCellsRenderer struct {
widget.BaseRenderer
cells *tableCells
pool, headerPool async.Pool[fyne.CanvasObject]
visible, headers map[TableCellID]fyne.CanvasObject
hover, marker *canvas.Rectangle
dividers []fyne.CanvasObject
headColBG, headRowBG, headRowStickyBG, headColStickyBG *canvas.Rectangle
}
func (r *tableCellsRenderer) Layout(fyne.Size) {
r.moveIndicators()
}
func (r *tableCellsRenderer) MinSize() fyne.Size {
rows, cols := 0, 0
if f := r.cells.t.Length; f != nil {
rows, cols = r.cells.t.Length()
} else {
fyne.LogError("Missing Length callback required for Table", nil)
}
stickRows := r.cells.t.StickyRowCount
stickCols := r.cells.t.StickyColumnCount
width := float32(0)
if len(r.cells.t.columnWidths) == 0 {
width = r.cells.t.cellSize.Width * float32(cols-stickCols)
} else {
cellWidth := r.cells.t.cellSize.Width
for col := stickCols; col < cols; col++ {
colWidth, ok := r.cells.t.columnWidths[col]
if ok {
width += colWidth
} else {
width += cellWidth
}
}
}
height := float32(0)
if len(r.cells.t.rowHeights) == 0 {
height = r.cells.t.cellSize.Height * float32(rows-stickRows)
} else {
cellHeight := r.cells.t.cellSize.Height
for row := stickRows; row < rows; row++ {
rowHeight, ok := r.cells.t.rowHeights[row]
if ok {
height += rowHeight
} else {
height += cellHeight
}
}
}
separatorSize := r.cells.t.Theme().Size(theme.SizeNamePadding)
return fyne.NewSize(width+float32(cols-stickCols-1)*separatorSize, height+float32(rows-stickRows-1)*separatorSize)
}
func (r *tableCellsRenderer) Refresh() {
r.refreshForID(r.cells.nextRefreshCellsID)
}
func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) {
th := r.cells.t.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
separatorThickness := th.Size(theme.SizeNamePadding)
dataRows, dataCols := 0, 0
if f := r.cells.t.Length; f != nil {
dataRows, dataCols = r.cells.t.Length()
}
visibleColWidths, offX, minCol, maxCol := r.cells.t.visibleColumnWidths(r.cells.t.cellSize.Width, dataCols)
if len(visibleColWidths) == 0 && dataCols > 0 { // we can't show anything until we have some dimensions
return
}
visibleRowHeights, offY, minRow, maxRow := r.cells.t.visibleRowHeights(r.cells.t.cellSize.Height, dataRows)
if len(visibleRowHeights) == 0 && dataRows > 0 { // we can't show anything until we have some dimensions
return
}
var cellXOffset, cellYOffset float32
stickRows := r.cells.t.StickyRowCount
if r.cells.t.ShowHeaderRow {
cellYOffset += r.cells.t.headerSize.Height
}
stickCols := r.cells.t.StickyColumnCount
if r.cells.t.ShowHeaderColumn {
cellXOffset += r.cells.t.headerSize.Width
}
startRow := minRow + stickRows
if startRow < stickRows {
startRow = stickRows
}
startCol := minCol + stickCols
if startCol < stickCols {
startCol = stickCols
}
wasVisible := r.visible
r.visible = make(map[TableCellID]fyne.CanvasObject)
var cells []fyne.CanvasObject
displayCol := func(row, col int, rowHeight float32, cells *[]fyne.CanvasObject) {
id := TableCellID{row, col}
colWidth := visibleColWidths[col]
c, ok := wasVisible[id]
if !ok {
c = r.pool.Get()
if f := r.cells.t.CreateCell; f != nil && c == nil {
c = createItemAndApplyThemeScope(f, r.cells.t)
}
if c == nil {
return
}
}
c.Move(fyne.NewPos(cellXOffset, cellYOffset))
c.Resize(fyne.NewSize(colWidth, rowHeight))
r.visible[id] = c
*cells = append(*cells, c)
cellXOffset += colWidth + separatorThickness
}
displayRow := func(row int, cells *[]fyne.CanvasObject) {
rowHeight := visibleRowHeights[row]
cellXOffset = offX
for col := startCol; col < maxCol; col++ {
displayCol(row, col, rowHeight, cells)
}
cellXOffset = r.cells.t.content.Offset.X
if r.cells.t.ShowHeaderColumn {
cellXOffset += r.cells.t.headerSize.Width
}
cellYOffset += rowHeight + separatorThickness
}
cellYOffset = offY
for row := startRow; row < maxRow; row++ {
displayRow(row, &cells)
}
inline := r.refreshHeaders(visibleRowHeights, visibleColWidths, offX, offY, startRow, maxRow, startCol, maxCol,
separatorThickness, th, v)
cells = append(cells, inline...)
offX -= r.cells.t.content.Offset.X
cellYOffset = r.cells.t.stuckYOff
for row := 0; row < stickRows; row++ {
displayRow(row, &r.cells.t.top.Content.(*fyne.Container).Objects)
}
cellYOffset = offY - r.cells.t.content.Offset.Y
for row := startRow; row < maxRow; row++ {
cellXOffset = r.cells.t.stuckXOff
rowHeight := visibleRowHeights[row]
for col := 0; col < stickCols; col++ {
displayCol(row, col, rowHeight, &r.cells.t.left.Content.(*fyne.Container).Objects)
}
cellYOffset += rowHeight + separatorThickness
}
cellYOffset = r.cells.t.stuckYOff
for row := 0; row < stickRows; row++ {
cellXOffset = r.cells.t.stuckXOff
rowHeight := visibleRowHeights[row]
for col := 0; col < stickCols; col++ {
displayCol(row, col, rowHeight, &r.cells.t.corner.Content.(*fyne.Container).Objects)
}
cellYOffset += rowHeight + separatorThickness
}
for id, old := range wasVisible {
if _, ok := r.visible[id]; !ok {
r.pool.Put(old)
}
}
r.SetObjects(cells)
r.updateCells(toDraw, r.visible, wasVisible)
for id, head := range r.headers {
r.cells.t.updateHeader(id, head)
}
r.moveIndicators()
r.marker.FillColor = th.Color(theme.ColorNameSelection, v)
r.marker.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
r.marker.Refresh()
r.hover.FillColor = th.Color(theme.ColorNameHover, v)
r.hover.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
r.hover.Refresh()
}
func (r *tableCellsRenderer) updateCells(toDraw TableCellID, visible, wasVisible map[TableCellID]fyne.CanvasObject) {
updateCell := r.cells.t.UpdateCell
if updateCell == nil {
fyne.LogError("Missing UpdateCell callback required for Table", nil)
return
}
if toDraw == onlyNewTableCellsID {
for id, cell := range visible {
if _, ok := wasVisible[id]; !ok {
updateCell(id, cell)
}
}
return
}
for id, cell := range visible {
if toDraw != allTableCellsID && toDraw != id {
continue
}
updateCell(id, cell)
}
}
func (r *tableCellsRenderer) moveIndicators() {
rows, cols := 0, 0
if f := r.cells.t.Length; f != nil {
rows, cols = r.cells.t.Length()
}
visibleColWidths, offX, minCol, maxCol := r.cells.t.visibleColumnWidths(r.cells.t.cellSize.Width, cols)
visibleRowHeights, offY, minRow, maxRow := r.cells.t.visibleRowHeights(r.cells.t.cellSize.Height, rows)
th := r.cells.t.Theme()
separatorThickness := th.Size(theme.SizeNameSeparatorThickness)
padding := th.Size(theme.SizeNamePadding)
dividerOff := (padding - separatorThickness) / 2
stickRows := r.cells.t.StickyRowCount
stickCols := r.cells.t.StickyColumnCount
if r.cells.t.ShowHeaderColumn {
offX += r.cells.t.headerSize.Width
}
if r.cells.t.ShowHeaderRow {
offY += r.cells.t.headerSize.Height
}
if r.cells.t.selectedCell == nil {
r.moveMarker(r.marker, -1, -1, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights)
} else {
r.moveMarker(r.marker, r.cells.t.selectedCell.Row, r.cells.t.selectedCell.Col, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights)
}
if r.cells.t.hoveredCell == nil && !r.cells.t.focused {
r.moveMarker(r.hover, -1, -1, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights)
} else if r.cells.t.focused {
r.moveMarker(r.hover, r.cells.t.currentFocus.Row, r.cells.t.currentFocus.Col, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights)
} else {
r.moveMarker(r.hover, r.cells.t.hoveredCell.Row, r.cells.t.hoveredCell.Col, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights)
}
colDivs := stickCols + maxCol - minCol - 1
if colDivs < 0 {
colDivs = 0
}
rowDivs := stickRows + maxRow - minRow - 1
if rowDivs < 0 {
rowDivs = 0
}
if colDivs < 0 {
colDivs = 0
}
if rowDivs < 0 {
rowDivs = 0
}
if len(r.dividers) < colDivs+rowDivs {
for i := len(r.dividers); i < colDivs+rowDivs; i++ {
r.dividers = append(r.dividers, NewSeparator())
}
objs := []fyne.CanvasObject{r.marker, r.hover}
r.cells.t.dividerLayer.Content.(*fyne.Container).Objects = append(objs, r.dividers...)
r.cells.t.dividerLayer.Content.Refresh()
}
size := r.cells.t.Size()
divs := 0
i := 0
if stickCols > 0 {
for x := r.cells.t.stuckXOff + visibleColWidths[i]; i < stickCols && divs < colDivs; x += visibleColWidths[i] + padding {
i++
xPos := x + dividerOff
r.dividers[divs].Resize(fyne.NewSize(separatorThickness, size.Height))
r.dividers[divs].Move(fyne.NewPos(xPos, 0))
r.dividers[divs].Show()
divs++
}
}
i = minCol + stickCols
for x := offX + r.cells.t.stuckWidth + visibleColWidths[i]; i < maxCol-1 && divs < colDivs; x += visibleColWidths[i] + padding {
i++
xPos := x - r.cells.t.content.Offset.X + dividerOff
r.dividers[divs].Resize(fyne.NewSize(separatorThickness, size.Height))
r.dividers[divs].Move(fyne.NewPos(xPos, 0))
r.dividers[divs].Show()
divs++
}
i = 0
if stickRows > 0 {
for y := r.cells.t.stuckYOff + visibleRowHeights[i]; i < stickRows && divs-colDivs < rowDivs; y += visibleRowHeights[i] + padding {
i++
yPos := y + dividerOff
r.dividers[divs].Resize(fyne.NewSize(size.Width, separatorThickness))
r.dividers[divs].Move(fyne.NewPos(0, yPos))
r.dividers[divs].Show()
divs++
}
}
i = minRow + stickRows
for y := offY + r.cells.t.stuckHeight + visibleRowHeights[i]; i < maxRow-1 && divs-colDivs < rowDivs; y += visibleRowHeights[i] + padding {
i++
yPos := y - r.cells.t.content.Offset.Y + dividerOff
r.dividers[divs].Resize(fyne.NewSize(size.Width, separatorThickness))
r.dividers[divs].Move(fyne.NewPos(0, yPos))
r.dividers[divs].Show()
divs++
}
for i := divs; i < len(r.dividers); i++ {
r.dividers[i].Hide()
}
}
func (r *tableCellsRenderer) moveMarker(marker fyne.CanvasObject, row, col int, offX, offY float32, minCol, minRow int, widths, heights map[int]float32) {
if col == -1 || row == -1 {
marker.Hide()
marker.Refresh()
return
}
xPos := offX
stickCols := r.cells.t.StickyColumnCount
if col < stickCols {
if r.cells.t.ShowHeaderColumn {
xPos = r.cells.t.stuckXOff
} else {
xPos = 0
}
minCol = 0
}
padding := r.cells.t.Theme().Size(theme.SizeNamePadding)
for i := minCol; i < col; i++ {
xPos += widths[i]
xPos += padding
}
x1 := xPos
if col >= stickCols {
x1 -= r.cells.t.content.Offset.X
}
x2 := x1 + widths[col]
yPos := offY
stickRows := r.cells.t.StickyRowCount
if row < stickRows {
if r.cells.t.ShowHeaderRow {
yPos = r.cells.t.stuckYOff
} else {
yPos = 0
}
minRow = 0
}
for i := minRow; i < row; i++ {
yPos += heights[i]
yPos += padding
}
y1 := yPos
if row >= stickRows {
y1 -= r.cells.t.content.Offset.Y
}
y2 := y1 + heights[row]
size := r.cells.t.Size()
if x2 < 0 || x1 > size.Width || y2 < 0 || y1 > size.Height {
marker.Hide()
} else {
left := x1
if col >= stickCols { // clip X
left = fyne.Max(r.cells.t.stuckXOff+r.cells.t.stuckWidth, x1)
}
top := y1
if row >= stickRows { // clip Y
top = fyne.Max(r.cells.t.stuckYOff+r.cells.t.stuckHeight, y1)
}
marker.Move(fyne.NewPos(left, top))
marker.Resize(fyne.NewSize(x2-left, y2-top))
marker.Show()
}
marker.Refresh()
}
func (r *tableCellsRenderer) refreshHeaders(visibleRowHeights, visibleColWidths map[int]float32, offX, offY float32,
startRow, maxRow, startCol, maxCol int, separatorThickness float32, th fyne.Theme, v fyne.ThemeVariant,
) []fyne.CanvasObject {
wasVisible := r.headers
r.headers = make(map[TableCellID]fyne.CanvasObject)
headerMin := r.cells.t.headerSize
rowHeight := headerMin.Height
colWidth := headerMin.Width
var cells, over []fyne.CanvasObject
corner := []fyne.CanvasObject{r.headColStickyBG, r.headRowStickyBG}
over = []fyne.CanvasObject{r.headRowBG}
if r.cells.t.ShowHeaderRow {
cellXOffset := offX - r.cells.t.content.Offset.X
displayColHeader := func(col int, list *[]fyne.CanvasObject) {
id := TableCellID{-1, col}
colWidth := visibleColWidths[col]
c, ok := wasVisible[id]
if !ok {
c = r.headerPool.Get()
if c == nil {
c = r.cells.t.createHeader()
}
if c == nil {
return
}
}
c.Move(fyne.NewPos(cellXOffset, 0))
c.Resize(fyne.NewSize(colWidth, rowHeight))
r.headers[id] = c
*list = append(*list, c)
cellXOffset += colWidth + separatorThickness
}
for col := startCol; col < maxCol; col++ {
displayColHeader(col, &over)
}
if r.cells.t.StickyColumnCount > 0 {
cellXOffset = 0
if r.cells.t.ShowHeaderColumn {
cellXOffset += r.cells.t.headerSize.Width
}
for col := 0; col < r.cells.t.StickyColumnCount; col++ {
displayColHeader(col, &corner)
}
}
}
r.cells.t.top.Content.(*fyne.Container).Objects = over
r.cells.t.top.Content.Refresh()
over = []fyne.CanvasObject{r.headColBG}
if r.cells.t.ShowHeaderColumn {
cellYOffset := offY - r.cells.t.content.Offset.Y
displayRowHeader := func(row int, list *[]fyne.CanvasObject) {
id := TableCellID{row, -1}
rowHeight := visibleRowHeights[row]
c, ok := wasVisible[id]
if !ok {
c = r.headerPool.Get()
if c == nil {
c = r.cells.t.createHeader()
}
if c == nil {
return
}
}
c.Move(fyne.NewPos(0, cellYOffset))
c.Resize(fyne.NewSize(colWidth, rowHeight))
r.headers[id] = c
*list = append(*list, c)
cellYOffset += rowHeight + separatorThickness
}
for row := startRow; row < maxRow; row++ {
displayRowHeader(row, &over)
}
if r.cells.t.StickyRowCount > 0 {
cellYOffset = 0
if r.cells.t.ShowHeaderRow {
cellYOffset += r.cells.t.headerSize.Height
}
for row := 0; row < r.cells.t.StickyRowCount; row++ {
displayRowHeader(row, &corner)
}
}
}
r.cells.t.left.Content.(*fyne.Container).Objects = over
r.cells.t.left.Content.Refresh()
r.headColBG.Hidden = !r.cells.t.ShowHeaderColumn
r.headColBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v)
r.headColBG.Resize(fyne.NewSize(colWidth, r.cells.t.Size().Height))
r.headColStickyBG.Hidden = !r.cells.t.ShowHeaderColumn
r.headColStickyBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v)
r.headColStickyBG.Resize(fyne.NewSize(colWidth, r.cells.t.stuckHeight+rowHeight))
r.headRowBG.Hidden = !r.cells.t.ShowHeaderRow
r.headRowBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v)
r.headRowBG.Resize(fyne.NewSize(r.cells.t.Size().Width, rowHeight))
r.headRowStickyBG.Hidden = !r.cells.t.ShowHeaderRow
r.headRowStickyBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v)
r.headRowStickyBG.Resize(fyne.NewSize(r.cells.t.stuckWidth+colWidth, rowHeight))
r.cells.t.corner.Content.(*fyne.Container).Objects = corner
r.cells.t.corner.Content.Refresh()
for id, old := range wasVisible {
if _, ok := r.headers[id]; !ok {
r.headerPool.Put(old)
}
}
return cells
}
type clip struct {
widget.Scroll
t *Table
}
func newClip(t *Table, o fyne.CanvasObject) *clip {
c := &clip{t: t}
c.Content = o
c.Direction = widget.ScrollNone
return c
}
func (c *clip) DragEnd() {
c.t.DragEnd()
c.t.dragCol = noCellMatch
c.t.dragRow = noCellMatch
}
func (c *clip) Dragged(e *fyne.DragEvent) {
c.t.Dragged(e)
}