Section 02: Pure Ori Layers

Status: Research Complete Goal: Design the ~10,300 lines of pure Ori code that form the framework’s core — layout, scene, widgets, animation, events, state, and theming. These layers have zero capabilities and the compiler can statically prove they never touch C code.

Context: The pure Ori layers are the majority of the framework. They port directly from oriterm_ui’s Rust architecture but expressed in Ori’s type system. Ori’s expression-based syntax, pattern matching, traits, and capabilities make several patterns more natural than their Rust equivalents.


02.1 ori_layout — CSS Layout Engine (~2,000 lines)

Port from: oriterm_ui/src/layout/ New additions: CSS gaps identified in Section 01.2

Core Types

// Layout descriptor — widgets produce these, solver consumes them
type LayoutBox = {
    width: SizeSpec,
    height: SizeSpec,
    min_width: Option<float>,
    max_width: Option<float>,
    min_height: Option<float>,
    max_height: Option<float>,
    padding: Insets,
    margin: Insets,
    flex: Option<FlexLayout>,
    grid: Option<GridLayout>,
    position: Position,
    z_index: int,
    clip: bool,
    children: [LayoutBox],
}

type SizeSpec = Hug | Fill | Fixed(value: float) | Percent(value: float)
    | FillRemaining(margin: float) | Calc(expr: SizeExpr)

type Position = Static | Relative(offset: Offset) | Absolute(anchor: Anchor)
    | Fixed(anchor: Anchor) | Sticky(threshold: float)

type FlexLayout = {
    direction: Direction,
    align: Align,
    justify: Justify,
    gap: float,
    wrap: bool,
    grow: float,
    shrink: float,
}

type GridLayout = {
    columns: GridColumns,
    row_gap: float,
    col_gap: float,
}

type GridColumns = Fixed(count: int) | AutoFill(min_width: float)
    | Template(tracks: [TrackSize])

type TrackSize = FixedTrack(value: float) | Fr(value: float)
    | MinMax(min: float, max: float) | Auto

// Constraints passed down from parent to child
type LayoutConstraints = {
    min_width: float,
    max_width: float,
    min_height: float,
    max_height: float,
}

// Resolved layout tree — output of solver
type LayoutNode = {
    bounds: Rect,
    content_bounds: Rect,
    children: [LayoutNode],
    widget_id: Option<WidgetId>,
    clip_rect: Option<Rect>,
    z_index: int,
}

Solver Architecture

// Pure function — no side effects, no capabilities needed
@compute_layout (box: LayoutBox, constraints: LayoutConstraints) -> LayoutNode = {
    match box {
        { flex: Some(flex), .. } -> solve_flex(box, flex, constraints),
        { grid: Some(grid), .. } -> solve_grid(box, grid, constraints),
        _ -> solve_leaf(box, constraints),
    }
}

The solver is a pure function: (LayoutBox, LayoutConstraints) -> LayoutNode. Two-pass algorithm: measure children, then distribute space. Follows the CSS Flexbox specification for sizing, alignment, and space distribution.

What Ori Makes Better Than Rust

  • Pattern matching on layout mode: match box { { flex: Some(flex), .. } -> ... } is cleaner than Rust’s nested if-let chains
  • Expression-based builders: LayoutBox { ...defaults, width: Fixed(100.0), flex: Some(row_layout) } with struct spread
  • No mutability concerns: Pure computation, no &mut threading through the solver

02.2 ori_scene — Scene Primitives (~1,000 lines)

Port from: oriterm_ui/src/draw/scene/ New additions: Rounded rects, gradients, shadows, blur

Core Types

type Scene = {
    quads: [Quad],
    text_runs: [TextRun],
    lines: [LinePrimitive],
    icons: [IconPrimitive],
    images: [ImagePrimitive],
    clip_stack: [Rect],
    offset_stack: [(float, float)],
    opacity_stack: [float],
}

type Quad = {
    bounds: Rect,
    style: RectStyle,
    mask: ContentMask,
    widget_id: Option<WidgetId>,
}

type RectStyle = {
    fill: Option<Fill>,
    border: Option<Border>,
    shadow: Option<Shadow>,
    corner_radius: CornerRadius,
}

