From 380e4a5118327581f522094e4a60fe25ebdb2035 Mon Sep 17 00:00:00 2001 From: Vladimir Markelov Date: Fri, 9 Nov 2018 22:52:36 -0800 Subject: [PATCH] TableView's OnBeforeDraw feature closes #113 --- README.md | 2 +- VERSION | 2 +- changelog | 15 +++- demos/tableview-preload/tableview.go | 130 +++++++++++++++++++++++++++ tableview.go | 76 ++++++++++++++-- 5 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 demos/tableview-preload/tableview.go diff --git a/README.md b/README.md index ec3af2a..c777e23 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/VERSION b/VERSION index 9af2979..b4f4dfe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.0-rc5 +1.0.0-rc6 diff --git a/changelog b/changelog index 6ca7f4a..39719fa 100644 --- a/changelog +++ b/changelog @@ -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) diff --git a/demos/tableview-preload/tableview.go b/demos/tableview-preload/tableview.go new file mode 100644 index 0000000..a0bcd36 --- /dev/null +++ b/demos/tableview-preload/tableview.go @@ -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() +} diff --git a/tableview.go b/tableview.go index 97a0349..7aafc79 100644 --- a/tableview.go +++ b/tableview.go @@ -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 +}