UNPKG

lazy-widgets

Version:

Typescript retained mode GUI for the HTML canvas API

1,147 lines 46.1 kB
import { PointerEvent } from '../events/PointerEvent.js'; import { AutoScrollEvent } from '../events/AutoScrollEvent.js'; import { TabSelectEvent } from '../events/TabSelectEvent.js'; import { BaseTheme } from '../theme/BaseTheme.js'; import { DynMsg } from '../core/Strings.js'; import { eventEmitterHandleEvent, eventEmitterOff, eventEmitterOffAny, eventEmitterOn, eventEmitterOnAny } from '../helpers/WidgetEventEmitter-premade-functions.js'; import { PropagationModel } from "../events/WidgetEvent.js"; /** * 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 class Widget extends BaseTheme { /** * How much this widget will expand relative to other widgets in a flexbox * container. If changed, sets {@link Widget#_layoutDirty} to true. */ get flex() { return this._flex; } set flex(flex) { if (flex !== this._flex) { this._flex = flex; this._layoutDirty = true; } } /** * 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() { return this._flexShrink; } set flexShrink(flexShrink) { if (flexShrink !== this._flexShrink) { this._flexShrink = flexShrink; this._layoutDirty = true; } } /** * 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() { return this._flexBasis; } set flexBasis(flexBasis) { if (flexBasis !== this._flexBasis) { this._flexBasis = flexBasis; this._layoutDirty = true; } } /** * Minimum width of widget. Defaults to 0. If changed, sets * {@link Widget#_minWidth} to true. */ get minWidth() { return this._minWidth; } set minWidth(minWidth) { if (minWidth !== this._minWidth) { this._minWidth = minWidth; this._layoutDirty = true; } } /** * Maximum width of widget. Defaults to Infinity. If changed, sets * {@link Widget#_maxWidth} to true. */ get maxWidth() { return this._maxWidth; } set maxWidth(maxWidth) { if (maxWidth !== this._maxWidth) { this._maxWidth = maxWidth; this._layoutDirty = true; } } /** * Minimum height of widget. Defaults to 0. If changed, sets * {@link Widget#_minHeight} to true. */ get minHeight() { return this._minHeight; } set minHeight(minHeight) { if (minHeight !== this._minHeight) { this._minHeight = minHeight; this._layoutDirty = true; } } /** * Maximum height of widget. Defaults to Infinity. If changed, sets * {@link Widget#_maxHeight} to true. */ get maxHeight() { return this._maxHeight; } set maxHeight(maxHeight) { if (maxHeight !== this._maxHeight) { this._maxHeight = maxHeight; this._layoutDirty = true; } } constructor(properties) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; super(properties); /** * If this is true, widget needs their layout resolved. If implementing a * container, propagate this up. */ this._layoutDirty = true; /** Width of widget in pixels. */ this.width = 0; /** Height of widget in pixels. */ this.height = 0; /** Absolute horizontal offset of widget in pixels. */ this.x = 0; /** Absolute vertical offset of widget in pixels. */ this.y = 0; /** * 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. */ this.idealWidth = 0; /** The ideal height of the widget in pixels. See {@link Widget#width}. */ this.idealHeight = 0; /** * 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. */ this.idealX = 0; /** * The ideal absolute vertical offset of the widget in pixels. See * {@link Widget#x}. */ this.idealY = 0; /** * 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. */ this._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. */ this._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. */ this._parent = null; /** Can this widget be focused by pressing tab? */ this.tabFocusable = false; /** {@link Widget#active} but for internal use. */ this._active = false; /** Typed user listeners attached to this Widget */ this.typedListeners = new Map(); /** Untyped user listeners attached to this Widget */ this.untypedListeners = []; /** Next user listener ID */ this.nextListener = 0; /** Internal field for {@link Widget#id}. */ this._id = null; /** * Last {@link idealWidth}. Internal field used for marking widgets as * dirty. */ this.lastIdealWidth = 0; /** * Last {@link idealHeight}. Internal field used for marking widgets as * dirty. */ this.lastIdealHeight = 0; this._enabled = (_a = properties === null || properties === void 0 ? void 0 : properties.enabled) !== null && _a !== void 0 ? _a : true; this._flex = (_b = properties === null || properties === void 0 ? void 0 : properties.flex) !== null && _b !== void 0 ? _b : 0; this._flexShrink = (_c = properties === null || properties === void 0 ? void 0 : properties.flexShrink) !== null && _c !== void 0 ? _c : 0; this._flexBasis = (_d = properties === null || properties === void 0 ? void 0 : properties.flexBasis) !== null && _d !== void 0 ? _d : null; this._minWidth = (_e = properties === null || properties === void 0 ? void 0 : properties.minWidth) !== null && _e !== void 0 ? _e : 0; this._maxWidth = (_f = properties === null || properties === void 0 ? void 0 : properties.maxWidth) !== null && _f !== void 0 ? _f : Infinity; this._minHeight = (_g = properties === null || properties === void 0 ? void 0 : properties.minHeight) !== null && _g !== void 0 ? _g : 0; this._maxHeight = (_h = properties === null || properties === void 0 ? void 0 : properties.maxHeight) !== null && _h !== void 0 ? _h : Infinity; this.id = (_j = properties === null || properties === void 0 ? void 0 : properties.id) !== null && _j !== void 0 ? _j : null; } /** * 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) { if (enabled === this._enabled) { return; } this._enabled = enabled; this.updateActiveState(); } get enabled() { return this._enabled; } /** * The inherited theme of this widget. Sets {@link BaseTheme#fallbackTheme}. */ set inheritedTheme(theme) { this.fallbackTheme = theme; } get inheritedTheme() { return this.fallbackTheme; } /** * 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() { return [this.width, this.height]; } /** * 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() { return [this.idealWidth, this.idealHeight]; } /** * 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() { return [this.x, this.y]; } /** * 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() { return [this.idealX, this.idealY]; } /** Get the rectangle bounds (left, right, top, bottom) of this widget. */ get bounds() { const x = this.x; const y = this.y; return [x, x + this.width, y, y + this.height]; } /** Similar to {@link Widget#bounds}, but uses ideal values */ get idealBounds() { const x = this.idealX; const y = this.idealY; return [x, x + this.idealWidth, y, y + this.idealHeight]; } /** Get the rectangle (x, y, width, height) of this widget. */ get rect() { return [this.x, this.y, this.width, this.height]; } /** Similar to {@link Widget#rect}, but uses ideal values */ get idealRect() { return [this.idealX, this.idealY, this.idealWidth, this.idealHeight]; } /** * Check if the widget's layout is dirty. Returns * {@link Widget#_layoutDirty}. */ get layoutDirty() { return this._layoutDirty; } /** * 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() { return this.width == 0 || this.height == 0; } /** * 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() { return this._id; } set id(id) { if (id === this._id) { return; } // request/set id const oldID = this._id; if (id !== null && this._root) { this._root.requestID(id, this); } this._id = id; // drop id if (this._root && oldID !== null) { this._root.dropID(oldID); } } /** * 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. */ handleEvent(baseEvent) { if (baseEvent.propagation === PropagationModel.Trickling) { if (baseEvent.target === this) { return this; } else { return null; } } else if (baseEvent.propagation === PropagationModel.Bubbling) { if (this._parent) { return this._parent.dispatchEvent(baseEvent); } else if (this._root) { this._root.dispatchEvent(baseEvent); } } return 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) { // ignore event if widget is disabled if (!this._enabled) { return null; } if (baseEvent.propagation === PropagationModel.Trickling) { const event = baseEvent; // ignore event if the event is targetted but not a descendant of // this widget (or not this widget), or if the event is an // untargetted pointer event outside the bounds of the widget let isPointerEvent = null; if (event.target === null) { isPointerEvent = event instanceof PointerEvent; // XXX typescript is being derpy again, so we have to typecast // everything to a pointer event despite having a guard // right before the usage if (isPointerEvent) { const pointerEvent = event; if (pointerEvent.x < this.x || pointerEvent.y < this.y || pointerEvent.x >= this.x + this.width || pointerEvent.y >= this.y + this.height) { return null; } } } else if (event.target !== this) { // XXX trace back the event target. if we are an ascendant of // the target, continue, otherwise, stop and don't capture. this // is probably going to be a bottleneck, however, we shouldn't // cache the "trace" of the event, since the tree can change // while traversing the ui tree let head = event.target._parent; while (head !== this) { if (head === null) { return null; } head = head._parent; } } // if this is a pointer event, mark as being hovered if (isPointerEvent === null) { isPointerEvent = event instanceof PointerEvent; } if (isPointerEvent) { this.root.markHovered(this); } // dispatch to user event listeners if (eventEmitterHandleEvent(this, this.typedListeners, this.untypedListeners, baseEvent)) { return this; } // handle special case for auto-scroll event if (event.type === AutoScrollEvent.type && event.originallyRelativeTo === this) { return this; } // handle event let capturer = null; if (event.reversed) { capturer = this.handleEvent(event); } if (event.isa(TabSelectEvent)) { if (event.reachedRelative) { if (this.tabFocusable && (capturer === this || capturer === null)) { return this; } } else if (event.relativeTo === this) { event.reachedRelative = true; } } if (!event.reversed) { capturer = this.handleEvent(event); } return capturer; } else { // dispatch to user event listeners if (eventEmitterHandleEvent(this, this.typedListeners, this.untypedListeners, baseEvent)) { return this; } return this.handleEvent(baseEvent); } } /** * Generic update method which is called before layout is resolved. Does * nothing by default. Should be implemented. */ // eslint-disable-next-line @typescript-eslint/no-empty-function handlePreLayoutUpdate() { } /** * Generic update method which is called before layout is resolved. Calls * {@link Widget#handlePreLayoutUpdate} if widget is enabled. Must not be * overridden. */ preLayoutUpdate() { if (this._enabled) { this.handlePreLayoutUpdate(); } } /** * 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, maxWidth, minHeight, maxHeight) { // Return early if disabled; make widget dimensionless and clear layout // dirty flag if (!this._enabled) { this.width = 0; this.height = 0; this.idealWidth = 0; this.idealHeight = 0; this._layoutDirty = false; return; } // Apply self-constraints minWidth = Math.max(minWidth, this._minWidth); minHeight = Math.max(minHeight, this._minHeight); maxWidth = Math.min(maxWidth, this._maxWidth); maxHeight = Math.min(maxHeight, this._maxHeight); // Validate constraints // FIXME find a better solution for reporting constraint warnings // removed warnings for now if (minWidth == Infinity) { throw new Error(DynMsg.INVALID_VALUE('minWidth', minWidth)); } if (minWidth > maxWidth) { // Not throwing here because floating pointer precision errors // sometimes trigger this due to tight constraints // console.warn(DynMsg.SWAPPED_MIN_MAX_DIMS(minWidth, maxWidth, 'minWidth', 'maxWidth')); minWidth = maxWidth; } if (minWidth < 0) { // console.warn(DynMsg.NEGATIVE_DIMS(minWidth, 'minWidth')); minWidth = 0; } if (minHeight == Infinity) { throw new Error(DynMsg.INVALID_VALUE('minHeight', minHeight)); } if (minHeight > maxHeight) { // console.warn(DynMsg.SWAPPED_MIN_MAX_DIMS(minHeight, maxHeight, 'minHeight', 'maxHeight')); minHeight = maxHeight; } if (minHeight < 0) { // console.warn(DynMsg.NEGATIVE_DIMS(minHeight, 'minHeight')); minHeight = 0; } // Resolve dimensions this.handleResolveDimensions(minWidth, maxWidth, minHeight, maxHeight); // Validate resolved dimensions, handling overflows, underflows and // invalid dimensions if (this.idealWidth < minWidth) { // console.warn(DynMsg.BROKEN_CONSTRAINTS(this.idealWidth, minWidth, true, false)); this.idealWidth = minWidth; } else if (this.idealWidth > maxWidth) { // console.warn(DynMsg.BROKEN_CONSTRAINTS(this.idealWidth, maxWidth, true, true)); this.idealWidth = maxWidth; } if (this.idealWidth < 0 || !isFinite(this.idealWidth) || isNaN(this.idealWidth)) { throw new Error(DynMsg.INVALID_DIMS(true, this.idealWidth)); } if (this.idealHeight < minHeight) { // console.warn(DynMsg.BROKEN_CONSTRAINTS(this.idealHeight, minHeight, false, false)); this.idealHeight = minHeight; } else if (this.idealHeight > maxHeight) { // console.warn(DynMsg.BROKEN_CONSTRAINTS(this.idealHeight, maxHeight, false, true)); this.idealHeight = maxHeight; } if (this.idealHeight < 0 || !isFinite(this.idealHeight) || isNaN(this.idealHeight)) { throw new Error(DynMsg.INVALID_DIMS(false, this.idealHeight)); } // Clear layout dirty flag this._layoutDirty = false; } /** * 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, y) { // Set position this.idealX = x; this.idealY = y; } /** * 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() { // Round bounds const [scaleX, scaleY] = this.viewport.effectiveScale; const newX = Math.floor(this.idealX * scaleX) / scaleX; const newY = Math.floor(this.idealY * scaleY) / scaleY; const newWidth = Math.ceil((this.idealX + this.idealWidth) * scaleX) / scaleX - newX; const newHeight = Math.ceil((this.idealY + this.idealHeight) * scaleY) / scaleY - newY; // Mark as dirty if bounds have changed (with old bounds) const changedBounds = newX !== this.x || newY !== this.y || newWidth !== this.width || newHeight !== this.height; if (changedBounds) { this.markWholeAsDirty(); } // Set final bounds this.x = newX; this.y = newY; this.width = newWidth; this.height = newHeight; // Mark as dirty if bounds have changed (with new bounds) if (changedBounds) { this.markWholeAsDirty(); } } /** * Generic update method which is called after layout is resolved. Does * nothing by default. Should be implemented. */ // eslint-disable-next-line @typescript-eslint/no-empty-function handlePostLayoutUpdate() { } /** * Generic update method which is called after layout is resolved. Calls * {@link Widget#handlePostLayoutUpdate} if widget is enabled. Must not be * overridden. */ postLayoutUpdate() { if (this._enabled) { this.handlePostLayoutUpdate(); // if the rendering of this widget depends on the ideal dimensions // of the widget (e.g. if the widget uses the widget's ideal // dimensions to render the thickness of a line), then we need to // mark the widget as dirty in case the ideal dimensions changed // without changing the actual finalized dimensions. // this is checked here instead of in resolveDimensions so that // unnecesary paint damage isn't propagated in the probing phase // (when widgets are resolved with infinite bounds, since that will // likely change the ideal width, but will soon be invalidated by a // second resolveDimensions call with finite bounds) if (this.idealWidth !== this.lastIdealWidth || this.idealHeight !== this.lastIdealHeight) { this.lastIdealWidth = this.idealWidth; this.lastIdealHeight = this.idealHeight; this.markWholeAsDirty(); } } } /** * 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 */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars handlePainting(dirtyRects) { } /** * 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) { // TODO in-place clamping to reduce GC? if (this.dimensionless || dirtyRects.length === 0) { return; } const widgetRight = this.x + this.width; const widgetBottom = this.y + this.height; const effectiveDirtyRects = []; for (const rect of dirtyRects) { // check if damage intersects widget const origLeft = rect[0]; if (origLeft >= widgetRight) { continue; } const origRight = origLeft + rect[2]; if (origRight <= this.x) { continue; } const origTop = rect[1]; if (origTop >= widgetBottom) { continue; } const origBottom = origTop + rect[3]; if (origBottom <= this.y) { continue; } // clamp damage region const left = Math.max(origLeft, this.x); const top = Math.max(origTop, this.y); const right = Math.min(origRight, widgetRight); const bottom = Math.min(origBottom, widgetBottom); effectiveDirtyRects.push([left, top, right - left, bottom - top]); } if (effectiveDirtyRects.length === 0) { return; } if (this._enabled) { this.handlePainting(effectiveDirtyRects); } } /** * 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() { return this._root !== null; } /** * Similar to {@link Widget#_root}, but throws an error if the widget is not * {@link Widget#attached}. */ get root() { if (!this.attached) { throw new Error(DynMsg.DETACHED_WIDGET('root')); } // XXX attached makes sure that _root is not null, but typescript // doesn't detect this. force the type system to treat it as non-null return this._root; } /** * Similar to {@link Widget#_viewport}, but throws an error if the widget is * not {@link Widget#attached}. */ get viewport() { if (!this.attached) { throw new Error(DynMsg.DETACHED_WIDGET('viewport')); } // XXX attached makes sure that _root is not null, but typescript // doesn't detect this. force the type system to treat it as non-null return this._viewport; } /** * Similar to {@link Widget#_parent}, but throws an error if the widget is * not {@link Widget#attached}. */ get parent() { if (!this.attached) { throw new Error(DynMsg.DETACHED_WIDGET('parent')); } return this._parent; } /** * 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. */ handleAttachment() { } /** * 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. */ handleDetachment() { } /** * 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, viewport, parent) { if (this.attached) { throw new Error(DynMsg.INVALID_ATTACHMENT(true)); } if (this._id !== null) { root.requestID(this._id, this); } this._root = root; this._viewport = viewport; this._parent = parent; this.handleAttachment(); this.updateActiveState(); } /** * 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() { if (!this.attached) { throw new Error(DynMsg.INVALID_ATTACHMENT(false)); } this.handleDetachment(); const root = this._root; if (this._id !== null) { root.dropID(this._id); } root.dropFoci(this); root.clearPointerStylesFromWidget(this); this._root = null; this._viewport = null; this._parent = null; this.updateActiveState(); } /** * 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() { return this._active; } /** * 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() { const oldActive = this._active; this._active = false; if (this._enabled && this.attached) { if (this._parent === null) { // XXX typescript doesn't know that attached implies that _root // is not null, hence the type cast this._active = this._root.enabled; } else { this._active = this._parent.active; } } if (!oldActive && this._active) { this.activate(); return true; } else if (oldActive && !this._active) { this.deactivate(); return true; } else { return false; } } /** * 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. */ activate() { this.markWholeAsDirty(); this._layoutDirty = true; } /** * 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. */ deactivate() { this._layoutDirty = true; if (this.attached) { this.unsafeMarkAsDirty([this.x, this.y, this.width, this.height]); this._root.dropFoci(this); this._root.clearPointerStylesFromWidget(this); } } /** * {@link AutoScrollEvent | Auto-scroll} to this widget. Uses the whole * widget as the {@link AutoScrollEvent#bounds | auto-scroll bounds}. */ autoScroll() { this.root.dispatchEvent(new AutoScrollEvent(this, [0, this.idealWidth, 0, this.idealHeight])); } /** * 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) { this.markAsDirty(rect); } /** * 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. */ unsafeMarkAsDirty(rect) { if (this._parent) { this._parent.propagateDirtyRect(rect); } else if (this._viewport) { this._viewport.markDirtyRect(rect); } else { console.warn('Could not mark rectangle as dirty; Widget is in invalid state (_active is true, but _parent and _viewport are null)'); } } /** * 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. */ markAsDirty(rect) { if (this._active) { this.unsafeMarkAsDirty(rect); } } /** * Mark the entire widget as dirty. Convenience method that calls * {@link Widget#markAsDirty}. */ markWholeAsDirty() { this.markAsDirty([this.x, this.y, this.width, this.height]); } /** * 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, relativeTo = null) { if (relativeTo === this) { return rect; } else if (this._parent === null) { if (relativeTo !== null) { throw new Error("Can't query rectangle relative to this widget; relative widget is not an ascendant of the starting widget"); } return rect; } else { return this._parent.queryRect(rect, relativeTo); } } /** * 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, relativeTo = null) { if (this._parent === null) { if (relativeTo !== null) { throw new Error("Can't query rectangle relative to this widget; relative widget is not an ascendant of the starting widget"); } return rect; } else { return this._parent.queryRect(rect, relativeTo); } } /** * 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, y, relativeTo = null) { if (relativeTo === this) { return [x, y]; } else if (this._parent === null) { if (relativeTo !== null) { throw new Error("Can't query point relative to this widget; relative widget is not an ascendant of the starting widget"); } return [x, y]; } else { return this._parent.queryPoint(x, y, relativeTo); } } /** * 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, y, relativeTo = null) { if (this._parent === null) { if (relativeTo !== null) { throw new Error("Can't query point relative to this widget; relative widget is not an ascendant of the starting widget"); } return [x, y]; } else { return this._parent.queryPoint(x, y, relativeTo); } } /** * 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, listener, once = false) { eventEmitterOn(this.nextListener, this.typedListeners, eventType, listener, once); this.nextListener++; return 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) { eventEmitterOnAny(this.nextListener, this.untypedListeners, listener); this.nextListener++; return 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, listener, once = false) { return eventEmitterOff(this.typedListeners, eventType, listener, once); } /** * 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) { return eventEmitterOffAny(this.untypedListeners, listener); } /** * Request a pointer style from the currently attached {@link Root}. * Convenience method, which just calls {@link Root#requestPointerStyle} * with `this` as the Widget. */ requestPointerStyle(pointerStyle, source) { this.root.requestPointerStyle(this, pointerStyle, source); } /** * Clear the pointer style from the currently attached {@link Root}. * Convenience method, which just calls {@link Root#clearPointerStyle} with * `this` as the Widget. */ clearPointerStyle(source) { this.root.clearPointerStyle(this, source); } } /** * 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. */ Widget.autoXML = null; //# sourceMappingURL=Widget.js.map