type Fill = Solid(color: Color) | LinearGradient(start: Point, end: Point, stops: [ColorStop])
    | RadialGradient(center: Point, radius: float, stops: [ColorStop])

type CornerRadius = Uniform(r: float) | PerCorner(tl: float, tr: float, br: float, bl: float)

type Shadow = {
    offset: (float, float),
    blur_radius: float,
    spread: float,
    color: Color,
    inset: bool,
}

type ContentMask = {
    clip: Option<Rect>,
    offset: (float, float),
    opacity: float,
}

type TextRun = {
    position: Point,
    shaped: ShapedText,
    color: Color,
    bg_hint: Option<Color>,
    mask: ContentMask,
    widget_id: Option<WidgetId>,
}

Scene API

impl Scene {
    @new () -> Scene = Scene {
        quads: [], text_runs: [], lines: [], icons: [], images: [],
        clip_stack: [], offset_stack: [], opacity_stack: [],
    }

    @push_rect (self, bounds: Rect, style: RectStyle) -> void = {
        let mask = self.resolve_content_mask()
        self.quads = [...self.quads, Quad { bounds, style, mask, widget_id: None }]
    }

    @push_clip (self, rect: Rect) -> void = {
        self.clip_stack = [...self.clip_stack, rect]
    }

    @pop_clip (self) -> void = {
        self.clip_stack = self.clip_stack.slice(start: 0, end: self.clip_stack.len() - 1)
    }

    @resolve_content_mask (self) -> ContentMask = {
        let clip = if self.clip_stack.is_empty() then None
                   else Some(self.clip_stack.fold(initial: self.clip_stack[0], op: (a, b) -> a.intersect(b)))
        let offset = self.offset_stack.fold(initial: (0.0, 0.0), op: (a, b) -> (a.0 + b.0, a.1 + b.1))
        let opacity = self.opacity_stack.fold(initial: 1.0, op: (a, b) -> a * b)
        ContentMask { clip, offset, opacity }
    }
}

02.3 ori_widgets — Widget System (~5,000 lines)

Port from: oriterm_ui/src/widgets/

Widget Trait

trait Widget {
    type State

    @layout (self, ctx: LayoutCtx) -> LayoutBox
    @paint (self, ctx: DrawCtx) -> void
    @on_event (self, event: Event, bounds: Rect) -> EventResult

    // Defaults
    @prepaint (self, ctx: PrepaintCtx) -> void = ()
    @is_focusable (self) -> bool = false
    @sense (self) -> Sense = Sense.none()
    @children (self) -> [impl Widget] = []
}

Declarative Composition

// Expression-based syntax IS the component tree
@counter_app () -> impl Widget = {
    let count = state(0)

    flex(direction: column, align: center, padding: 16, gap: 12) {
        text(`Count: {count.value}`)
            .font_size(24)
            .color(theme.text.primary)

        flex(direction: row, gap: 8) {
            button("Decrement", on_click: () -> count.update(n -> n - 1))
            button("Increment", on_click: () -> count.update(n -> n + 1))
        }
    }
}

Ori advantages over oriterm_ui’s Rust:

  • No Box<dyn Widget> — Ori uses impl Widget existential types
  • No RefCell for cached layout — Ori’s value semantics + COW handle this naturally
  • No closure boxing — Ori closures capture by value with ARC sharing
  • No lifetime annotations — ARC handles lifetimes automatically
  • if condition then widget_a else widget_b — conditional composition is just expressions

Standard Widgets

WidgetDescriptionApproximate Lines
flexFlexbox container~200
gridGrid container~200
stackZ-stack (overlapping children)~100
textStatic text display~150
buttonClickable with visual states~200
text_inputEditable text with cursor/selection~500
checkbox / toggleBinary state~150 each
dropdownOverlay-based selection~300
scrollScrollable container with scrollbar~400
dialogModal overlay~200
dividerVisual separator~50
spacerFlexible space~30
imageImage display~100

02.4 ori_animation — Animation System (~500 lines)

Port from: oriterm_ui/src/animation/

Core Design

trait Lerp {
    @lerp (self, target: Self, t: float) -> Self
}

impl float: Lerp {
    @lerp (self, target: float, t: float) -> float =
        self + (target - self) * t
}

