1676 lines
42 KiB
Go
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)
|
|
}
|