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

1676 lines
42 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/cache"
"fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/theme"
)
const noCellMatch = math.MaxInt32 // TODO make this MaxInt once we move to newer Go version
// allTableCellsID represents all table cells when refreshing requested cells
var allTableCellsID = TableCellID{-1, -1}
// Declare conformity with interfaces
var _ desktop.Cursorable = (*Table)(nil)
var _ fyne.Draggable = (*Table)(nil)
var _ fyne.Focusable = (*Table)(nil)
var _ desktop.Hoverable = (*Table)(nil)
var _ fyne.Tappable = (*Table)(nil)
var _ 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
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.
//
// Implements: fyne.Widget
func (t *Table) CreateRenderer() fyne.WidgetRenderer {
t.ExtendBaseWidget(t)
t.propertyLock.Lock()
t.headerSize = t.createHeader().MinSize()
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.propertyLock.Unlock()
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.Refresh()
}
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) {
t.propertyLock.Lock()
min := t.cellSize
col := t.dragCol
row := t.dragRow
startPos := t.dragStartPos
startSize := t.dragStartSize
t.propertyLock.Unlock()
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.
//
// Implements: fyne.Focusable
func (t *Table) FocusGained() {
t.focused = true
t.ScrollTo(t.currentFocus)
t.RefreshItem(t.currentFocus)
}
// FocusLost is called after this Table has lost focus.
//
// Implements: fyne.Focusable
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
}
r := cache.Renderer(t.cells)
if r == nil {
return
}
r.(*tableCellsRenderer).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 >= rows || 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.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) {
t.propertyLock.Lock()
if t.columnWidths == nil {
t.columnWidths = make(map[int]float32)
}
t.columnWidths[id] = width
t.propertyLock.Unlock()
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) {
t.propertyLock.Lock()
if t.rowHeights == nil {
t.rowHeights = make(map[int]float32)
}
t.rowHeights[id] = height
t.propertyLock.Unlock()
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.
//
// Implements: fyne.Focusable
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.
//
// Implements: fyne.Focusable
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()
}
// 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 {
return // out of col range
}
row := t.rowAt(e.Position)
if row == noCellMatch {
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)
}
t.currentFocus = TableCellID{row, col}
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 := theme.Padding()
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 := theme.Padding()
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
}
func (t *Table) findY(row int) (cellY float32, cellHeight float32) {
cellSize := t.templateSize()
padding := theme.Padding()
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
}
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 := theme.Padding()
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 := f() // 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
}
for i := 0; i < max; i++ {
height := colWidth
if h, ok := t.columnWidths[i]; ok {
height = h
}
visible[i] = height
}
return
}
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
}
// theme.Padding is a slow call, so we cache it
padding := theme.Padding()
stick := t.StickyColumnCount
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 + t.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
}
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+t.size.Width {
maxCol = i + 1
} else {
break
}
colOffset += width + padding
if isVisible || i < stick {
visible[i] = width
}
}
return
}
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
}
for i := 0; i < max; i++ {
height := rowHeight
if h, ok := t.rowHeights[i]; ok {
height = h
}
visible[i] = height
}
return
}
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
}
// theme.Padding is a slow call, so we cache it
padding := theme.Padding()
stick := t.StickyRowCount
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 + t.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
}
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+t.size.Height {
maxRow = i + 1
} else {
break
}
rowOffset += height + padding
if isVisible || i < stick {
visible[i] = height
}
}
return
}
// Declare conformity with WidgetRenderer interface.
var _ fyne.WidgetRenderer = (*tableRenderer)(nil)
type tableRenderer struct {
widget.BaseRenderer
t *Table
}
func (t *tableRenderer) Layout(s fyne.Size) {
t.t.propertyLock.RLock()
t.calculateHeaderSizes()
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.propertyLock.RUnlock()
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)
}
func (t *tableRenderer) MinSize() fyne.Size {
sep := theme.Padding()
t.t.propertyLock.RLock()
defer t.t.propertyLock.RUnlock()
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() {
t.t.propertyLock.Lock()
t.t.headerSize = t.t.createHeader().MinSize()
t.t.cellSize = t.t.templateSize()
t.calculateHeaderSizes()
t.t.propertyLock.Unlock()
t.Layout(t.t.Size())
t.t.cells.Refresh()
}
func (t *tableRenderer) calculateHeaderSizes() {
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 := theme.Padding()
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
}
func newTableCells(t *Table) *tableCells {
c := &tableCells{t: t}
c.ExtendBaseWidget(c)
return c
}
func (c *tableCells) CreateRenderer() fyne.WidgetRenderer {
marker := canvas.NewRectangle(theme.SelectionColor())
marker.CornerRadius = theme.SelectionRadiusSize()
hover := canvas.NewRectangle(theme.HoverColor())
hover.CornerRadius = theme.SelectionRadiusSize()
r := &tableCellsRenderer{cells: c, pool: &syncPool{}, headerPool: &syncPool{},
visible: make(map[TableCellID]fyne.CanvasObject), headers: make(map[TableCellID]fyne.CanvasObject),
headRowBG: canvas.NewRectangle(theme.HeaderBackgroundColor()), headColBG: canvas.NewRectangle(theme.HeaderBackgroundColor()),
headRowStickyBG: canvas.NewRectangle(theme.HeaderBackgroundColor()), headColStickyBG: canvas.NewRectangle(theme.HeaderBackgroundColor()),
marker: marker, hover: hover}
c.t.moveCallback = r.moveIndicators
return r
}
func (c *tableCells) Resize(s fyne.Size) {
c.BaseWidget.Resize(s)
c.Refresh() // trigger a redraw
}
// Declare conformity with WidgetRenderer interface.
var _ fyne.WidgetRenderer = (*tableCellsRenderer)(nil)
type tableCellsRenderer struct {
widget.BaseRenderer
cells *tableCells
pool, headerPool pool
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.cells.propertyLock.Lock()
r.moveIndicators()
r.cells.propertyLock.Unlock()
}
func (r *tableCellsRenderer) MinSize() fyne.Size {
r.cells.propertyLock.RLock()
defer r.cells.propertyLock.RUnlock()
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 := theme.Padding()
return fyne.NewSize(width+float32(cols-stickCols-1)*separatorSize, height+float32(rows-stickRows-1)*separatorSize)
}
func (r *tableCellsRenderer) Refresh() {
r.refreshForID(allTableCellsID)
}
func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) {
r.cells.propertyLock.Lock()
separatorThickness := theme.Padding()
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
r.cells.propertyLock.Unlock()
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
r.cells.propertyLock.Unlock()
return
}
updateCell := r.cells.t.UpdateCell
if updateCell == nil {
fyne.LogError("Missing UpdateCell callback required for Table", nil)
}
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.Obtain()
if f := r.cells.t.CreateCell; f != nil && c == nil {
c = f()
}
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
stick := r.cells.t.StickyColumnCount
if r.cells.t.ShowHeaderColumn {
cellXOffset += r.cells.t.headerSize.Width
stick--
}
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)
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.Release(old)
}
}
visible := r.visible
headers := r.headers
r.cells.propertyLock.Unlock()
r.SetObjects(cells)
if updateCell != nil {
for id, cell := range visible {
if toDraw != allTableCellsID && toDraw != id {
continue
}
updateCell(id, cell)
}
}
for id, head := range headers {
r.cells.t.updateHeader(id, head)
}
r.moveIndicators()
r.marker.FillColor = theme.SelectionColor()
r.marker.CornerRadius = theme.SelectionRadiusSize()
r.marker.Refresh()
r.hover.FillColor = theme.HoverColor()
r.hover.CornerRadius = theme.SelectionRadiusSize()
r.hover.Refresh()
}
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)
separatorThickness := theme.SeparatorThicknessSize()
padding := theme.Padding()
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()
}
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, r.cells.t.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, r.cells.t.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(r.cells.t.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(r.cells.t.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 := theme.Padding()
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]
if x2 < 0 || x1 > r.cells.t.size.Width || y2 < 0 || y1 > r.cells.t.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) []fyne.CanvasObject {
wasVisible := r.headers
r.headers = make(map[TableCellID]fyne.CanvasObject)
headerMin := r.cells.t.createHeader().MinSize()
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.Obtain()
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.Obtain()
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 = theme.HeaderBackgroundColor()
r.headColBG.Resize(fyne.NewSize(colWidth, r.cells.t.Size().Height))
r.headColStickyBG.Hidden = !r.cells.t.ShowHeaderColumn
r.headColStickyBG.FillColor = theme.HeaderBackgroundColor()
r.headColStickyBG.Resize(fyne.NewSize(colWidth, r.cells.t.stuckHeight+rowHeight))
r.headRowBG.Hidden = !r.cells.t.ShowHeaderRow
r.headRowBG.FillColor = theme.HeaderBackgroundColor()
r.headRowBG.Resize(fyne.NewSize(r.cells.t.Size().Width, rowHeight))
r.headRowStickyBG.Hidden = !r.cells.t.ShowHeaderRow
r.headRowStickyBG.FillColor = theme.HeaderBackgroundColor()
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.Release(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)
}