impl Color: Lerp {
    @lerp (self, target: Color, t: float) -> Color =
        Color {
            r: self.r.lerp(target: target.r, t:),
            g: self.g.lerp(target: target.g, t:),
            b: self.b.lerp(target: target.b, t:),
            a: self.a.lerp(target: target.a, t:),
        }
}

type Easing = Linear | EaseIn | EaseOut | EaseInOut
    | CubicBezier(x1: float, y1: float, x2: float, y2: float)

@ease (easing: Easing, t: float) -> float = match easing {
    Linear -> t,
    EaseIn -> t * t,
    EaseOut -> t * (2.0 - t),
    EaseInOut -> if t < 0.5 then 2.0 * t * t else -1.0 + (4.0 - 2.0 * t) * t,
    CubicBezier(x1, y1, x2, y2) -> solve_cubic_bezier(x1, y1, x2, y2, t),
}

// Visual state animation — framework resolves interaction state → property values
type VisualStateAnimator<T: Lerp> = {
    current: T,
    target: T,
    progress: float,
    duration: Duration,
    easing: Easing,
}

02.5 ori_events — Event System (~800 lines)

Port from: oriterm_ui/src/input/, oriterm_ui/src/controllers/

Hit Testing

@hit_test (root: LayoutNode, point: Point) -> Option<HitPath> = {
    // Walk layout tree, respect clip rects, find deepest widget containing point
    // Returns path from root to leaf for capture/bubble dispatch
}

Two-Phase Dispatch

type EventPhase = Capture | Bubble

@dispatch_event (root: impl Widget, event: Event, hit_path: HitPath) -> EventResult = {
    // Phase 1: Capture — deliver root → leaf
    for widget in hit_path.ancestors() do {
        match widget.on_event(event, phase: Capture) {
            Handled -> break,
            Continue -> (),
        }
    }
    // Phase 2: Bubble — deliver leaf → root
    for widget in hit_path.from_leaf() do {
        match widget.on_event(event, phase: Bubble) {
            Handled -> break,
            Continue -> (),
        }
    }
}

Composable Controllers

trait EventController {
    @on_event (self, event: Event, ctx: ControllerCtx) -> ControllerResult
    @on_lifecycle (self, event: LifecycleEvent, ctx: ControllerCtx) -> void = ()
}

// Attach multiple controllers to a widget
@my_widget () -> impl Widget = {
    button("Click me")
        .controller(on_click(handler: () -> print(msg: "clicked!")))
        .controller(on_hover(enter: () -> ..., leave: () -> ...))
        .controller(on_drag(start: ..., move: ..., end: ...))
}

02.6 ori_state — Reactive State (~600 lines)

State Management

// State cell — triggers re-render when updated
type State<T> = { value: T, version: int }

@state<T> (initial: T) -> State<T> = State { value: initial, version: 0 }

impl<T> State<T> {
    @update (self, transform: (T) -> T) -> void = {
        self.value = transform(self.value)
        self.version = self.version + 1
        // Framework detects version change → schedules re-render
    }

    @set (self, value: T) -> void = self.update(transform: _ -> value)
}

Ori’s ARC + COW advantage: When state updates, unchanged widget subtrees share memory via ARC. Only the changed path gets new allocations. This is what React’s virtual DOM approximates with diffing — but at the memory level, with zero diffing overhead.


02.7 ori_theme — Capability-Based Theming (~400 lines)

Design

// Theme is a capability — scoped, mockable, type-safe
type Theme = {
    text: TextTheme,
    color: ColorTheme,
    spacing: SpacingTheme,
    typography: TypographyTheme,
    border: BorderTheme,
    shadow: ShadowTheme,
    animation: AnimationTheme,
}

// Provide theme via capabilities — no prop drilling
@app () -> impl Widget uses Render = {
    with Theme = dark_theme() in {
        my_app_content()
    }
}

// Nested theme override — scoped to subtree
@settings_panel () -> impl Widget = {
    with Theme = high_contrast_theme() in {
        // This subtree uses high contrast
        settings_content()
    }
}

No CSS cascade complexity. Theme flows through capabilities — explicit, scoped, type-checked. No specificity wars, no !important, no inheritance surprises.