pokerogue/src/ui/scrollable-grid-handler.ts

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;
}
}