UNPKG

@unblessed/layout

Version:
522 lines (510 loc) 15.1 kB
import { Screen, Element } from '@unblessed/core'; import { Node } from 'yoga-layout'; /** * base.ts - Base class for widget descriptors * * This module defines the WidgetDescriptor base class that all widget types * extend. Each descriptor encapsulates: * - Typed props for the widget * - Flexbox property extraction * - Widget options extraction * - Event handler extraction * - Widget instance creation * * This eliminates the need for string-based type discrimination and large * switch statements throughout the codebase. */ /** * Base class for all widget descriptors. * * A WidgetDescriptor is responsible for: * 1. Storing typed props for a specific widget type * 2. Extracting flexbox-related props for Yoga layout * 3. Extracting widget-specific options for unblessed widgets * 4. Extracting event handlers for binding * 5. Creating the actual unblessed widget instance * * @template TProps - The typed props interface for this widget */ declare abstract class WidgetDescriptor<TProps = any> { props: TProps; /** * Widget type identifier (e.g., 'box', 'text', 'button') */ abstract readonly type: string; /** * Constructor stores the props * @param props - Typed props for this widget */ constructor(props: TProps); /** * Extract flexbox-related properties for Yoga layout engine. * Only include properties that affect layout (width, height, padding, etc.) * * @returns FlexboxProps for Yoga */ abstract get flexProps(): FlexboxProps; /** * Extract widget-specific options for unblessed widget creation. * Include visual properties (border, content, style) but NOT layout properties. * * @returns Options object for unblessed widget constructor */ abstract get widgetOptions(): Record<string, any>; /** * Extract event handlers from props. * * @returns Map of event name → handler function */ abstract get eventHandlers(): Record<string, Function>; /** * Create the actual unblessed widget instance. * Called after Yoga has calculated the layout. * * @param layout - Computed layout from Yoga (top, left, width, height) * @param screen - Screen instance to attach widget to * @returns Unblessed Element instance */ abstract createWidget(layout: ComputedLayout, screen: Screen): Element; /** * Update an existing widget with new layout and options. * * IMPORTANT: This method must preserve runtime hover/focus state. * When hover/focus effects are active, Screen.setEffects() modifies widget.style * and stores original values in temporary objects (_htemp, _ftemp). * We must not overwrite widget.style when effects are active. * * @param widget - Existing widget instance to update * @param layout - New computed layout from Yoga (with border adjustments already applied) */ updateWidget(widget: Element, layout: ComputedLayout): void; /** * Update temporary storage with new base values while preserving effect state. * When props change during hover/focus, we need to update what values will be * restored on mouseout/blur, but keep the current hover/focus styling active. * * @param temp - Temporary storage object (_htemp or _ftemp) * @param newBaseStyle - New base style from props * @param effects - Effects configuration (hoverEffects or focusEffects) */ private updateTempFromNewBase; /** * Deep clone options to avoid mutating shared object references. * * @param options - Options object to clone * @returns Deep cloned options object */ private deepCloneOptions; } /** * types.ts - Type definitions for @unblessed/layout */ /** * Flexbox style properties supported by the layout engine. * These map to Yoga layout properties. */ interface FlexboxProps { /** * Flex grow factor - how much the element should grow to fill available space. * @default 0 */ flexGrow?: number; /** * Flex shrink factor - how much the element should shrink when space is limited. * @default 1 */ flexShrink?: number; /** * Flex basis - initial size before flex grow/shrink is applied. */ flexBasis?: number | string; /** * Direction of flex items in the container. * @default 'column' */ flexDirection?: "row" | "column" | "row-reverse" | "column-reverse"; /** * How to wrap flex items. * @default 'nowrap' */ flexWrap?: "nowrap" | "wrap" | "wrap-reverse"; /** * Alignment along the main axis. * @default 'flex-start' */ justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | "space-evenly"; /** * Alignment along the cross axis. * @default 'stretch' */ alignItems?: "flex-start" | "center" | "flex-end" | "stretch"; /** * Override alignItems for this specific element. * @default 'auto' */ alignSelf?: "auto" | "flex-start" | "center" | "flex-end" | "stretch"; /** * Width of the element in terminal columns. * Can be a number or percentage string ('50%'). */ width?: number | string; /** * Height of the element in terminal rows. * Can be a number or percentage string ('50%'). */ height?: number | string; /** * Minimum width in terminal columns. */ minWidth?: number | string; /** * Minimum height in terminal rows. */ minHeight?: number | string; /** * Maximum width in terminal columns. */ maxWidth?: number | string; /** * Maximum height in terminal rows. */ maxHeight?: number | string; /** * Margin on all sides. */ margin?: number; /** * Margin on horizontal sides (left and right). */ marginX?: number; /** * Margin on vertical sides (top and bottom). */ marginY?: number; /** * Top margin. */ marginTop?: number; /** * Bottom margin. */ marginBottom?: number; /** * Left margin. */ marginLeft?: number; /** * Right margin. */ marginRight?: number; /** * Padding on all sides. */ padding?: number; /** * Padding on horizontal sides (left and right). */ paddingX?: number; /** * Padding on vertical sides (top and bottom). */ paddingY?: number; /** * Top padding. */ paddingTop?: number; /** * Bottom padding. */ paddingBottom?: number; /** * Left padding. */ paddingLeft?: number; /** * Right padding. */ paddingRight?: number; /** * Border on all sides. */ border?: number; /** * Top border. */ borderTop?: number; /** * Bottom border. */ borderBottom?: number; /** * Left border. */ borderLeft?: number; /** * Right border. */ borderRight?: number; /** * Gap between flex items (applies to both row and column gap). */ gap?: number; /** * Gap between columns. */ columnGap?: number; /** * Gap between rows. */ rowGap?: number; /** * Position type. * @default 'relative' */ position?: "relative" | "absolute"; /** * Display type. * @default 'flex' */ display?: "flex" | "none"; } /** * A layout node represents a virtual DOM node with Yoga layout. * It bridges React/framework layer to unblessed widgets. */ interface LayoutNode { /** * Node type identifier. */ type: string; /** * Yoga layout node - handles flexbox calculations. */ yogaNode: Node; /** * Flexbox properties for this node. */ props: FlexboxProps; /** * Child layout nodes. */ children: LayoutNode[]; /** * Parent layout node. */ parent: LayoutNode | null; /** * The created unblessed widget (set after layout calculation). */ widget?: Element; /** * Additional unblessed widget options (non-layout props). */ widgetOptions?: any; /** * Event handlers to bind to the widget (from React props like onClick, onFocus). */ eventHandlers?: Record<string, Function>; /** * Currently bound event handlers (used for cleanup on update). */ _boundHandlers?: Record<string, Function>; /** * Widget descriptor instance * This encapsulates all widget configuration and creation logic. */ _descriptor?: WidgetDescriptor; } /** * Computed layout from Yoga. */ interface ComputedLayout { /** * Top position in terminal rows. */ top: number; /** * Left position in terminal columns. */ left: number; /** * Width in terminal columns. */ width: number; /** * Height in terminal rows. */ height: number; } /** * Options for creating a layout manager. */ interface LayoutManagerOptions { /** * The screen to attach widgets to. */ screen: Screen; /** * Debug mode - logs layout calculations. * @default false */ debug?: boolean; } /** * layout-engine.ts - Main layout engine for flexbox calculations */ /** * LayoutManager - Main API for flexbox layout in unblessed. * * This class manages the lifecycle of a Yoga layout tree and synchronizes * it with unblessed widgets. It's designed to be used by framework integrations * like @unblessed/react. * * @example * ```typescript * const screen = new Screen(); * const manager = new LayoutManager({ screen }); * * const root = manager.createNode('container', { * flexDirection: 'row', * gap: 2 * }); * * const left = manager.createNode('box', { width: 20 }); * const spacer = manager.createNode('spacer', { flexGrow: 1 }); * const right = manager.createNode('box', { width: 20 }); * * manager.appendChild(root, left); * manager.appendChild(root, spacer); * manager.appendChild(root, right); * * manager.performLayout(root); * ``` */ declare class LayoutManager { private screen; private debug; constructor(options: LayoutManagerOptions); /** * Creates a new layout node. * @param type - Type identifier for the node * @param props - Flexbox properties * @param widgetOptions - Additional unblessed widget options * @returns A new layout node */ createNode(type: string, props?: any, widgetOptions?: any): LayoutNode; /** * Appends a child node to a parent. * @param parent - Parent layout node * @param child - Child layout node */ appendChild(parent: LayoutNode, child: LayoutNode): void; /** * Inserts a child node before another child. * @param parent - Parent layout node * @param child - Child layout node to insert * @param beforeChild - Reference child to insert before */ insertBefore(parent: LayoutNode, child: LayoutNode, beforeChild: LayoutNode): void; /** * Removes a child node from its parent. * @param parent - Parent layout node * @param child - Child layout node to remove */ removeChild(parent: LayoutNode, child: LayoutNode): void; /** * Calculates layout for a tree and synchronizes with unblessed widgets. * This is the main function that bridges Yoga calculations to unblessed rendering. * * WORKFLOW: * 1. Calculate layout with Yoga (flexbox positioning) * 2. Extract computed coordinates from Yoga nodes * 3. Create/update unblessed widgets with those coordinates * 4. Render the screen * * @param rootNode - Root of the layout tree */ performLayout(rootNode: LayoutNode): void; /** * Calculates layout using Yoga. * After this, all Yoga nodes will have computed positions. * @param rootNode - Root of the layout tree */ private calculateLayout; /** * Logs the computed layout for debugging. * @param node - Node to log (recursive) * @param depth - Current depth for indentation */ private logLayout; /** * Destroys a layout tree and cleans up all resources. * IMPORTANT: Call this to prevent memory leaks. * @param node - Root node to destroy */ destroy(node: LayoutNode): void; } /** * widget-sync.ts - Synchronize Yoga layout to unblessed widgets */ /** * Extracts computed layout from a Yoga node. */ declare function getComputedLayout(node: LayoutNode): ComputedLayout; /** * Synchronizes a layout node tree to unblessed widgets. * Yoga positions are applied to widgets. Widgets are created/updated as needed. */ declare function syncWidgetWithYoga(node: LayoutNode, screen: Screen): Element; /** * Synchronizes layout tree to widgets and renders to screen. */ declare function syncTreeAndRender(rootNode: LayoutNode, screen: Screen): void; /** * Destroys all widgets in a layout tree and unbinds event handlers. */ declare function destroyWidgets(node: LayoutNode): void; /** * yoga-node.ts - Yoga node lifecycle management */ /** * Creates a new layout node with a Yoga node. * @param type - Type identifier for the node * @param props - Flexbox properties to apply * @returns A new LayoutNode */ declare function createLayoutNode(type: string, props?: FlexboxProps): LayoutNode; /** * Applies flexbox style properties to a Yoga node. * This is where React/framework props get translated to Yoga API calls. * @param yogaNode - The Yoga node to apply styles to * @param props - Flexbox properties */ declare function applyFlexStyles(yogaNode: Node, props: FlexboxProps): void; /** * Updates an existing layout node with new props. * @param node - The layout node to update * @param newProps - New flexbox properties */ declare function updateLayoutNode(node: LayoutNode, newProps: FlexboxProps): void; /** * Appends a child node to a parent node. * Updates both the layout tree and Yoga tree. * @param parent - Parent layout node * @param child - Child layout node to append */ declare function appendChild(parent: LayoutNode, child: LayoutNode): void; /** * Removes a child node from its parent. * Updates both the layout tree and Yoga tree. * @param parent - Parent layout node * @param child - Child layout node to remove */ declare function removeChild(parent: LayoutNode, child: LayoutNode): void; /** * Destroys a layout node and frees Yoga resources. * IMPORTANT: Always call this to prevent memory leaks. * @param node - The layout node to destroy */ declare function destroyLayoutNode(node: LayoutNode): void; export { type ComputedLayout, type FlexboxProps, LayoutManager, type LayoutManagerOptions, type LayoutNode, WidgetDescriptor, appendChild, applyFlexStyles, createLayoutNode, destroyLayoutNode, destroyWidgets, getComputedLayout, removeChild, syncTreeAndRender, syncWidgetWithYoga, updateLayoutNode };