TableView's OnBeforeDraw feature closes #113

This commit is contained in:
Vladimir Markelov 2018-11-09 22:52:36 -08:00
parent 80da317c70
commit 380e4a5118
5 changed files with 214 additions and 11 deletions

View File

@ -7,7 +7,7 @@ Command Line User Interface (Console UI inspired by TurboVision) with built-in t
## Current version
The current version is 0.9.0 RC5. Please see details in [changelog](./changelog).
The current version is 1.0.0 RC6. Please see details in [changelog](./changelog).
## Applications that uses the library
* Terminal FB2 reader(termfb2): https://github.com/VladimirMarkelov/termfb2

View File

@ -1 +1 @@
0.9.0-rc5
1.0.0-rc6

View File

@ -1,3 +1,16 @@
2018-11-09 - version 1.0.0 RC6
[*] If no error is found in the next few weeks, this release will become 1.0.
It is high time to release 1.0. Everything looks working.
[+] TableView new method VisibleArea - returns first visible column and row,
and the number of visible columns and rows. It can be useful to prepare
data beforehand to draw the data faster
[+] Table new event OnBeforeDraw(firstCol, firstRow, colCount, rowCount int).
The event is fired right before TableView is going to draw itself. So the
application can prepare all data in one step and then fetch them quickly
from the cache. The arguments of a callback are the same as returns values
of VisibleArea method
[+] Added new demo tableview-preload to show how to use new event
2018-10-08 - version 0.9.0 RC5
[*] Fix Frame border drawing
@ -6,7 +19,7 @@
[+] ScrollTo API for scrollable frame
[*] Clipper fix
[*] ChildAt should skip hidden controls and skip a control if its parent
is invisble
is invisible
[*] Enter key did not work in TableView control
2018-09-06 - version 0.9.0 RC3 (Thanks to Leandro Dorileo)

View File

@ -0,0 +1,130 @@
/*
* Demo includes:
* - How to use OnBeforeDraw event
* - a simple example of "DBCache" for faster drawing
*/
package main
import (
"fmt"
ui "github.com/VladimirMarkelov/clui"
)
// number of columns in a table
const columnInTable = 6
// dbCache for data from DB. It always caches the whole table row, so it does not
// use firstCol and colCount values from OnBeforeDraw event. But you can do more
// granular storage to minimize memory usage by cache
// dbCache is quite dumb: if it detects that topRow or the number of visible rows
// is changed it invalidates the cache and reloads all the data from new row span.
// In real application, it would be good to make it smarter, e.g:
// - if rowCount descreased and firstRow does not change - the cache is valid,
// and redundant rereading data can be skipped
// - usually visible area changes by 1 row, so performance-wise the cache can
// shift row slice and read only new rows
// - etc
type dbCache struct {
firstRow int // previous first visible row
rowCount int // previous visible row count
data [][]string // cache - contains at least 'rowCount' rows from DB
}
// cache data from a new row span
// It imitates a random data by selecting values from predefined arrays. Sizes
// of all arrays should be different to make TableView data look more random
func (d *dbCache) preload(firstRow, rowCount int) {
if firstRow == d.firstRow && rowCount == d.rowCount {
// fast path: view area is the same, return immediately
return
}
// slow path: refill cache
fNames := []string{"Jack", "Alisa", "Richard", "Paul", "Nicole", "Steven", "Jane"}
lNames := []string{"Smith", "Catcher", "Stone", "White", "Black"}
posts := []string{"Engineer", "Manager", "Janitor", "Driver"}
deps := []string{"IT", "Financial", "Support"}
salary := []int{40000, 38000, 41000, 32000}
d.data = make([][]string, rowCount, rowCount)
for i := 0; i < rowCount; i++ {
absIndex := firstRow + i
d.data[i] = make([]string, columnInTable, columnInTable)
d.data[i][0] = fNames[absIndex%len(fNames)]
d.data[i][1] = lNames[absIndex%len(lNames)]
d.data[i][2] = fmt.Sprintf("%08d", 100+absIndex)
d.data[i][3] = posts[absIndex%len(posts)]
d.data[i][4] = deps[absIndex%len(deps)]
d.data[i][5] = fmt.Sprintf("%d k/year", salary[absIndex%len(salary)]/1000)
}
// do not forget to save the last values
d.firstRow = firstRow
d.rowCount = rowCount
}
// returns the cell value for a given col and row. Col and row are absolute
// value. But cache keeps limited number of rows to minimize memory usage.
// So, the position of the value of the cell should be calculated
// To simplify, the function just returns empty string if the cell is not
// cached. It is unlikely but can happen
func (d *dbCache) value(row, col int) string {
rowId := row - d.firstRow
if rowId >= len(d.data) {
return ""
}
rowValues := d.data[rowId]
if col >= len(rowValues) {
return ""
}
return rowValues[col]
}
var (
view *ui.Window
)
func createView() *ui.TableView {
view = ui.AddWindow(0, 0, 10, 7, "TableView Preload Demo")
bch := ui.CreateTableView(view, 35, 12, 1)
ui.ActivateControl(view, bch)
return bch
}
func mainLoop() {
// Every application must create a single Composer and
// call its intialize method
ui.InitLibrary()
defer ui.DeinitLibrary()
cache := &dbCache{firstRow: -1}
b := createView()
b.SetShowLines(true)
b.SetShowRowNumber(true)
b.SetRowCount(25)
cols := []ui.Column{
ui.Column{Title: "First Name", Width: 10, Alignment: ui.AlignLeft},
ui.Column{Title: "Last Name", Width: 12, Alignment: ui.AlignLeft},
ui.Column{Title: "ID", Width: 12, Alignment: ui.AlignRight},
ui.Column{Title: "Post", Width: 12, Alignment: ui.AlignLeft},
ui.Column{Title: "Department", Width: 15, Alignment: ui.AlignLeft},
ui.Column{Title: "Salary", Width: 12, Alignment: ui.AlignRight},
}
b.SetColumns(cols)
b.OnBeforeDraw(func(col, row, colCnt, rowCnt int) {
cache.preload(row, rowCnt)
l, t, w, h := b.VisibleArea()
view.SetTitle(fmt.Sprintf("Caching: %d:%d - %dx%d", l, t, w, h))
})
b.OnDrawCell(func(info *ui.ColumnDrawInfo) {
info.Text = cache.value(info.Row, info.Col)
})
// start event processing loop - the main core of the library
ui.MainLoop()
}
func main() {
mainLoop()
}

