UNPKG

lazy-widgets

Version:

Typescript retained mode GUI for the HTML canvas API

657 lines (656 loc) 28.3 kB
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; }