198 lines
7.3 KiB
TypeScript
198 lines
7.3 KiB
TypeScript
import { Button } from "#enums/buttons";
|
|
import UiHandler from "#app/ui/ui-handler";
|
|
import { ScrollBar } from "#app/ui/scroll-bar";
|
|
|
|
type UpdateGridCallbackFunction = () => void;
|
|
type UpdateDetailsCallbackFunction = (index: number) => void;
|
|
|
|
/**
|
|
* A helper class to handle navigation through a grid of elements that can scroll vertically
|
|
* Needs to be used by a {@linkcode UiHandler}
|
|
* How to use:
|
|
* - in `UiHandler.setup`: Initialize with the {@linkcode UiHandler} that handles the grid,
|
|
* the number of rows and columns that can be shown at once,
|
|
* an optional {@linkcode ScrollBar}, and optional callbacks that will get called after scrolling
|
|
* - in `UiHandler.show`: Set `setTotalElements` to the total number of elements in the list to display
|
|
* - in `UiHandler.processInput`: call `processNavigationInput` to have it handle the cursor updates while calling the defined callbacks
|
|
* - in `UiHandler.clear`: call `reset`
|
|
*/
|
|
export default class ScrollableGridUiHandler {
|
|
private readonly ROWS: number;
|
|
private readonly COLUMNS: number;
|
|
private handler: UiHandler;
|
|
private totalElements: number;
|
|
private cursor: number;
|
|
private scrollCursor: number;
|
|
private scrollBar?: ScrollBar;
|
|
private updateGridCallback?: UpdateGridCallbackFunction;
|
|
private updateDetailsCallback?: UpdateDetailsCallbackFunction;
|
|
|
|
/**
|
|
* @param scene the {@linkcode UiHandler} that needs its cursor updated based on the scrolling
|
|
* @param rows the maximum number of rows shown at once
|
|
* @param columns the maximum number of columns shown at once
|
|
* @param updateGridCallback optional function that will get called if the whole grid needs to get updated
|
|
* @param updateDetailsCallback optional function that will get called if a single element's information needs to get updated
|
|
*/
|
|
constructor(handler: UiHandler, rows: number, columns: number) {
|
|
this.handler = handler;
|
|
this.ROWS = rows;
|
|
this.COLUMNS = columns;
|
|
this.scrollCursor = 0;
|
|
this.cursor = 0;
|
|
this.totalElements = rows * columns; // default value for the number of elements
|
|
}
|
|
|
|
/**
|
|
* Set a scrollBar to get updated with the scrolling
|
|
* @param scrollBar {@linkcode ScrollBar}
|
|
* @returns this
|
|
*/
|
|
withScrollBar(scrollBar: ScrollBar): ScrollableGridUiHandler {
|
|
this.scrollBar = scrollBar;
|
|
this.scrollBar.setTotalRows(Math.ceil(this.totalElements / this.COLUMNS));
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set function that will get called if the whole grid needs to get updated
|
|
* @param callback {@linkcode UpdateGridCallbackFunction}
|
|
* @returns this
|
|
*/
|
|
withUpdateGridCallBack(callback: UpdateGridCallbackFunction): ScrollableGridUiHandler {
|
|
this.updateGridCallback = callback;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set function that will get called if a single element in the grid needs to get updated
|
|
* @param callback {@linkcode UpdateDetailsCallbackFunction}
|
|
* @returns this
|
|
*/
|
|
withUpdateSingleElementCallback(callback: UpdateDetailsCallbackFunction): ScrollableGridUiHandler {
|
|
this.updateDetailsCallback = callback;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* @param totalElements the total number of elements that the grid needs to display
|
|
*/
|
|
setTotalElements(totalElements: number) {
|
|
this.totalElements = totalElements;
|
|
if (this.scrollBar) {
|
|
this.scrollBar.setTotalRows(Math.ceil(this.totalElements / this.COLUMNS));
|
|
}
|
|
this.setScrollCursor(0);
|
|
}
|
|
|
|
/**
|
|
* @returns how many elements are hidden due to scrolling
|
|
*/
|
|
getItemOffset(): number {
|
|
return this.scrollCursor * this.COLUMNS;
|
|
}
|
|
|
|
/**
|
|
* Update the cursor and scrollCursor based on user input
|
|
* @param button the button that was pressed
|
|
* @returns `true` if either the cursor or scrollCursor was updated
|
|
*/
|
|
processInput(button: Button): boolean {
|
|
let success = false;
|
|
const onScreenRows = Math.min(this.ROWS, Math.ceil(this.totalElements / this.COLUMNS));
|
|
const maxScrollCursor = Math.max(0, Math.ceil(this.totalElements / this.COLUMNS) - onScreenRows);
|
|
const currentRowIndex = Math.floor(this.cursor / this.COLUMNS);
|
|
const currentColumnIndex = this.cursor % this.COLUMNS;
|
|
const itemOffset = this.scrollCursor * this.COLUMNS;
|
|
const lastVisibleIndex = Math.min(this.totalElements - 1, this.totalElements - maxScrollCursor * this.COLUMNS - 1);
|
|
switch (button) {
|
|
case Button.UP:
|
|
if (currentRowIndex > 0) {
|
|
success = this.setCursor(this.cursor - this.COLUMNS);
|
|
} else if (this.scrollCursor > 0) {
|
|
success = this.setScrollCursor(this.scrollCursor - 1);
|
|
} else {
|
|
// wrap around to the last row
|
|
let newCursor = this.cursor + (onScreenRows - 1) * this.COLUMNS;
|
|
if (newCursor > lastVisibleIndex) {
|
|
newCursor -= this.COLUMNS;
|
|
}
|
|
success = this.setScrollCursor(maxScrollCursor, newCursor);
|
|
}
|
|
break;
|
|
case Button.DOWN:
|
|
if (currentRowIndex < onScreenRows - 1) {
|
|
// Go down one row
|
|
success = this.setCursor(Math.min(this.cursor + this.COLUMNS, this.totalElements - itemOffset - 1));
|
|
} else if (this.scrollCursor < maxScrollCursor) {
|
|
// Scroll down one row
|
|
success = this.setScrollCursor(this.scrollCursor + 1);
|
|
} else {
|
|
// Wrap around to the top row
|
|
success = this.setScrollCursor(0, this.cursor % this.COLUMNS);
|
|
}
|
|
break;
|
|
case Button.LEFT:
|
|
if (currentColumnIndex > 0) {
|
|
success = this.setCursor(this.cursor - 1);
|
|
} else if (this.scrollCursor === maxScrollCursor && currentRowIndex === onScreenRows - 1) {
|
|
success = this.setCursor(lastVisibleIndex);
|
|
} else {
|
|
success = this.setCursor(this.cursor + this.COLUMNS - 1);
|
|
}
|
|
break;
|
|
case Button.RIGHT:
|
|
if (currentColumnIndex < this.COLUMNS - 1 && this.cursor + itemOffset < this.totalElements - 1) {
|
|
success = this.setCursor(this.cursor + 1);
|
|
} else {
|
|
success = this.setCursor(this.cursor - currentColumnIndex);
|
|
}
|
|
break;
|
|
}
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* Reset the scrolling
|
|
*/
|
|
reset(): void {
|
|
this.setScrollCursor(0);
|
|
this.setCursor(0);
|
|
}
|
|
|
|
private setCursor(cursor: number): boolean {
|
|
this.cursor = cursor;
|
|
return this.handler.setCursor(cursor);
|
|
}
|
|
|
|
private setScrollCursor(scrollCursor: number, cursor?: number): boolean {
|
|
const scrollChanged = scrollCursor !== this.scrollCursor;
|
|
|
|
// update the scrolling cursor
|
|
if (scrollChanged) {
|
|
this.scrollCursor = scrollCursor;
|
|
if (this.scrollBar) {
|
|
this.scrollBar.setScrollCursor(scrollCursor);
|
|
}
|
|
if (this.updateGridCallback) {
|
|
this.updateGridCallback();
|
|
}
|
|
}
|
|
|
|
let cursorChanged = false;
|
|
const newElementIndex = this.cursor + this.scrollCursor * this.COLUMNS;
|
|
if (cursor !== undefined) {
|
|
cursorChanged = this.setCursor(cursor);
|
|
} else if (newElementIndex >= this.totalElements) {
|
|
// make sure the cursor does not go past the end of the list
|
|
cursorChanged = this.setCursor(this.totalElements - this.scrollCursor * this.COLUMNS - 1);
|
|
} else if (scrollChanged && this.updateDetailsCallback) {
|
|
// scroll was changed but not the normal cursor, update the selected element
|
|
this.updateDetailsCallback(newElementIndex);
|
|
}
|
|
|
|
return scrollChanged || cursorChanged;
|
|
}
|
|
|
|
}
|