View File

@ -35,14 +35,19 @@ Events:
OnKeyPress - called every time a user presses a key. Callback should
return true if TableView must skip internal key processing.
E.g, a user can disable emitting TableActionDelete event by
adding callback OnKeyPress and retun true in case of Delete
adding callback OnKeyPress and return true in case of Delete
key is pressed
OnSelectCell - called in case of the currently selected row or
column is changed
OnBeforeDraw - called right before the TableView is going to repaint
itself. It can be used to prepare all the data beforehand and
then quickly use cached data inside OnDrawCell. Callback
receives 4 arguments: first visible column, first visible row,
number of visible columns, number of visible rows.
*/
type TableView struct {
BaseControl
// own listbox members
// own TableView members
topRow int
topCol int
selectedRow int
@ -57,6 +62,7 @@ type TableView struct {
onAction func(TableEvent)
onKeyPress func(term.Key) bool
onSelectCell func(int, int)
onBeforeDraw func(int, int, int, int)
// internal variable to avoid sending onSelectCell twice or more
// in case of current cell is unchanged
@ -81,7 +87,7 @@ type Column struct {
// will be empty. In addition to it, the callback can
// change Bg, Fg, and Alignment to display customizes
// info. All other non-mentioned fields are for a user
// convinience and used to describe the cell more detailed,
// convenience and used to describe the cell more detailed,
// changing that fields affects nothing
type ColumnDrawInfo struct {
// row number
@ -121,7 +127,7 @@ type TableEvent struct {
NewTableView creates a new frame.
view - is a View that manages the control
parent - is container that keeps the control. The same View can be a view and a parent at the same time.
width and heigth - are minimal size of the control.
width and height - are minimal size of the control.
scale - the way of scaling the control when the parent is resized. Use DoNotScale constant if the
control should keep its original size.
*/
@ -361,6 +367,11 @@ func (l *TableView) Draw() {
x, y := l.Pos()
w, h := l.Size()
if l.onBeforeDraw != nil {
firstCol, firstRow, colCount, rowCount := l.VisibleArea()
l.onBeforeDraw(firstCol, firstRow, colCount, rowCount)
}
bg := RealColor(l.bg, l.Style(), ColorTableBack)
SetBackColor(bg)
FillRect(x, y+2, w, h-2, ' ')
@ -524,7 +535,7 @@ func (l *TableView) isColVisible(idx int) bool {
}
// EnsureColVisible scrolls the table horizontally
// to make the curently selected column fully visible
// to make the currently selected column fully visible
func (l *TableView) EnsureColVisible() {
if l.isColVisible(l.selectedCol) {
return
@ -567,7 +578,7 @@ func (l *TableView) EnsureColVisible() {
}
// EnsureRowVisible scrolls the table vertically
// to make the curently selected row visible
// to make the currently selected row visible
func (l *TableView) EnsureRowVisible() {
length := l.rowCount
@ -935,9 +946,8 @@ func (l *TableView) OnKeyPress(fn func(term.Key) bool) {
// a cell
func (l *TableView) OnDrawCell(fn func(*ColumnDrawInfo)) {
l.mtx.Lock()
defer l.mtx.Unlock()
l.onDrawCell = fn
l.mtx.Unlock()
}
// OnAction is called when the table wants a user application to
@ -993,3 +1003,53 @@ func (l *TableView) SetSelectedCol(col int) {
l.emitSelectionChange()
}
}
// OnBeforeDraw is called when TableView is going to draw its cells.
// Can be used to precache the data, and make OnDrawCell faster.
// Callback receives 4 arguments: first visible column, first visible row,
// the number of visible columns, the number of visible rows
func (l *TableView) OnBeforeDraw(fn func(int, int, int, int)) {
l.mtx.Lock()
l.onBeforeDraw = fn
l.mtx.Unlock()
}
// VisibleArea returns which rows and columns are currently visible. It can be
// used instead of OnBeforeDraw event to prepare the data for drawing without
// waiting until TableView starts drawing itself.
// It can be useful in case of you update your database, so at the same moment
// you can request the visible area and update database cache - it can improve
// performance.
// Returns:
// * firstCol - first visible column
// * firstRow - first visible row
// * colCount - the number of visible columns
// * rowCount - the number of visible rows
func (l *TableView) VisibleArea() (firstCol, firstRow, colCount, rowCount int) {
firstRow = l.topRow
maxDy := l.height - 3
if firstRow+maxDy < l.rowCount {
rowCount = maxDy
} else {
rowCount = l.rowCount - l.topRow
}
total := l.width - 1
if l.showRowNo {
total -= l.counterWidth()
if l.showVLines {
total--
}
}
colNo := l.topCol
colCount = 0
for colNo < len(l.columns) && total > 0 {
w := l.columns[colNo].Width
total -= w
colNo++
colCount++
}
return l.topCol, l.topRow, colCount, rowCount
}