lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
657 lines (656 loc) • 28.3 kB
TypeScript
import { ThemeProperties } from "../theme/ThemeProperties.js";
import { BaseTheme } from '../theme/BaseTheme.js';
import { WidgetEvent } from "../events/WidgetEvent.js";
import type { Viewport } from '../core/Viewport.js';
import type { Bounds } from '../helpers/Bounds.js';
import type { Theme } from '../theme/Theme.js';
import type { Rect } from '../helpers/Rect.js';
import type { Root } from '../core/Root.js';
import type { WidgetEventEmitter, WidgetEventListener } from '../events/WidgetEventEmitter.js';
import type { WidgetAutoXML } from "../xml/WidgetAutoXML.js";
/**
* Optional Widget constructor properties.
*
* @category Widget
*/
export interface WidgetProperties extends ThemeProperties {
/** Sets {@link Widget#enabled}. */
enabled?: boolean;
/** Sets {@link Widget#flex}. */
flex?: number;
/** Sets {@link Widget#flexShrink}. */
flexShrink?: number;
/** Sets {@link Widget#flexBasis}. */
flexBasis?: number | null;
/** Sets {@link Widget#minWidth}. */
minWidth?: number;
/** Sets {@link Widget#maxWidth}. */
maxWidth?: number;
/** Sets {@link Widget#minHeight}. */
minHeight?: number;
/** Sets {@link Widget#maxHeight}. */
maxHeight?: number;
/** Sets {@link Widget#id}. */
id?: string | null;
}
/**
* A generic widget. All widgets extend this class. All widgets extend
* {@link BaseTheme} so that the theme in use can be overridden.
*
* @category Widget
*/
export declare abstract class Widget extends BaseTheme implements WidgetEventEmitter {
/**
* Input mapping for automatically generating a widget factory for a
* {@link BaseXMLUIParser} with {@link BaseXMLUIParser#autoRegisterFactory}.
* If null, then {@link BaseXMLUIParser#registerFactory} must be manually
* called by the user.
*
* This static property must be overridden by a concrete child class if you
* want to provide auto-factory support.
*/
static autoXML: WidgetAutoXML | null;
/** {@link Widget#enabled} but for internal use. */
private _enabled;
/**
* If this is true, widget needs their layout resolved. If implementing a
* container, propagate this up.
*/
protected _layoutDirty: boolean;
/** Width of widget in pixels. */
protected width: number;
/** Height of widget in pixels. */
protected height: number;
/** Absolute horizontal offset of widget in pixels. */
protected x: number;
/** Absolute vertical offset of widget in pixels. */
protected y: number;
/**
* The ideal width of the widget in pixels; if non-integer widget dimensions
* were allowed, the widget would have this size. Use this for layout
* calculations, but never use this for painting so that subpixel issues are
* avoided.
*/
protected idealWidth: number;
/** The ideal height of the widget in pixels. See {@link Widget#width}. */
protected idealHeight: number;
/**
* The ideal absolute horizontal offset of the widget in pixels; if
* non-integer positions were allowed, the widget would have this position.
* Use this for layout calculations, but never use this for painting so that
* subpixel issues are avoided.
*/
protected idealX: number;
/**
* The ideal absolute vertical offset of the widget in pixels. See
* {@link Widget#x}.
*/
protected idealY: number;
/** {@link Widget#flex} but for internal use. */
protected _flex: number;
/** {@link Widget#flexShrink} but for internal use. */
protected _flexShrink: number;
/** {@link Widget#flexBasis} but for internal use. */
protected _flexBasis: number | null;
/** {@link Widget#minWidth} but for internal use. */
protected _minWidth: number;
/** {@link Widget#maxWidth} but for internal use. */
protected _maxWidth: number;
/** {@link Widget#minHeight} but for internal use. */
protected _minHeight: number;
/** {@link Widget#maxHeight} but for internal use. */
protected _maxHeight: number;
/**
* The {@link Root} that this widget is currently inside.
*
* Widgets not {@link Widget#attached} to a UI tree will have this property
* set to null.
*/
protected _root: Root | null;
/**
* The {@link Viewport} that this widget is currently painting to. A UI tree
* can have multiple Viewports due to {@link ViewportWidget}, so this is not
* equivalent to {@link Root#viewport}.
*
* Widgets not {@link Widget#attached} to a UI tree will have this property
* set to null.
*/
protected _viewport: Viewport | null;
/**
* The parent {@link Widget} of this widget.
*
* Widgets not {@link Widget#attached} to a UI tree will have this property
* set to null, but root widgets will also have a null parent.
*/
protected _parent: Widget | null;
/** Can this widget be focused by pressing tab? */
protected tabFocusable: boolean;
/** {@link Widget#active} but for internal use. */
private _active;
/** Typed user listeners attached to this Widget */
private typedListeners;
/** Untyped user listeners attached to this Widget */
private untypedListeners;
/** Next user listener ID */
private nextListener;
/** Internal field for {@link Widget#id}. */
private _id;
/**
* Last {@link idealWidth}. Internal field used for marking widgets as
* dirty.
*/
private lastIdealWidth;
/**
* Last {@link idealHeight}. Internal field used for marking widgets as
* dirty.
*/
private lastIdealHeight;
/**
* How much this widget will expand relative to other widgets in a flexbox
* container. If changed, sets {@link Widget#_layoutDirty} to true.
*/
get flex(): number;
set flex(flex: number);
/**
* How much this widget will shrink relative to other widgets in a flexbox
* container if the maximum size is exceeded. If changed, sets
* {@link Widget#_layoutDirty} to true.
*/
get flexShrink(): number;
set flexShrink(flexShrink: number);
/**
* The starting length of this widget when inside a flexbox, which overrides
* the intrinsic length of this widget (the natural length of the content).
* If `null` (default), then the widget's intrinsic length will be used. If
* changed, sets {@link Widget#_layoutDirty} to true.
*
* Note that, if set, minimum and maximum self-constraints will still be
* respected.
*/
get flexBasis(): number | null;
set flexBasis(flexBasis: number | null);
/**
* Minimum width of widget. Defaults to 0. If changed, sets
* {@link Widget#_minWidth} to true.
*/
get minWidth(): number;
set minWidth(minWidth: number);
/**
* Maximum width of widget. Defaults to Infinity. If changed, sets
* {@link Widget#_maxWidth} to true.
*/
get maxWidth(): number;
set maxWidth(maxWidth: number);
/**
* Minimum height of widget. Defaults to 0. If changed, sets
* {@link Widget#_minHeight} to true.
*/
get minHeight(): number;
set minHeight(minHeight: number);
/**
* Maximum height of widget. Defaults to Infinity. If changed, sets
* {@link Widget#_maxHeight} to true.
*/
get maxHeight(): number;
set maxHeight(maxHeight: number);
constructor(properties?: Readonly<WidgetProperties>);
/**
* Is this widget enabled? If it isn't, it will act as if it doesn't exist,
* but will still be present in the UI tree.
*/
set enabled(enabled: boolean);
get enabled(): boolean;
/**
* The inherited theme of this widget. Sets {@link BaseTheme#fallbackTheme}.
*/
set inheritedTheme(theme: Theme | undefined);
get inheritedTheme(): Theme | undefined;
/**
* Get the resolved dimensions. Returns a 2-tuple containing
* {@link Widget#width} and {@link Widget#height}.
*
* Use {@link Widget#idealDimensions} for layout calculations.
*/
get dimensions(): [number, number];
/**
* Get the resolved ideal dimensions. Returns a 2-tuple containing
* {@link Widget#idealWidth} and {@link Widget#idealHeight}.
*
* Use this for layout calculations, and {@link Widget#dimensions} for
* painting.
*/
get idealDimensions(): [number, number];
/**
* Get the resolved position. Returns a 2-tuple containing {@link Widget#x}
* and {@link Widget#y}.
*
* Use {@link Widget#idealPosition} for layout calculations.
*/
get position(): [number, number];
/**
* Get the resolved ideal position. Returns a 2-tuple containing
* {@link Widget#idealX} and {@link Widget#idealY}.
*
* Use this for layout calculations, and {@link Widget#position} for
* painting.
*/
get idealPosition(): [number, number];
/** Get the rectangle bounds (left, right, top, bottom) of this widget. */
get bounds(): Bounds;
/** Similar to {@link Widget#bounds}, but uses ideal values */
get idealBounds(): Bounds;
/** Get the rectangle (x, y, width, height) of this widget. */
get rect(): Rect;
/** Similar to {@link Widget#rect}, but uses ideal values */
get idealRect(): Rect;
/**
* Check if the widget's layout is dirty. Returns
* {@link Widget#_layoutDirty}.
*/
get layoutDirty(): boolean;
/**
* Check if the widget has zero width or height. If true, then
* {@link Widget#paint} will do nothing, which usually happens when
* containers overflow.
*/
get dimensionless(): boolean;
/**
* The unique ID of this Widget. If the Widget has no ID, this value will be
* null. Uniqueness is tested per-UI tree; ID uniqueness is enforced when
* the ID is changed or when the Widget is attached to a {@link Root}.
*
* If the ID is already taken, setting the ID will have no effect and an
* error will be thrown.
*/
get id(): string | null;
set id(id: string | null);
/**
* Widget event handling callback. If the event is to be captured, the
* capturer is returned, else, null.
*
* By default, this will do nothing and capture the event if it is targeted
* at itself. Bubbling events will be automatically dispatched to the parent
* or root. Sticky events will be ignored.
*
* If overriding, return the widget that has captured the event (could be
* `this`, for example, or a child widget if implementing a container), or
* null if no widget captured the event. Make sure to not capture any events
* that you do not need, or you may have unexpected results; for example, if
* you capture all dispatched events indiscriminately, a
* {@link TabSelectEvent} event may be captured and result in weird
* behaviour when the user attempts to use tab to select another widget.
*
* Parent widgets should dispatch {@link TricklingEvent | TricklingEvents}
* to children. All widgets should dispatch
* {@link BubblingEvent | BubblingEvents} to the {@link Widget#parent} or
* {@link Widget#root}, if available. {@link StickyEvent | StickyEvents}
* should never be dispatched to children or parents.
*
* Note that bubbling events captured by a Root will return null, since
* there is no capturing **Widget**.
*
* Since the default handleEvent implementation already correctly handles
* bubbling and sticky events, it's a good idea to call super.handleEvent on
* these cases to avoid rewriting code, after transforming the event if
* necessary.
*/
protected handleEvent(baseEvent: WidgetEvent): Widget | null;
/**
* Called when an event is passed to the Widget. Must not be overridden.
* Dispatches to user event listeners first; if a user listener captures the
* event, then `this` is returned.
*
* For trickling events:
* Checks if the target matches the Widget, unless the Widget propagates
* events, or if the event is a {@link PointerEvent} and is in the bounds of
* the Widget. If neither of the conditions are true, the event is not
* captured (null is returned), else, the {@link Widget#handleEvent} method
* is called and its result is returned.
*
* For bubbling or sticky events:
* Passes the event to the handleEvent method and returns the result.
*
* @returns Returns the widget that captured the event or null if none captured the event.
*/
dispatchEvent(baseEvent: WidgetEvent): Widget | null;
/**
* Generic update method which is called before layout is resolved. Does
* nothing by default. Should be implemented.
*/
protected handlePreLayoutUpdate(): void;
/**
* Generic update method which is called before layout is resolved. Calls
* {@link Widget#handlePreLayoutUpdate} if widget is enabled. Must not be
* overridden.
*/
preLayoutUpdate(): void;
/**
* Resolve dimensions of this widget. Must be implemented; set
* {@link Widget#width} and {@link Widget#height}.
*/
protected abstract handleResolveDimensions(minWidth: number, maxWidth: number, minHeight: number, maxHeight: number): void;
/**
* Wrapper for {@link Widget#handleResolveDimensions}. Does nothing if
* {@link Widget#enabled} is false. If the resolved dimensions change,
* the widget is marked as dirty. {@link Widget#_layoutDirty} is set to
* false. If the widget is not loose and the layout has non-infinite max
* constraints, then the widget is stretched to fit max constraints. Must
* not be overridden.
*/
resolveDimensions(minWidth: number, maxWidth: number, minHeight: number, maxHeight: number): void;
/**
* Set the ideal position of this widget ({@link Widget#idealX} and
* {@link Widget#idealY}). Does not set any flags of the widget.
*
* Can be overridden, but `super.resolvePosition` must always be called, and
* the arguments must be preserved. Container widgets should override this
* method such that `resolvePosition` is called for each child of the
* container.
*/
resolvePosition(x: number, y: number): void;
/**
* Sets {@link Widget#x}, {@link Widget#y}, {@link Widget#width} and
* {@link Widget#y} from {@link Widget#idealX}, {@link Widget#idealY},
* {@link Widget#idealWidth} and {@link Widget#idealHeight} by rounding
* them. If the final values have changed, {@link Widget#markWholeAsDirty}
* is called.
*
* Can be overridden, but `super.finalizeBounds` must still be called; if
* you have parts of the widget that can be pre-calculated when the layout
* is known, such as the length and offset of a {@link Checkbox},
* then this is the perfect method to override, since it's only called after
* the layout is resolved to final (non-ideal) values, is only called if
* needed (unlike {@link postLayoutUpdate}, which is always called after the
* layout phase) and can be used to compare old and new positions and
* dimensions.
*
* Abstract container widgets such as {@link Parent} must always override
* this and call `finalizeBounds` on each child widget.
*/
finalizeBounds(): void;
/**
* Generic update method which is called after layout is resolved. Does
* nothing by default. Should be implemented.
*/
protected handlePostLayoutUpdate(): void;
/**
* Generic update method which is called after layout is resolved. Calls
* {@link Widget#handlePostLayoutUpdate} if widget is enabled. Must not be
* overridden.
*/
postLayoutUpdate(): void;
/**
* Widget painting callback. Should be overridden; does nothing by default.
* Do painting logic here when extending Widget.
*
* It's safe to repaint the whole widget even if only a part of the widget
* is damaged, since the painting is automatically clipped to the damage
* regions, however, it's preferred to only repaint the damaged parts for
* performance reasons.
*
* All passed dirty rectangles intersect the widget, have an area greater
* than 0, and are clamped to the widget bounds.
*
* The painting logic of this widget can modify the rendering context in a
* way that changes rendering for its children, however, the context must be
* kept clean for the parent of this widget; operations like clipping for
* children are OK, but make sure to restore the context to a state with no
* clipping when the painting logic is finished. This does not apply to some
* basic context properties such as fillStyle or strokeStyle; you are
* expected to set these on every handlePainting, do not assume the state of
* these properties.
*
* @param dirtyRects - The damaged regions that need to be re-painted, as a list of dirty rectangles
*/
protected handlePainting(dirtyRects: Array<Rect>): void;
/**
* Called when the Widget needs to be re-painted and the Root is being
* rendered. Does nothing if none of the dirty rectangles intersect the
* widget or the widget is {@link Widget#dimensionless}, else, calls the
* {@link Widget#handlePainting} method. Must not be overridden.
*
* @param dirtyRects - The damaged regions that need to be re-painted, as a list of dirty rectangles
*/
paint(dirtyRects: Array<Rect>): void;
/**
* Check if this Widget is attached to a UI tree. If not, then this Widget
* must not be used. Must not be overridden.
*/
get attached(): boolean;
/**
* Similar to {@link Widget#_root}, but throws an error if the widget is not
* {@link Widget#attached}.
*/
get root(): Root;
/**
* Similar to {@link Widget#_viewport}, but throws an error if the widget is
* not {@link Widget#attached}.
*/
get viewport(): Viewport;
/**
* Similar to {@link Widget#_parent}, but throws an error if the widget is
* not {@link Widget#attached}.
*/
get parent(): Widget | null;
/**
* Called when the Widget is attached to a UI tree. Must be overridden by
* container widgets to attach children or for resource management, but
* `child.attach` must be called instead of `child.handleAttachment`.
*
* Note that, to call `attach` on a child widget, you need a root, viewport
* and parent. The parent would be `this`, but the root and viewport are not
* passed to this method, unlike in `attach`. This is because most of the
* time you won't be implementing a container widget, so these parameters
* are just an unnecessary hassle. Get them from `this.root` and
* `this.viewport` instead (or `this._root` and `this._viewport`),
* respectively.
*
* `super.handleAttachment` doesn't have to be called and does nothing by
* default, unless you are deriving a class that has overridden this method.
* If you're not sure, it's safe to call super anyway.
*/
protected handleAttachment(): void;
/**
* Called when the Widget is detached from a UI tree. Must be overridden by
* container widgets to detach children or for resource management, but
* `child.detach` must be called instead of `child.handleDetachment`.
*
* `super.handleDetachment` doesn't have to be called and does nothing by
* default, unless you are deriving a class that has overridden this method.
* If you're not sure, it's safe to call super anyway.
*/
protected handleDetachment(): void;
/**
* Called when the Widget is attached to a UI tree. Must never be
* overridden. See {@link Widget#handleAttachment} instead.
*
* If the widget is already in a UI tree (already has a {@link parent} or is
* the {@link Root#child | root Widget}, both checked via
* {@link Widget#attached}), then this method will throw an exception; a
* Widget cannot be in multiple UI trees.
*
* @param root - The {@link Root} of the UI tree
* @param viewport - The {@link Viewport} in this part of the UI tree. A UI tree can have multiple nested Viewports due to {@link ViewportWidget}
* @param parent - The new parent of this Widget. If `null`, then this Widget has no parent and is the {@link Root#child | root Widget}
*/
attach(root: Root, viewport: Viewport, parent: Widget | null): void;
/**
* Called when the Widget is detached from a UI tree. Must never be
* overridden. See {@link Widget#handleDetachment} instead.
*
* Sets {@link Widget#_root}, {@link Widget#_viewport} and
* {@link Widget#_parent} to null.
*
* Drops all foci set to this Widget.
*
* If the widget was not in a UI tree, then an exception is thrown.
*/
detach(): void;
/**
* Is the Widget attached to a UI tree, enabled and in a UI sub-tree where
* all ascendants are enabled?
*
* Can only be updated by calling {@link Widget#updateActiveState}, although
* this should never be done manually; only done automatically by container
* Widgets and Roots.
*/
get active(): boolean;
/**
* Update the {@link Widget#active} state of the Widget. If the active state
* changes from `false` to `true`, then {@link Widget#activate} is called.
* If the active state changes from `true` to `false`, then
* {@link Widget#deactivate} is called.
*
* Container Widgets must override this so that the active state of each
* child is updated, but `super.updateActiveState` must still be called.
* Each child's active state must only be updated if the container's active
* state changed; this is indicated by the return value of this method.
*
* @returns Returns true if the active state changed.
*/
updateActiveState(): boolean;
/**
* Called after the Widget is attached to a UI tree, its parent is
* {@link Widget#active} (or {@link Root} is enabled if this is the top
* Widget), and the Widget itself is enabled; only called when all of the
* previous conditions are fulfilled, not when one of the conditions is
* fulfilled. Should be overridden for resource management, but
* `super.activate` must be called.
*
* Must not be propagated to children by container Widgets. This is already
* done automatically by {@link Widget#updateActiveState}.
*
* Marks {@link Widget#layoutDirty} as true, and marks the whole widget as
* dirty.
*/
protected activate(): void;
/**
* Called when the Widget is no longer {@link Widget#active}. Should be
* overridden for resource management, but `super.deactivate` must be
* called.
*
* Must not be propagated to children by container Widgets. This is already
* done automatically by {@link Widget#updateActiveState}.
*
* Marks {@link Widget#layoutDirty} as true and drops all foci set to this
* Widget if the Widget is attached.
*/
protected deactivate(): void;
/**
* {@link AutoScrollEvent | Auto-scroll} to this widget. Uses the whole
* widget as the {@link AutoScrollEvent#bounds | auto-scroll bounds}.
*/
autoScroll(): void;
/**
* Propagate a dirty rectangle from a child widget to the parent.
*
* Should be overridden by Widgets that transform their children, to correct
* the position and dimensions of the dirty rectangle.
*/
propagateDirtyRect(rect: Rect): void;
/**
* Similar to {@link Widget#markAsDirty}, but does not check whether the
* widget is active. For internal use only.
*
* markAsDirty actually calls this method; the other method is only a guard.
*/
private unsafeMarkAsDirty;
/**
* Mark a part of this widget as dirty. The dirty rectangle will be
* propagated via ascendant widgets until it reaches a CanvasViewport.
*
* If the widget is not active, then this method call is ignored.
*
* Must not be overridden; you probably want to override
* {@link Widget#propagateDirtyRect} instead.
*/
protected markAsDirty(rect: Rect): void;
/**
* Mark the entire widget as dirty. Convenience method that calls
* {@link Widget#markAsDirty}.
*/
protected markWholeAsDirty(): void;
/**
* Query the position and dimensions of a rectangle as if it were in the
* same coordinate origin as an ascendant widget (or the root). Call this
* from a child widget.
*
* Useful for reversing transformations made by ascendant widgets.
*
* Must be overridden by widgets that transform children or that have a
* different coordinate origin.
*/
queryRect(rect: Rect, relativeTo?: Widget | null): Rect;
/**
* Query the position and dimensions of a rectangle as if it were in the
* same coordinate origin as an ascendant widget (or the root).
*
* Useful for reversing transformations made by ascendant widgets.
*
* Must not be overridden.
*/
queryRectFromHere(rect: Rect, relativeTo?: Widget | null): Rect;
/**
* Query the position of a point as if it were in the same coordinate origin
* as an ascendant widget (or the root). Call this from a child widget.
*
* Useful for reversing transformations made by ascendant widgets.
*
* Must be overridden by widgets that transform children or that have a
* different coordinate origin.
*/
queryPoint(x: number, y: number, relativeTo?: Widget | null): [x: number, y: number];
/**
* Query the position of a point as if it were in the same coordinate origin
* as an ascendant widget (or the root).
*
* Useful for reversing transformations made by ascendant widgets.
*
* Must not be overridden.
*/
queryPointFromHere(x: number, y: number, relativeTo?: Widget | null): [x: number, y: number];
/**
* Listen to a specific event with a user listener. Only events that pass
* through this widget will be listened. Chainable.
*
* @param eventType - The {@link WidgetEvent#"type"} to listen to
* @param listener - The user-provided callback that will be invoked when the event is listened
* @param once - Should the listener only be invoked once? False by default
*/
on(eventType: string, listener: WidgetEventListener, once?: boolean): this;
/**
* Similar to {@link Widget#on}, but any event type invokes the
* user-provided callback, the listener can't be invoked only once, and the
* listener is called with a lower priority than specific event listeners.
* Chainable.
*
* @param listener - The user-provided callback that will be invoked when a event is listened
*/
onAny(listener: WidgetEventListener): this;
/**
* Remove an event listeners added with {@link Widget#on}. Not chainable.
*
* @param eventType - The {@link WidgetEvent#"type"} to stop listening to
* @param listener - The user-provided callback that was used in {@link Widget#on}
* @param once - Was the listener only meant to be invoked once? Must match what was used in {@link Widget#on}
*/
off(eventType: string, listener: WidgetEventListener, once?: boolean): boolean;
/**
* Remove an event listeners added with {@link Widget#onAny}. Not chainable.
*
* @param listener - The user-provided callback that was used in {@link Widget#onAny}
*/
offAny(listener: WidgetEventListener): boolean;
/**
* Request a pointer style from the currently attached {@link Root}.
* Convenience method, which just calls {@link Root#requestPointerStyle}
* with `this` as the Widget.
*/
protected requestPointerStyle(pointerStyle: string, source?: unknown): void;
/**
* Clear the pointer style from the currently attached {@link Root}.
* Convenience method, which just calls {@link Root#clearPointerStyle} with
* `this` as the Widget.
*/
protected clearPointerStyle(source?: unknown): void;
}