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.MaxInt

// 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

	// 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.
//
// Implements: fyne.Widget
func (t *Table) CreateRenderer() fyne.WidgetRenderer {
	t.ExtendBaseWidget(t)

	t.propertyLock.Lock()
	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.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 id < 0 {
		if t.headerSize.Width == width {
			t.propertyLock.Unlock()
			return
		}
		t.headerSize.Width = width
	}

	if t.columnWidths == nil {
		t.columnWidths = make(map[int]float32)
	}

	if set, ok := t.columnWidths[id]; ok && set == width {
		t.propertyLock.Unlock()
		return
	}
	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 id < 0 {
		if t.headerSize.Height == height {
			t.propertyLock.Unlock()
			return
		}
		t.headerSize.Height = height
	}

	if t.rowHeights == nil {
		t.rowHeights = make(map[int]float32)
	}

	if set, ok := t.rowHeights[id]; ok && set == height {
		t.propertyLock.Unlock()
		return
	}
	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 := 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
}

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
}

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
	}

	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
	}

	padding := t.Theme().Size(theme.SizeNamePadding)
	stick := t.StickyColumnCount
	size := t.size.Load()

	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
	}

	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
}

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
	}

	padding := t.Theme().Size(theme.SizeNamePadding)
	stick := t.StickyRowCount
	size := t.size.Load()

	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
	}

	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
}

// 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.t.propertyLock.RLock()

	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.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)
	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)
	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() {
	th := t.t.Theme()
	t.t.propertyLock.Lock()
	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.t.propertyLock.Unlock()

	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
}

func newTableCells(t *Table) *tableCells {
	c := &tableCells{t: t}
	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, pool: &syncPool{}, headerPool: &syncPool{},
		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.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 := 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(allTableCellsID)
}

func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) {
	th := r.cells.t.Theme()
	v := fyne.CurrentApp().Settings().ThemeVariant()

	r.cells.propertyLock.Lock()
	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
		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 = 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
		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, 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.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 = 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) 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.Load()

	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.Load()
	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.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 = 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.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)
}
