Grid widget rework; smooth scrolling

This commit is contained in:
Vadim Lopatin 2016-10-13 09:17:02 +03:00
parent 13acf5d1ac
commit a4af36daab
2 changed files with 417 additions and 316 deletions

View File

@ -49,6 +49,13 @@ import std.algorithm;
struct Point { struct Point {
int x; int x;
int y; int y;
Point opBinary(string op)(Point v) if (op == "+") {
return Point(x + v.x, y + v.y);
}
Point opBinary(string op)(Point v) if (op == "-") {
return Point(x - v.x, y - v.y);
}
} }
/// 2D rectangle /// 2D rectangle
@ -73,6 +80,9 @@ struct Rect {
/// returns bottom right point of rectangle /// returns bottom right point of rectangle
@property Point bottomRight() { return Point(right, bottom); } @property Point bottomRight() { return Point(right, bottom); }
/// returns size (width, height) in Point
@property Point size() { return Point(right - left, bottom - top); }
/// add offset to horizontal and vertical coordinates /// add offset to horizontal and vertical coordinates
void offset(int dx, int dy) { void offset(int dx, int dy) {
left += dx; left += dx;

View File

@ -267,14 +267,44 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
/// Set adapter to hold grid model data /// Set adapter to hold grid model data
@property GridWidgetBase gridModelAdapter(GridModelAdapter adapter) { _gridModelAdapter = adapter; return this; } @property GridWidgetBase gridModelAdapter(GridModelAdapter adapter) { _gridModelAdapter = adapter; return this; }
protected bool _smoothHScroll = true;
/// Get smooth horizontal scroll flag - when true - scrolling by pixels, when false - by cells
@property bool smoothHScroll() { return _smoothHScroll; }
/// Get smooth horizontal scroll flag - when true - scrolling by pixels, when false - by cells
@property GridWidgetBase smoothHScroll(bool flgSmoothScroll) {
if (_smoothHScroll != flgSmoothScroll) {
_smoothHScroll = flgSmoothScroll;
// TODO: snap to grid if necessary
updateScrollBars();
}
return this;
}
protected bool _smoothVScroll = true;
/// Get smooth vertical scroll flag - when true - scrolling by pixels, when false - by cells
@property bool smoothVScroll() { return _smoothVScroll; }
/// Get smooth vertical scroll flag - when true - scrolling by pixels, when false - by cells
@property GridWidgetBase smoothVScroll(bool flgSmoothScroll) {
if (_smoothVScroll != flgSmoothScroll) {
_smoothVScroll = flgSmoothScroll;
// TODO: snap to grid if necessary
updateScrollBars();
}
return this;
}
/// column count (including header columns and fixed columns) /// column count (including header columns and fixed columns)
protected int _cols; protected int _cols;
/// row count (including header rows and fixed rows) /// row count (including header rows and fixed rows)
protected int _rows; protected int _rows;
/// column widths /// column widths
protected int[] _colWidths; protected int[] _colWidths;
/// total width from first column to right of this
protected int[] _colCumulativeWidths;
/// row heights /// row heights
protected int[] _rowHeights; protected int[] _rowHeights;
/// total height from first row to bottom of this
protected int[] _rowCumulativeHeights;
/// when true, shows col headers row /// when true, shows col headers row
protected bool _showColHeaders; protected bool _showColHeaders;
/// when true, shows row headers column /// when true, shows row headers column
@ -287,10 +317,12 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
protected int _fixedCols; protected int _fixedCols;
/// number of fixed (non-scrollable) rows /// number of fixed (non-scrollable) rows
protected int _fixedRows; protected int _fixedRows;
/// column scroll offset, relative to last fixed col; 0 = not scrolled
protected int _scrollCol; /// scroll X offset in pixels
/// row scroll offset, relative to last fixed row; 0 = not scrolled protected int _scrollX;
protected int _scrollRow; /// scroll Y offset in pixels
protected int _scrollY;
/// selected cell column /// selected cell column
protected int _col; protected int _col;
/// selected cell row /// selected cell row
@ -388,7 +420,7 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
/// set bool property value, for ML loaders /// set bool property value, for ML loaders
mixin(generatePropertySettersMethodOverride("setBoolProperty", "bool", mixin(generatePropertySettersMethodOverride("setBoolProperty", "bool",
"showColHeaders", "showColHeaders", "rowSelect")); "showColHeaders", "showColHeaders", "rowSelect", "smoothHScroll", "smoothVScroll"));
/// set int property value, for ML loaders /// set int property value, for ML loaders
mixin(generatePropertySettersMethodOverride("setIntProperty", "int", mixin(generatePropertySettersMethodOverride("setIntProperty", "int",
@ -424,6 +456,24 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
return this; return this;
} }
/// recalculate colCumulativeWidths, rowCumulativeHeights after resizes
protected void updateCumulativeSizes() {
_colCumulativeWidths.length = _colWidths.length;
_rowCumulativeHeights.length = _rowHeights.length;
for (int i = 0; i < _colCumulativeWidths.length; i++) {
if (i == 0)
_colCumulativeWidths[i] = _colWidths[i];
else
_colCumulativeWidths[i] = _colWidths[i] + _colCumulativeWidths[i - 1];
}
for (int i = 0; i < _rowCumulativeHeights.length; i++) {
if (i == 0)
_rowCumulativeHeights[i] = _rowHeights[i];
else
_rowCumulativeHeights[i] = _rowHeights[i] + _rowCumulativeHeights[i - 1];
}
}
/// set new size /// set new size
void resize(int c, int r) { void resize(int c, int r) {
if (c == cols && r == rows) if (c == cols && r == rows)
@ -438,164 +488,266 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
} }
_cols = c + _headerCols; _cols = c + _headerCols;
_rows = r + _headerRows; _rows = r + _headerRows;
updateCumulativeSizes();
} }
/// count of non-scrollable columns (header + fixed)
/// returns column width (index includes col/row headers, if any); returns 0 for columns hidden by scroll at the left @property int nonScrollCols() { return _headerCols + fixedCols; }
int colWidth(int x) { /// count of non-scrollable rows (header + fixed)
if (x >= _headerCols + fixedCols && x < _headerCols + fixedCols + _scrollCol) @property int nonScrollRows() { return _headerRows + fixedRows; }
return 0; /// return all (fixed + scrollable) cells size in pixels
return _colWidths[x]; @property Point fullAreaPixels() {
return Point(_cols ? _colCumulativeWidths[_cols - 1] : 0, _rows ? _rowCumulativeHeights[_rows - 1] : 0);
}
/// non-scrollable area size in pixels
@property Point nonScrollAreaPixels() {
int nscols = nonScrollCols;
int nsrows = nonScrollRows;
return Point(nscols ? _colCumulativeWidths[nscols - 1] : 0, nsrows ? _rowCumulativeHeights[nsrows - 1] : 0);
}
/// scrollable area size in pixels
@property Point scrollAreaPixels() {
return fullAreaPixels - nonScrollAreaPixels;
}
/// get cell rectangle (relative to client area) not counting scroll position
Rect cellRectNoScroll(int x, int y) {
if (x < 0 || y < 0 || x >= _cols || y >= _rows)
return Rect(0,0,0,0);
return Rect(x ? _colCumulativeWidths[x - 1] : 0, y ? _rowCumulativeHeights[y - 1] : 0,
_colCumulativeWidths[x], _rowCumulativeHeights[y]);
}
/// get cell rectangle moved by scroll pixels (may overlap non-scroll cols!)
Rect cellRectScroll(int x, int y) {
Rect rc = cellRectNoScroll(x, y);
int nscols = nonScrollCols;
int nsrows = nonScrollRows;
if (x >= nscols) {
rc.left -= _scrollX;
rc.right -= _scrollX;
}
if (y >= nsrows) {
rc.top -= _scrollY;
rc.bottom -= _scrollY;
}
return rc;
}
/// returns true if column is inside client area and not overlapped outside scroll area
bool colVisible(int x) {
if (x < 0 || x >= _cols)
return false;
if (x == 0)
return true;
int nscols = nonScrollCols;
if (x < nscols) {
// non-scrollable
return _colCumulativeWidths[x - 1] < _clientRect.width;
} else {
// scrollable
int start = _colCumulativeWidths[x - 1] - _scrollX;
int end = _colCumulativeWidths[x] - _scrollX;
if (start >= _clientRect.width)
return false; // at right
if (end <= (nscols ? _colCumulativeWidths[nscols - 1] : 0))
return false; // at left
return true; // visible
}
}
/// returns true if row is inside client area and not overlapped outside scroll area
bool rowVisible(int y) {
if (y < 0 || y >= _rows)
return false;
if (y == 0)
return true; // first row always visible
int nsrows = nonScrollRows;
if (y < nsrows) {
// non-scrollable
return _rowCumulativeHeights[y - 1] < _clientRect.height;
} else {
// scrollable
int start = _rowCumulativeHeights[y - 1] - _scrollY;
int end = _rowCumulativeHeights[y] - _scrollY;
if (start >= _clientRect.height)
return false; // at right
if (end <= (nsrows ? _rowCumulativeHeights[nsrows - 1] : 0))
return false; // at left
return true; // visible
}
} }
void setColWidth(int x, int w) { void setColWidth(int x, int w) {
_colWidths[x] = w; _colWidths[x] = w;
} updateCumulativeSizes();
/// returns row height (index includes col/row headers, if any); returns 0 for riws hidden by scroll at the top
int rowHeight(int y) {
if (y >= _headerRows + fixedRows && y < _headerRows + fixedRows + _scrollRow)
return 0;
return _rowHeights[y];
} }
void setRowHeight(int y, int w) { void setRowHeight(int y, int w) {
_rowHeights[y] = w; _rowHeights[y] = w;
updateCumulativeSizes();
} }
/// returns cell rectangle relative to client area; row 0 is col headers row; col 0 is row headers column /// returns cell rectangle relative to client area; row 0 is col headers row; col 0 is row headers column
Rect cellRect(int x, int y) { Rect cellRect(int x, int y) {
Rect rc; return cellRectScroll(x, y);
int xx = 0;
for (int i = 0; i <= x; i++) {
if (i == x)
rc.left = xx;
xx += colWidth(i);
if (i == x) {
rc.right = xx;
break;
}
}
int yy = 0;
for (int i = 0; i <= y; i++) {
if (i == y)
rc.top = yy;
yy += rowHeight(i);
if (i == y) {
rc.bottom = yy;
break;
}
}
return rc;
} }
/// converts client rect relative coordinates to cell coordinates /// converts client rect relative coordinates to cell coordinates
bool pointToCell(int x, int y, ref int col, ref int row, ref Rect cellRect) { bool pointToCell(int x, int y, ref int col, ref int row, ref Rect cellRect) {
col = row = -1; int nscols = nonScrollCols;
cellRect = Rect(); int nsrows = nonScrollRows;
Rect rc; Point ns = nonScrollAreaPixels;
int xx = 0; col = colByAbsoluteX(x < ns.x ? x : x + _scrollX);
for (int i = 0; i < _cols; i++) { row = rowByAbsoluteY(y < ns.y ? y : y + _scrollY);
rc.left = xx; cellRect = cellRectScroll(col, row);
xx += colWidth(i); return cellRect.isPointInside(x, y);
rc.right = xx;
if (rc.left < rc.right && x >= rc.left && x < rc.right) {
col = i;
break;
}
if (xx > x)
break;
}
int yy = 0;
for (int i = 0; i < _rows; i++) {
rc.top = yy;
yy += rowHeight(i);
rc.bottom = yy;
if (rc.top < rc.bottom && y >= rc.top && y < rc.bottom) {
row = i;
break;
}
if (yy > y)
break;
}
if (col >= 0 && row >= 0) {
cellRect = rc;
return true;
}
return false;
} }
/// update scrollbar positions /// update scrollbar positions
override protected void updateScrollBars() { override protected void updateScrollBars() {
calcScrollableAreaPos(); calcScrollableAreaPos();
correctScrollPos();
super.updateScrollBars(); super.updateScrollBars();
} }
/// search for index of position inside cumulative sizes array
protected static int findPosIndex(int[] cumulativeSizes, int pos) {
// binary search
if (pos < 0 || !cumulativeSizes.length)
return 0;
int a = 0; // inclusive lower bound
int b = cast(int)cumulativeSizes.length; // exclusive upper bound
if (pos >= cumulativeSizes[$ - 1])
return b - 1;
int * w = cumulativeSizes.ptr;
for(;;) {
if (a + 1 >= b)
return a; // single point
// middle point
// always inside range
int c = (a + b) >> 1;
int start = c ? w[c - 1] : 0;
int end = w[c];
if (pos < start) {
// left
b = c;
} else if (pos >= end) {
// right
a = c + 1;
} else {
// found
return c;
}
}
}
/// column by X, ignoring scroll position /// column by X, ignoring scroll position
protected int colByAbsoluteX(int x) { protected int colByAbsoluteX(int x) {
int xx = 0; return findPosIndex(_colCumulativeWidths, x);
for (int i = 0; i < _cols; i++) {
int w = _colWidths[i];
if (x < xx + w || i == _cols - 1)
return i;
xx += w;
}
return 0;
} }
/// row by Y, ignoring scroll position /// row by Y, ignoring scroll position
protected int rowByAbsoluteY(int y) { protected int rowByAbsoluteY(int y) {
int yy = 0; return findPosIndex(_rowCumulativeHeights, y);
for (int i = 0; i < _rows; i++) {
int w = _rowHeights[i];
if (y < yy + w || i == _rows - 1)
return i;
yy += w;
} }
return 0;
/// returns first fully visible column in scroll area
protected int scrollCol() {
int x = nonScrollAreaPixels.x + _scrollX;
int col = colByAbsoluteX(x);
int start = col ? _colCumulativeWidths[col - 1] : 0;
int end = _colCumulativeWidths[col];
if (x <= start)
return col;
// align to next col
return colByAbsoluteX(end);
}
/// returns last fully visible column in scroll area
protected int lastScrollCol() {
int x = nonScrollAreaPixels.x + _scrollX + _visibleScrollableArea.width - 1;
int col = colByAbsoluteX(x);
int start = col ? _colCumulativeWidths[col - 1] : 0;
int end = _colCumulativeWidths[col];
if (x >= end - 1) // fully visible
return col;
if (col > nonScrollCols && col > scrollCol)
col--;
return col;
}
/// returns first fully visible row in scroll area
protected int scrollRow() {
int y = nonScrollAreaPixels.y + _scrollY;
int row = rowByAbsoluteY(y);
int start = row ? _rowCumulativeHeights[row - 1] : 0;
int end = _rowCumulativeHeights[row];
if (y <= start)
return row;
// align to next col
return rowByAbsoluteY(end);
}
/// returns last fully visible row in scroll area
protected int lastScrollRow() {
int y = nonScrollAreaPixels.y + _scrollY + _visibleScrollableArea.height - 1;
int row = rowByAbsoluteY(y);
int start = row ? _rowCumulativeHeights[row - 1] : 0;
int end = _rowCumulativeHeights[row];
if (y >= end - 1) // fully visible
return row;
if (row > nonScrollRows && row > scrollRow)
row--;
return row;
} }
/// move scroll position horizontally by dx, and vertically by dy; returns true if scrolled /// move scroll position horizontally by dx, and vertically by dy; returns true if scrolled
bool scrollBy(int dx, int dy) { bool scrollBy(int dx, int dy) {
return scrollTo(_headerCols + fixedCols + _scrollCol + dx, _headerRows + fixedRows + _scrollRow + dy); int col = scrollCol + dx;
int row = scrollRow + dy;
if (col >= _cols)
col = _cols - 1;
if (row >= _rows)
row = _rows - 1;
if (col < nonScrollCols)
col = nonScrollCols;
if (row < nonScrollCols)
row = nonScrollRows;
Rect rc = cellRectNoScroll(col, row);
Point ns = nonScrollAreaPixels;
return scrollTo(rc.left - ns.x, rc.top - ns.y);
}
/// override to support modification of client rect after change, e.g. apply offset
override protected void handleClientRectLayout(ref Rect rc) {
//correctScrollPos();
}
// ensure scroll position is inside min/max area
protected void correctScrollPos() {
int maxscrollx = _fullScrollableArea.width - _visibleScrollableArea.width;
int maxscrolly = _fullScrollableArea.height - _visibleScrollableArea.height;
if (_scrollX < 0)
_scrollX = 0;
if (_scrollY < 0)
_scrollY = 0;
if (_scrollX > maxscrollx)
_scrollX = maxscrollx;
if (_scrollY > maxscrolly)
_scrollY = maxscrolly;
} }
/// set scroll position to show specified cell as top left in scrollable area; col or row -1 value means no change /// set scroll position to show specified cell as top left in scrollable area; col or row -1 value means no change
bool scrollTo(int col, int row, GridWidgetBase source = null, bool doNotify = true) { bool scrollTo(int x, int y, GridWidgetBase source = null, bool doNotify = true) {
int oldx = _scrollCol; int oldx = _scrollX;
int oldy = _scrollRow; int oldy = _scrollY;
int newScrollCol = col == -1 ? _scrollCol : col - _headerCols - fixedCols; _scrollX = x;
int newScrollRow = row == -1 ? _scrollRow : row - _headerRows - fixedRows; _scrollY = y;
if (newScrollCol > _maxScrollCol) correctScrollPos();
newScrollCol = _maxScrollCol;
if (newScrollCol < 0)
newScrollCol = 0;
if (newScrollRow > _maxScrollRow)
newScrollRow = _maxScrollRow;
if (newScrollRow < 0)
newScrollRow = 0;
//bool changed = false;
if (newScrollCol >= 0 && newScrollCol + _headerCols + fixedCols < _cols) {
if (_scrollCol != newScrollCol) {
_scrollCol = newScrollCol;
//changed = true;
}
}
if (newScrollRow >= 0 && newScrollRow + _headerRows + fixedRows < _rows) {
if (_scrollRow != newScrollRow) {
_scrollRow = newScrollRow;
//changed = true;
}
}
//if (changed)
updateScrollBars(); updateScrollBars();
invalidate(); invalidate();
bool changed = oldx != _scrollCol || oldy != _scrollRow; bool changed = oldx != _scrollX || oldy != _scrollY;
if (doNotify && changed && viewScrolled.assigned) { if (doNotify && changed && viewScrolled.assigned) {
if (source is null) if (source is null)
source = this; source = this;
viewScrolled(source, col, row); viewScrolled(source, x, y);
} }
return changed; return changed;
} }
@ -603,8 +755,7 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
/// process horizontal scrollbar event /// process horizontal scrollbar event
override bool onHScroll(ScrollEvent event) { override bool onHScroll(ScrollEvent event) {
if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
int col = colByAbsoluteX(event.position + _fullScrollableArea.left); scrollTo(event.position, _scrollY);
scrollTo(col, _scrollRow + _headerRows + fixedRows);
} else if (event.action == ScrollAction.PageUp) { } else if (event.action == ScrollAction.PageUp) {
dispatchAction(new Action(GridActions.ScrollPageLeft)); dispatchAction(new Action(GridActions.ScrollPageLeft));
} else if (event.action == ScrollAction.PageDown) { } else if (event.action == ScrollAction.PageDown) {
@ -620,8 +771,7 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
/// process vertical scrollbar event /// process vertical scrollbar event
override bool onVScroll(ScrollEvent event) { override bool onVScroll(ScrollEvent event) {
if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
int row = rowByAbsoluteY(event.position + _fullScrollableArea.top); scrollTo(_scrollX, event.position);
scrollTo(_scrollCol + _headerCols + fixedCols, row);
} else if (event.action == ScrollAction.PageUp) { } else if (event.action == ScrollAction.PageUp) {
dispatchAction(new Action(GridActions.ScrollPageUp)); dispatchAction(new Action(GridActions.ScrollPageUp));
} else if (event.action == ScrollAction.PageDown) { } else if (event.action == ScrollAction.PageDown) {
@ -637,39 +787,36 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
/// ensure that cell is visible (scroll if necessary) /// ensure that cell is visible (scroll if necessary)
void makeCellVisible(int col, int row) { void makeCellVisible(int col, int row) {
bool scrolled = false; bool scrolled = false;
Rect rc = cellRect(col, row); int newx = _scrollX;
if (col >= _headerCols + fixedCols && col < _headerCols + fixedCols + _scrollCol) { int newy = _scrollY;
// scroll to the left Rect rc = cellRectNoScroll(col, row);
_scrollCol = col - _headerCols - fixedCols; Rect visibleRc = _visibleScrollableArea;
scrolled = true; if (col >= nonScrollCols) {
} else { // can scroll X
while (rc.right > _clientRect.width && _scrollCol < _cols - fixedCols - _headerCols - 1) { if (rc.left < visibleRc.left) {
if (_scrollCol == col - _headerCols - fixedCols) // scroll left
break; newx += rc.left - visibleRc.left;
_scrollCol++; } else if (rc.right > visibleRc.right) {
rc = cellRect(col, row); // scroll right
scrolled = true; newx += rc.right - visibleRc.right;
} }
} }
if (row >= _headerRows + fixedRows && row < _headerRows + fixedRows + _scrollRow) { if (row >= nonScrollRows) {
// scroll to the left // can scroll Y
_scrollRow = row - _headerRows - fixedRows; if (rc.top < visibleRc.top) {
scrolled = true; // scroll left
} else { newy += rc.top - visibleRc.top;
while (rc.bottom > _clientRect.height && _scrollRow < _rows - fixedRows - _headerRows - 1) { } else if (rc.bottom > visibleRc.bottom) {
if (_scrollRow == row - _headerRows - fixedRows) // scroll right
break; newy += rc.bottom - visibleRc.bottom;
_scrollRow++;
rc = cellRect(col, row);
scrolled = true;
} }
} }
if (scrolled) { if (newy < 0)
updateScrollBars(); newy = 0;
invalidate(); if (newx < 0)
if (viewScrolled.assigned) { newx = 0;
viewScrolled(this, _scrollCol + _headerCols + fixedCols, _scrollRow + _headerRows + fixedRows); if (newx != _scrollX || newy != _scrollY) {
} scrollTo(newx, newy);
} }
} }
@ -800,108 +947,52 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
/// calculate scrollable area info /// calculate scrollable area info
protected void calcScrollableAreaPos() { protected void calcScrollableAreaPos() {
_maxScrollCol = _maxScrollRow = 0; if (_scrollX < 0)
_fullyVisibleCells.left = _headerCols + fixedCols + _scrollCol; _scrollX = 0;
_fullyVisibleCells.top = _headerRows + fixedRows + _scrollRow; if (_scrollY < 0)
Rect rc; _scrollY = 0;
int xx = 0; // calculate _fullScrollableArea, _visibleScrollableArea relative to clientRect
for (int i = 0; i < _cols && xx < _clientRect.width; i++) { Point nonscrollPixels = nonScrollAreaPixels;
if (i == _fullyVisibleCells.left) { Point scrollPixels = scrollAreaPixels;
_fullyVisibleCellsRect.left = _fullyVisibleCellsRect.right = xx; Point fullPixels = fullAreaPixels;
} Point clientPixels = _clientRect.size;
int w = colWidth(i); Point scrollableClient = clientPixels - nonscrollPixels;
if (i >= _fullyVisibleCells.left && xx + w <= _clientRect.width) { if (scrollableClient.x < 0)
_fullyVisibleCellsRect.right = xx + w; scrollableClient.x = 0;
_fullyVisibleCells.right = i; if (scrollableClient.y < 0)
} scrollableClient.y = 0;
xx += w; _fullScrollableArea = Rect(nonscrollPixels.x, nonscrollPixels.y, fullPixels.x, fullPixels.y);
} if (_fullScrollableArea.right < clientPixels.x)
int yy = 0; _fullScrollableArea.right = clientPixels.x;
for (int i = 0; i < _rows && yy < _clientRect.height; i++) { if (_fullScrollableArea.bottom < clientPixels.y)
if (i == _fullyVisibleCells.top) _fullScrollableArea.bottom = clientPixels.y;
_fullyVisibleCellsRect.top = _fullyVisibleCellsRect.bottom = yy;
int w = rowHeight(i); // extending scroll area if necessary
if (i >= _fullyVisibleCells.top && yy + w <= _clientRect.height) { int maxscrollx = _fullScrollableArea.right - scrollableClient.x;
_fullyVisibleCellsRect.bottom = yy + w; int col = colByAbsoluteX(maxscrollx);
_fullyVisibleCells.bottom = i; int maxscrolly = _fullScrollableArea.bottom - scrollableClient.y;
} int row = rowByAbsoluteY(maxscrolly);
yy += w; Rect rc = cellRectNoScroll(col, row);
// extend scroll area to show full column at left when scrolled to rightmost column
if (maxscrollx >= nonscrollPixels.x && rc.left < maxscrollx) {
_fullScrollableArea.right += rc.right - maxscrollx;
} }
int maxVisibleScrollWidth = _clientRect.width - _fullyVisibleCellsRect.left; // extend scroll area to show full row at top when scrolled to end row
int maxVisibleScrollHeight = _clientRect.height - _fullyVisibleCellsRect.top; if (maxscrolly >= nonscrollPixels.y && rc.top < maxscrolly) {
if (maxVisibleScrollWidth < 0) _fullScrollableArea.bottom += rc.bottom - maxscrolly;
maxVisibleScrollWidth = 0; }
if (maxVisibleScrollHeight < 0)
maxVisibleScrollHeight = 0;
// scrollable area
// calc scroll area in pixels Point scrollableClientAreaSize = scrollableClient; // size left for scrollable area
xx = 0; int scrollx = nonscrollPixels.x + _scrollX;
for (int i = 0; i < _cols; i++) { int scrolly = nonscrollPixels.y + _scrollY;
if (i == _headerCols + fixedCols) { _visibleScrollableArea = Rect(scrollx, scrolly, scrollx + scrollableClientAreaSize.x, scrolly + scrollableClientAreaSize.y);
_fullScrollableArea.left = xx;
}
if (i == _fullyVisibleCells.left) {
_visibleScrollableArea.left = xx;
}
int w = _colWidths[i];
xx += w;
if (i >= _headerCols + fixedCols) {
_fullScrollableArea.right = xx;
}
if (i >= _fullyVisibleCells.left) {
_visibleScrollableArea.right = xx;
}
}
xx = 0;
for (int i = _cols - 1; i >= _headerCols + fixedCols; i--) {
int w = _colWidths[i];
if (xx + w > maxVisibleScrollWidth) {
_fullScrollableArea.right += maxVisibleScrollWidth - xx;
break;
}
_maxScrollCol = i - _headerCols - fixedCols;
xx += w;
}
yy = 0;
for (int i = 0; i < _rows; i++) {
if (i == _headerRows + fixedRows) {
_fullScrollableArea.top = yy;
}
if (i == _fullyVisibleCells.top) {
_visibleScrollableArea.top = yy;
}
int w = _rowHeights[i];
yy += w;
if (i >= _headerRows + fixedRows) {
_fullScrollableArea.bottom = yy;
}
if (i >= _fullyVisibleCells.top) {
_visibleScrollableArea.bottom = yy;
}
}
yy = 0;
for (int i = _rows - 1; i >= _headerRows + fixedRows; i--) {
int w = _rowHeights[i];
if (yy + w > maxVisibleScrollHeight) {
_fullScrollableArea.bottom += maxVisibleScrollHeight - yy;
break;
}
_maxScrollRow = i - _headerRows - fixedRows;
yy += w;
}
// crop scroll area by client rect
//if (visibleScrollableArea.width > maxVisibleScrollWidth)
_visibleScrollableArea.right = _visibleScrollableArea.left + maxVisibleScrollWidth;
//if (visibleScrollableArea.height > maxVisibleScrollHeight)
_visibleScrollableArea.bottom = _visibleScrollableArea.top + maxVisibleScrollHeight;
} }
protected int _maxScrollCol; protected int _maxScrollCol;
protected int _maxScrollRow; protected int _maxScrollRow;
protected Rect _fullyVisibleCells;
protected Rect _fullyVisibleCellsRect;
override protected bool handleAction(const Action a) { override protected bool handleAction(const Action a) {
calcScrollableAreaPos(); calcScrollableAreaPos();
@ -926,6 +1017,8 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
} }
} }
int sc = scrollCol; // first fully visible column in scroll area
int sr = scrollRow; // first fully visible row in scroll area
switch (actionId) with(GridActions) switch (actionId) with(GridActions)
{ {
case ActivateCell: case ActivateCell:
@ -953,7 +1046,7 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
selectCell(_col, _row - 1); selectCell(_col, _row - 1);
return true; return true;
case ScrollDown: case ScrollDown:
if (_fullyVisibleCells.bottom < _rows - 1) if (lastScrollRow < _rows - 1)
scrollBy(0, 1); scrollBy(0, 1);
return true; return true;
case Down: case Down:
@ -961,42 +1054,42 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
return true; return true;
case ScrollPageLeft: case ScrollPageLeft:
// scroll left cell by cell // scroll left cell by cell
int prevCol = _headerCols + fixedCols + _scrollCol; while (scrollCol > nonScrollCols) {
while (_scrollCol > 0) {
scrollBy(-1, 0); scrollBy(-1, 0);
if (_fullyVisibleCells.right <= prevCol) if (lastScrollCol <= sc)
break; break;
} }
return true; return true;
case ScrollPageRight: case ScrollPageRight:
int prevCol = _fullyVisibleCells.right; int prevCol = lastScrollCol;
while (_headerCols + fixedCols + _scrollCol < prevCol) { while (scrollCol < prevCol) {
if (!scrollBy(1, 0)) if (!scrollBy(1, 0))
break; break;
} }
return true; return true;
case ScrollPageUp: case ScrollPageUp:
// scroll up line by line // scroll up line by line
int prevRow = _headerRows + fixedRows + _scrollRow; while (scrollRow > nonScrollRows) {
while (_scrollRow > 0) {
scrollBy(0, -1); scrollBy(0, -1);
if (_fullyVisibleCells.bottom <= prevRow) if (lastScrollRow <= sr)
break; break;
} }
return true; return true;
case ScrollPageDown: case ScrollPageDown:
int prevRow = _fullyVisibleCells.bottom; int prevRow = lastScrollRow;
while (_headerRows + fixedRows + _scrollRow < prevRow) { while (scrollRow < prevRow) {
if (!scrollBy(0, 1)) if (!scrollBy(0, 1))
break; break;
} }
return true; return true;
case LineBegin: case LineBegin:
if (_scrollCol > 0 && _col > _headerCols + fixedCols + _scrollCol && !_rowSelect) if (sc > nonScrollCols && _col > sc && !_rowSelect) {
selectCell(_headerCols + fixedCols + _scrollCol, _row); // move selection and don's scroll
else { selectCell(sc, _row);
if (_scrollCol > 0) { } else {
_scrollCol = 0; // scroll
if (sc > nonScrollCols) {
_scrollX = 0;
updateScrollBars(); updateScrollBars();
invalidate(); invalidate();
} }
@ -1004,11 +1097,16 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
} }
return true; return true;
case LineEnd: case LineEnd:
if (_col < lastScrollCol && !_rowSelect) {
// move selection and don's scroll
selectCell(lastScrollCol, _row);
} else {
selectCell(_cols - 1, _row); selectCell(_cols - 1, _row);
}
return true; return true;
case DocumentBegin: case DocumentBegin:
if (_scrollRow > 0) { if (_scrollY > 0) {
_scrollRow = 0; _scrollY = 0;
updateScrollBars(); updateScrollBars();
invalidate(); invalidate();
} }
@ -1018,35 +1116,26 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
selectCell(_col, _rows - 1); selectCell(_col, _rows - 1);
return true; return true;
case PageBegin: case PageBegin:
if (_scrollRow > 0) if (scrollRow > nonScrollRows)
selectCell(_col, _headerRows + fixedRows + _scrollRow); selectCell(_col, scrollRow);
else else
selectCell(_col, _headerRows); selectCell(_col, _headerRows);
return true; return true;
case PageEnd: case PageEnd:
int found = -1; selectCell(_col, lastScrollRow);
for (int i = fixedRows; i < _rows; i++) {
Rect rc = cellRect(_col, i);
if (rc.bottom <= _clientRect.height)
found = i;
else
break;
}
if (found >= 0)
selectCell(_col, found);
return true; return true;
case PageUp: case PageUp:
if (_row > _fullyVisibleCells.top) { if (_row > sr) {
// not at top scrollable cell // not at top scrollable cell
selectCell(_col, _fullyVisibleCells.top); selectCell(_col, sr);
} else { } else {
// at top of scrollable area // at top of scrollable area
if (_scrollRow > 0) { if (scrollRow > nonScrollRows) {
// scroll up line by line // scroll up line by line
int prevRow = _row; int prevRow = _row;
for (int i = prevRow - 1; i >= _headerRows; i--) { for (int i = prevRow - 1; i >= _headerRows; i--) {
selectCell(_col, i); selectCell(_col, i);
if (_fullyVisibleCells.bottom <= prevRow) if (lastScrollRow <= prevRow)
break; break;
} }
} else { } else {
@ -1056,17 +1145,18 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
} }
return true; return true;
case PageDown: case PageDown:
if (_row < _rows) { if (_row < _rows - 1) {
if (_row < _fullyVisibleCells.bottom) { int lr = lastScrollRow;
// not at top scrollable cell if (_row < lr) {
selectCell(_col, _fullyVisibleCells.bottom); // not at bottom scrollable cell
selectCell(_col, lr);
} else { } else {
// scroll down // scroll down
int prevRow = _row; int prevRow = _row;
for (int i = prevRow + 1; i < _rows; i++) { for (int i = prevRow + 1; i < _rows; i++) {
selectCell(_col, i); selectCell(_col, i);
calcScrollableAreaPos(); calcScrollableAreaPos();
if (_fullyVisibleCells.top >= prevRow) if (scrollRow >= prevRow)
break; break;
} }
} }
@ -1088,45 +1178,46 @@ class GridWidgetBase : ScrollWidgetBase, GridModelAdapter, MenuItemActionHandler
} }
override protected void drawClient(DrawBuf buf) { override protected void drawClient(DrawBuf buf) {
if (!_cols || !_rows)
return; // no cells
auto saver = ClipRectSaver(buf, _clientRect, 0); auto saver = ClipRectSaver(buf, _clientRect, 0);
//buf.fillRect(_clientRect, 0x80A08080);
Rect rc; int nscols = nonScrollCols;
for (int phase = 0; phase < 2; phase++) { int nsrows = nonScrollRows;
int yy = 0; Point nspixels = nonScrollAreaPixels;
for (int y = 0; y < _rows; y++) { int maxVisibleCol = colByAbsoluteX(_clientRect.width + _scrollX);
int rh = rowHeight(y); int maxVisibleRow = rowByAbsoluteY(_clientRect.height + _scrollY);
rc.top = yy; for (int phase = 0; phase < 2; phase++) { // phase0 == background, phase1 == foreground
rc.bottom = yy + rh; for (int y = 0; y <= maxVisibleRow; y++) {
if (rh == 0) if (!rowVisible(y))
continue; continue;
if (yy > _clientRect.height) for (int x = 0; x <= maxVisibleCol; x++) {
break; if (!colVisible(x))
yy += rh;
int xx = 0;
for (int x = 0; x < _cols; x++) {
int cw = colWidth(x);
rc.left = xx;
rc.right = xx + cw;
if (cw == 0)
continue; continue;
if (xx > _clientRect.width) Rect cellRect = cellRectScroll(x, y);
break; Rect clippedCellRect = cellRect;
xx += cw; if (x >= nscols && cellRect.left < nspixels.x)
// draw cell clippedCellRect.left = nspixels.x; // clip scrolled left
Rect cellRect = rc; if (y >= nsrows && cellRect.top < nspixels.y)
clippedCellRect.top = nspixels.y; // clip scrolled left
if (clippedCellRect.empty)
continue; // completely clipped out
cellRect.moveBy(_clientRect.left, _clientRect.top); cellRect.moveBy(_clientRect.left, _clientRect.top);
auto cellSaver = ClipRectSaver(buf, cellRect, 0); clippedCellRect.moveBy(_clientRect.left, _clientRect.top);
auto cellSaver = ClipRectSaver(buf, clippedCellRect, 0);
bool isHeader = x < _headerCols || y < _headerRows; bool isHeader = x < _headerCols || y < _headerRows;
if (phase == 0) { if (phase == 0) {
if (isHeader) if (isHeader)
drawHeaderCellBackground(buf, buf.clipRect, x - _headerCols, y - _headerRows); drawHeaderCellBackground(buf, cellRect, x - _headerCols, y - _headerRows);
else else
drawCellBackground(buf, buf.clipRect, x - _headerCols, y - _headerRows); drawCellBackground(buf, cellRect, x - _headerCols, y - _headerRows);
} else { } else {
if (isHeader) if (isHeader)
drawHeaderCell(buf, buf.clipRect, x - _headerCols, y - _headerRows); drawHeaderCell(buf, cellRect, x - _headerCols, y - _headerRows);
else else
drawCell(buf, buf.clipRect, x - _headerCols, y - _headerRows); drawCell(buf, cellRect, x - _headerCols, y - _headerRows);
} }
} }
} }