@unblessed/layout
Version:
Flexbox layout engine for unblessed using Yoga
522 lines (510 loc) • 15.1 kB
TypeScript
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 };