Skip to content

Core API

The nuiitivet package exposes the core primitives, layout widgets, and state management utilities.

nuiitivet

nuiitivet core package.

This module is the single source of truth for all public core symbols (layout, widgets, state management, theming, animation, modifiers, ...).

Applications should not import from this package directly. Instead they pick a UI design system root — currently only :mod:nuiitivet.material — which re-exports everything here plus its own widgets::

import nuiitivet.material as nv

nv.Column(...)   # core symbol, re-exported here
nv.Button(...)   # material symbol

That single import is the whole supported surface. Reaching into the internal modules below (nuiitivet.layout.column, nuiitivet.widgeting.widget, ...) is unsupported and may break without notice.

InputFilterLike module-attribute

InputFilterLike = Union[InputFilter, Callable[[str], str]]

An :class:InputFilter, or a plain str -> str callable standing in for one.

RendererMode module-attribute

RendererMode = Literal['auto', 'gpu', 'cpu']

Renderer selection accepted by :meth:nuiitivet.runtime.app.App.run.

  • "auto": try the GPU first and silently fall back to software (raster) rendering when the GPU backend is unavailable. This is the default.
  • "gpu": require the GPU. Initialization or per-frame GPU failures raise a :class:RuntimeError instead of degrading, so remote/GPU-less environments surface a clear error rather than running unexpectedly slowly.
  • "cpu": always render in software (raster); the GPU is never touched.

Note that App.run always needs a display/window; truly headless environments should render offscreen via :meth:App.render_to_png instead.

Column

Column(children: Optional[Sequence[Widget]] = None, *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, gap: Union[int, ObservableBase] = 0, main_alignment: str = 'start', cross_alignment: str = 'start', key: Optional[str] = None)

Bases: Widget

Layout children vertically.

Parameters - gap: pixels between children - cross_alignment: 'start'|'center'|'end' for cross-axis (horizontal) alignment - main_alignment: 'start'|'center'|'end'|'space-between'|'space-around'|'space-evenly' - padding: inner padding as int (all sides), (h, v), or (left, top, right, bottom) - overflow: 'visible'|'clip'|'scroll' - how to handle children that overflow the container - 'visible' (default): children may extend beyond container (Phase 1 behavior) - 'clip': children are clipped to container bounds - 'scroll': requires VerticalScrollable / HorizontalScrollable wrapper (Phase 3)

Initialize Column and configure layout.

Parameters:

Name Type Description Default
children Optional[Sequence[Widget]]

List of child widgets to arrange vertically.

None
width SizingLike

Column width.

None
height SizingLike

Column height.

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Padding around the content.

0
gap Union[int, ObservableBase]

Space between children in pixels.

0
main_alignment str

Vertical alignment of children. 'start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'.

'start'
cross_alignment str

Horizontal alignment of children. 'start', 'center', 'end', 'stretch'.

'start'
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/column.py
def __init__(
    self,
    children: Optional[Sequence[Widget]] = None,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    gap: Union[int, ObservableBase] = 0,
    main_alignment: str = "start",
    cross_alignment: str = "start",
    key: Optional[str] = None,
):
    """Initialize Column and configure layout.

    Args:
        children: List of child widgets to arrange vertically.
        width: Column width.
        height: Column height.
        padding: Padding around the content.
        gap: Space between children in pixels.
        main_alignment: Vertical alignment of children.
            'start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'.
        cross_alignment: Horizontal alignment of children.
            'start', 'center', 'end', 'stretch'.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, padding=padding, key=key)
    if children:
        for c in children:
            self.add_child(c)

    self._gap = 0
    self.gap = gap  # type: ignore

    self.main_alignment = main_alignment
    self.cross_alignment = cross_alignment

builder classmethod

builder(items: ItemsLike, builder: BuilderFn, *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, gap: int = 0, main_alignment: str = 'start', cross_alignment: str = 'start', key: Optional[str] = None) -> Column

Create a Column that materializes children from items via ForEach.

Parameters:

Name Type Description Default
items ItemsLike

Source data collection.

required
builder BuilderFn

Function to create a widget for each item.

required
width SizingLike

Column width.

None
height SizingLike

Column height.

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Padding around the content.

0
gap int

Space between children.

0
main_alignment str

Vertical alignment of children.

'start'
cross_alignment str

Horizontal alignment of children.

'start'
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/column.py
@classmethod
def builder(
    cls,
    items: ItemsLike,
    builder: BuilderFn,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    gap: int = 0,
    main_alignment: str = "start",
    cross_alignment: str = "start",
    key: Optional[str] = None,
) -> "Column":
    """Create a Column that materializes children from items via ForEach.

    Args:
        items: Source data collection.
        builder: Function to create a widget for each item.
        width: Column width.
        height: Column height.
        padding: Padding around the content.
        gap: Space between children.
        main_alignment: Vertical alignment of children.
        cross_alignment: Horizontal alignment of children.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    provider = ForEach(items, builder)
    return cls(
        children=[provider],
        width=width,
        height=height,
        padding=padding,
        gap=gap,
        main_alignment=main_alignment,
        cross_alignment=cross_alignment,
        key=key,
    )

Row

Row(children: Optional[List[Widget]] = None, *, width: SizingLike = None, height: SizingLike = None, padding: PaddingLike = 0, gap: Union[int, ObservableBase] = 0, main_alignment: str = 'start', cross_alignment: str = 'start', key: Optional[str] = None)

Bases: Widget

Layout children horizontally.

Parameters - gap: pixels between children - cross_alignment: 'start'|'center'|'end' for cross-axis (vertical) alignment - main_alignment: 'start'|'center'|'end'|'space-between'|'space-around'|'space-evenly' - padding: inner padding as int (all sides), (h, v), or (left, top, right, bottom)

Initialize Row and configure layout.

Parameters:

Name Type Description Default
children Optional[List[Widget]]

List of child widgets to arrange horizontally.

None
width SizingLike

Row width. Defaults to None (shrinkwrap).

None
height SizingLike

Row height. Defaults to None (shrinkwrap).

None
padding PaddingLike

Padding around the content.

0
gap Union[int, ObservableBase]

Space between children in pixels.

0
main_alignment str

Horizontal alignment of children. 'start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'.

'start'
cross_alignment str

Vertical alignment of children. 'start', 'center', 'end', 'stretch'.

'start'
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/row.py
def __init__(
    self,
    children: Optional[List[Widget]] = None,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: PaddingLike = 0,
    gap: Union[int, ObservableBase] = 0,
    main_alignment: str = "start",
    cross_alignment: str = "start",
    key: Optional[str] = None,
):
    """Initialize Row and configure layout.

    Args:
        children: List of child widgets to arrange horizontally.
        width: Row width. Defaults to None (shrinkwrap).
        height: Row height. Defaults to None (shrinkwrap).
        padding: Padding around the content.
        gap: Space between children in pixels.
        main_alignment: Horizontal alignment of children.
            'start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'.
        cross_alignment: Vertical alignment of children.
            'start', 'center', 'end', 'stretch'.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, padding=padding, key=key)
    if children:
        for c in children:
            self.add_child(c)
    self._gap = 0
    self.gap = gap  # type: ignore
    self.main_alignment = main_alignment
    self.cross_alignment = cross_alignment

builder classmethod

builder(items: ItemsLike, builder: BuilderFn, *, width: SizingLike = None, height: SizingLike = None, padding: PaddingLike = 0, gap: int = 0, main_alignment: str = 'start', cross_alignment: str = 'start', key: Optional[str] = None) -> Row

Create a Row that materializes children from items via ForEach.

Parameters:

Name Type Description Default
items ItemsLike

Source data collection.

required
builder BuilderFn

Function to create a widget for each item.

required
width SizingLike

Row width.

None
height SizingLike

Row height.

None
padding PaddingLike

Padding around the content.

0
gap int

Space between children.

0
main_alignment str

Horizontal alignment of children.

'start'
cross_alignment str

Vertical alignment of children.

'start'
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/row.py
@classmethod
def builder(
    cls,
    items: ItemsLike,
    builder: BuilderFn,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: PaddingLike = 0,
    gap: int = 0,
    main_alignment: str = "start",
    cross_alignment: str = "start",
    key: Optional[str] = None,
) -> "Row":
    """Create a Row that materializes children from items via ForEach.

    Args:
        items: Source data collection.
        builder: Function to create a widget for each item.
        width: Row width.
        height: Row height.
        padding: Padding around the content.
        gap: Space between children.
        main_alignment: Horizontal alignment of children.
        cross_alignment: Vertical alignment of children.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    provider = ForEach(items, builder)
    return cls(
        children=[provider],
        width=width,
        height=height,
        padding=padding,
        gap=gap,
        main_alignment=main_alignment,
        cross_alignment=cross_alignment,
        key=key,
    )

Stack

Stack(children: Sequence[Widget], *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, alignment: AlignmentLike = 'top-left', key: Optional[str] = None)

Bases: Widget

Layout children on top of each other.

Parameters - children: List of widgets to stack. - alignment: How to align children within the stack.

Initialize the Stack layout.

Parameters:

Name Type Description Default
children Sequence[Widget]

List of widgets to stack on top of each other.

required
width SizingLike

Stack width.

None
height SizingLike

Stack height.

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Padding around the content.

0
alignment AlignmentLike

Default alignment for children. (horizontal, vertical) tuple or string like "top-left", "center".

'top-left'
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/stack.py
def __init__(
    self,
    children: Sequence[Widget],
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    alignment: AlignmentLike = "top-left",
    key: Optional[str] = None,
) -> None:
    """Initialize the Stack layout.

    Args:
        children: List of widgets to stack on top of each other.
        width: Stack width.
        height: Stack height.
        padding: Padding around the content.
        alignment: Default alignment for children.
            (horizontal, vertical) tuple or string like "top-left", "center".
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, padding=padding, key=key)
    for c in children:
        self.add_child(c)
    self.alignment = normalize_alignment(alignment, default=("start", "start"))

builder classmethod

builder(items: ItemsLike, builder: BuilderFn, *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, alignment: AlignmentLike = 'center', key: Optional[str] = None) -> 'Stack'

Create a Stack that materializes children from items via ForEach.

Parameters:

Name Type Description Default
items ItemsLike

Source data collection.

required
builder BuilderFn

Function to create a widget for each item.

required
width SizingLike

Stack width.

None
height SizingLike

Stack height.

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Padding around the content.

0
alignment AlignmentLike

Default alignment for children.

'center'
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/stack.py
@classmethod
def builder(
    cls,
    items: ItemsLike,
    builder: BuilderFn,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    alignment: AlignmentLike = "center",
    key: Optional[str] = None,
) -> "Stack":
    """Create a Stack that materializes children from items via ForEach.

    Args:
        items: Source data collection.
        builder: Function to create a widget for each item.
        width: Stack width.
        height: Stack height.
        padding: Padding around the content.
        alignment: Default alignment for children.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    provider = ForEach(items, builder)
    return cls(
        children=[provider],
        width=width,
        height=height,
        padding=padding,
        alignment=alignment,
        key=key,
    )

Container

Container(child: Optional[Widget] = None, *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, alignment: Union[str, Tuple[str, str]] = 'start', key: Optional[str] = None)

Bases: Widget

Lightweight layout-only Container.

This Container is a minimal single-child layout box. It intentionally does not perform background/shadow/border drawing or clipping.

Initialize the Container.

Parameters:

Name Type Description Default
child Optional[Widget]

The child widget to be placed inside the container.

None
width SizingLike

The preferred width of the container. Defaults to None (shrinkwrap).

None
height SizingLike

The preferred height of the container. Defaults to None (shrinkwrap).

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Padding to apply around the child. Can be a single integer, a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).

0
alignment Union[str, Tuple[str, str]]

How to align the child within the container. Defaults to "start". Can be a string (e.g., "center") or a tuple (horizontal, vertical).

'start'
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/container.py
def __init__(
    self,
    child: Optional[Widget] = None,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    alignment: Union[str, Tuple[str, str]] = "start",
    key: Optional[str] = None,
):
    """Initialize the Container.

    Args:
        child: The child widget to be placed inside the container.
        width: The preferred width of the container. Defaults to None (shrinkwrap).
        height: The preferred height of the container. Defaults to None (shrinkwrap).
        padding: Padding to apply around the child. Can be a single integer,
            a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).
        alignment: How to align the child within the container. Defaults to "start".
            Can be a string (e.g., "center") or a tuple (horizontal, vertical).
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        width=width,
        height=height,
        padding=padding,
        max_children=1,
        overflow_policy="replace_last",
        key=key,
    )

    self._align = normalize_alignment(alignment, default=("start", "start"))

    # child management + layout engine
    if child is not None:
        self.add_child(child)
    self._layout = LayoutEngine(self)

add_child

add_child(w: Widget)

Keep at most one child; call ChildContainerMixin directly to bypass overrides.

Source code in src/nuiitivet/layout/container.py
def add_child(self, w: "Widget"):
    """Keep at most one child; call ChildContainerMixin directly to bypass overrides."""
    ChildContainerMixin.add_child(self, w)

Flow

Flow(children: Optional[Sequence[Widget]] = None, *, main_gap: int = 0, cross_gap: int = 0, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, main_alignment: str = 'start', run_alignment: str = 'start', cross_alignment: str = 'start', width: SizingLike = None, height: SizingLike = None, key: Optional[str] = None)

Bases: Widget

Layout children in rows that wrap.

Arranges children in a horizontal run, wrapping to a new line when the line runs out of space.

Source code in src/nuiitivet/layout/flow.py
def __init__(
    self,
    children: Optional[Sequence[Widget]] = None,
    *,
    main_gap: int = 0,
    cross_gap: int = 0,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    main_alignment: str = "start",
    run_alignment: str = "start",
    cross_alignment: str = "start",
    width: SizingLike = None,
    height: SizingLike = None,
    key: Optional[str] = None,
) -> None:
    super().__init__(width=width, height=height, padding=padding, key=key)
    if children:
        for child in children:
            self.add_child(child)

    self.main_gap = normalize_gap(main_gap)
    self.cross_gap = normalize_gap(cross_gap)
    self.main_alignment = main_alignment or "start"
    self.run_alignment = run_alignment or "start"
    self.cross_alignment = cross_alignment or "start"

builder classmethod

builder(items: ItemsLike, builder: BuilderFn, *, main_gap: int = 0, cross_gap: int = 0, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, main_alignment: str = 'start', run_alignment: str = 'start', cross_alignment: str = 'start', width: SizingLike = None, height: SizingLike = None, key: Optional[str] = None) -> 'Flow'

Create a Flow that materializes children from items via ForEach.

Source code in src/nuiitivet/layout/flow.py
@classmethod
def builder(
    cls,
    items: ItemsLike,
    builder: BuilderFn,
    *,
    main_gap: int = 0,
    cross_gap: int = 0,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    main_alignment: str = "start",
    run_alignment: str = "start",
    cross_alignment: str = "start",
    width: SizingLike = None,
    height: SizingLike = None,
    key: Optional[str] = None,
) -> "Flow":
    """Create a Flow that materializes children from items via ForEach."""

    provider = ForEach(items, builder)
    return cls(
        [provider],
        main_gap=main_gap,
        cross_gap=cross_gap,
        padding=padding,
        main_alignment=main_alignment,
        run_alignment=run_alignment,
        cross_alignment=cross_alignment,
        width=width,
        height=height,
        key=key,
    )

UniformFlow

UniformFlow(children: Optional[Sequence[Widget]] = None, *, columns: Optional[int] = None, max_column_width: Optional[int] = None, aspect_ratio: Optional[float] = None, main_gap: int = 0, cross_gap: int = 0, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, main_alignment: str = 'start', run_alignment: str = 'start', item_alignment: AlignValue = 'stretch', width: SizingLike = None, height: SizingLike = None, key: Optional[str] = None)

Bases: Widget

Layout children in a uniform grid.

This layout arranges children into columns with equal width.

Source code in src/nuiitivet/layout/uniform_flow.py
def __init__(
    self,
    children: Optional[Sequence[Widget]] = None,
    *,
    columns: Optional[int] = None,
    max_column_width: Optional[int] = None,
    aspect_ratio: Optional[float] = None,
    main_gap: int = 0,
    cross_gap: int = 0,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    main_alignment: str = "start",
    run_alignment: str = "start",
    item_alignment: AlignValue = "stretch",
    width: SizingLike = None,
    height: SizingLike = None,
    key: Optional[str] = None,
) -> None:
    super().__init__(width=width, height=height, padding=padding, key=key)
    if children:
        for child in children:
            self.add_child(child)

    self.columns = self._normalize_positive(columns)
    self.max_column_width = self._normalize_positive(max_column_width)
    self.aspect_ratio = float(aspect_ratio) if aspect_ratio else None
    self.main_gap = normalize_gap(main_gap)
    self.cross_gap = normalize_gap(cross_gap)
    self.main_alignment = main_alignment or "start"
    self.run_alignment = run_alignment or "start"
    self.item_alignment = self._normalize_align_pair(item_alignment)

builder classmethod

builder(items: ItemsLike, builder: BuilderFn, *, columns: Optional[int] = None, max_column_width: Optional[int] = None, aspect_ratio: Optional[float] = None, main_gap: int = 0, cross_gap: int = 0, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, main_alignment: str = 'start', run_alignment: str = 'start', item_alignment: AlignValue = 'stretch', width: SizingLike = None, height: SizingLike = None, key: Optional[str] = None) -> 'UniformFlow'

Create a UniformFlow that materializes children from items via ForEach.

Source code in src/nuiitivet/layout/uniform_flow.py
@classmethod
def builder(
    cls,
    items: ItemsLike,
    builder: BuilderFn,
    *,
    columns: Optional[int] = None,
    max_column_width: Optional[int] = None,
    aspect_ratio: Optional[float] = None,
    main_gap: int = 0,
    cross_gap: int = 0,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    main_alignment: str = "start",
    run_alignment: str = "start",
    item_alignment: AlignValue = "stretch",
    width: SizingLike = None,
    height: SizingLike = None,
    key: Optional[str] = None,
) -> "UniformFlow":
    """Create a UniformFlow that materializes children from items via ForEach."""
    provider = ForEach(items, builder)
    return cls(
        [provider],
        columns=columns,
        max_column_width=max_column_width,
        aspect_ratio=aspect_ratio,
        main_gap=main_gap,
        cross_gap=cross_gap,
        padding=padding,
        main_alignment=main_alignment,
        run_alignment=run_alignment,
        item_alignment=item_alignment,
        width=width,
        height=height,
        key=key,
    )

Grid

Grid(children: Optional[Sequence[Widget]], rows: Optional[Sequence[SizingLike]], columns: Optional[Sequence[SizingLike]], *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, row_gap: int = 0, column_gap: int = 0, key: Optional[str] = None)

Bases: Widget

Two-dimensional layout container with explicit tracks.

Initialize the Grid layout.

Parameters:

Name Type Description Default
children Optional[Sequence[Widget]]

List of GridItems to display.

required
rows Optional[Sequence[SizingLike]]

One track size per row: an int for fixed pixels, "auto" to fit the row's content, or a percentage such as "wt" to share the leftover space. Example: [60, "wt", "auto"].

required
columns Optional[Sequence[SizingLike]]

One track size per column; same forms as rows.

required
width SizingLike

Grid container width.

None
height SizingLike

Grid container height.

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Padding around the grid content.

0
row_gap int

Vertical gap between rows.

0
column_gap int

Horizontal gap between columns.

0
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/grid.py
def __init__(
    self,
    children: Optional[Sequence[Widget]],
    rows: Optional[Sequence[SizingLike]],
    columns: Optional[Sequence[SizingLike]],
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    row_gap: int = 0,
    column_gap: int = 0,
    key: Optional[str] = None,
):
    """Initialize the Grid layout.

    Args:
        children: List of GridItems to display.
        rows: One track size per row: an ``int`` for fixed pixels, ``"auto"``
            to fit the row's content, or a percentage such as ``"wt"`` to
            share the leftover space.
            Example: ``[60, "wt", "auto"]``.
        columns: One track size per column; same forms as ``rows``.
        width: Grid container width.
        height: Grid container height.
        padding: Padding around the grid content.
        row_gap: Vertical gap between rows.
        column_gap: Horizontal gap between columns.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, padding=padding, key=key)

    self._rows: List[Sizing] = [parse_sizing(d) for d in rows] if rows else []
    self._columns: List[Sizing] = [parse_sizing(d) for d in columns] if columns else []
    self.areas: Optional[List[List[str]]] = None
    self.row_gap = normalize_gap(row_gap)
    self.column_gap = normalize_gap(column_gap)

    if children:
        for child in children:
            self.add_child(child)

named_areas classmethod

named_areas(children: Sequence[Widget], areas: Sequence[Sequence[str]], *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, row_gap: int = 0, column_gap: int = 0, rows: Optional[Sequence[SizingLike]] = None, columns: Optional[Sequence[SizingLike]] = None) -> Grid

Create a Grid using named template areas.

Parameters:

Name Type Description Default
children Sequence[Widget]

List of child widgets (usually GridItem.named_area).

required
areas Sequence[Sequence[str]]

2D list of area names (e.g. [["header", "header"], ["sidebar", "content"]]).

required
width SizingLike

Grid width.

None
height SizingLike

Grid height.

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Grid padding.

0
row_gap int

Gap between rows.

0
column_gap int

Gap between columns.

0
rows Optional[Sequence[SizingLike]]

Explicit row sizing overrides.

None
columns Optional[Sequence[SizingLike]]

Explicit column sizing overrides.

None

Returns:

Name Type Description
Grid Grid

A configured Grid instance.

Source code in src/nuiitivet/layout/grid.py
@classmethod
def named_areas(
    cls,
    children: Sequence[Widget],
    areas: Sequence[Sequence[str]],
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    row_gap: int = 0,
    column_gap: int = 0,
    rows: Optional[Sequence[SizingLike]] = None,
    columns: Optional[Sequence[SizingLike]] = None,
) -> Grid:
    """Create a Grid using named template areas.

    Args:
        children: List of child widgets (usually GridItem.named_area).
        areas: 2D list of area names (e.g. [["header", "header"], ["sidebar", "content"]]).
        width: Grid width.
        height: Grid height.
        padding: Grid padding.
        row_gap: Gap between rows.
        column_gap: Gap between columns.
        rows: Explicit row sizing overrides.
        columns: Explicit column sizing overrides.

    Returns:
        Grid: A configured Grid instance.
    """
    grid = cls(
        children=children,
        width=width,
        height=height,
        padding=padding,
        row_gap=row_gap,
        column_gap=column_gap,
        rows=rows,
        columns=columns,
    )
    grid.areas = grid._normalize_areas(areas)
    return grid

GridItem

GridItem(child: Widget, row: Optional[GridIndex] = None, column: Optional[GridIndex] = None, *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, alignment: Union[str, Tuple[str, str]] = 'start', key: Optional[str] = None)

Bases: Container

Annotate a child with explicit grid placement data.

Initialize the GridItem wrapper.

Parameters:

Name Type Description Default
child Widget

The widget to place in the grid.

required
row Optional[GridIndex]

Row index or (start, end) tuple. 0-based.

None
column Optional[GridIndex]

Column index or (start, end) tuple. 0-based.

None
width SizingLike

Override width.

None
height SizingLike

Override height.

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Padding around the child within the grid cell.

0
alignment Union[str, Tuple[str, str]]

Alignment within the grid cell. Defaults to "start".

'start'
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/grid.py
def __init__(
    self,
    child: Widget,
    row: Optional[GridIndex] = None,
    column: Optional[GridIndex] = None,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    alignment: Union[str, Tuple[str, str]] = "start",
    key: Optional[str] = None,
):
    """Initialize the GridItem wrapper.

    Args:
        child: The widget to place in the grid.
        row: Row index or (start, end) tuple. 0-based.
        column: Column index or (start, end) tuple. 0-based.
        width: Override width.
        height: Override height.
        padding: Padding around the child within the grid cell.
        alignment: Alignment within the grid cell. Defaults to "start".
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        child=child,
        width=width,
        height=height,
        padding=padding,
        alignment=alignment,
        key=key,
    )
    self._row_spec = row
    self._column_spec = column
    self.area: Optional[str] = None

named_area classmethod

named_area(child: Widget, name: str, *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, alignment: Union[str, Tuple[str, str]] = 'start') -> GridItem

Create a GridItem placed in a named template area.

Parameters:

Name Type Description Default
child Widget

The child widget to place.

required
name str

The area name matching a name in Grid.named_areas().

required
width SizingLike

Override width.

None
height SizingLike

Override height.

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Padding around the child within the grid cell.

0
alignment Union[str, Tuple[str, str]]

Alignment within the grid cell. Defaults to "start".

'start'

Returns:

Name Type Description
GridItem GridItem

The wrapped widget with area information.

Source code in src/nuiitivet/layout/grid.py
@classmethod
def named_area(
    cls,
    child: Widget,
    name: str,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    alignment: Union[str, Tuple[str, str]] = "start",
) -> GridItem:
    """Create a GridItem placed in a named template area.

    Args:
        child: The child widget to place.
        name: The area name matching a name in Grid.named_areas().
        width: Override width.
        height: Override height.
        padding: Padding around the child within the grid cell.
        alignment: Alignment within the grid cell. Defaults to "start".

    Returns:
        GridItem: The wrapped widget with area information.
    """
    item = cls(
        child=child,
        width=width,
        height=height,
        padding=padding,
        alignment=alignment,
    )
    item.area = name
    return item

Spacer

Spacer(*, width: SizingLike = 0, height: SizingLike = 0, key: Optional[str] = None)

Bases: Widget

Invisible widget that reserves space.

This single Spacer supports both fixed-size and space-filling behavior.

Parameters:

Name Type Description Default
width SizingLike

preferred width (int, "auto", "wt", "wt{n}", or Sizing)

0
height SizingLike

preferred height (same accepted formats as width)

0

Initialize a Spacer.

Parameters:

Name Type Description Default
width SizingLike

Preferred width. Use Sizing.weight() or 0 for filling space.

0
height SizingLike

Preferred height. Use Sizing.weight() or 0 for filling space.

0
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/spacer.py
def __init__(self, *, width: SizingLike = 0, height: SizingLike = 0, key: Optional[str] = None):
    """Initialize a Spacer.

    Args:
        width: Preferred width. Use Sizing.weight() or 0 for filling space.
        height: Preferred height. Use Sizing.weight() or 0 for filling space.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, key=key)

preferred_size

preferred_size(max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]

Return preferred size based on Sizings.

  • fixed: return the fixed value
  • auto/weight: return 0 (minimum size, parent will allocate)
Source code in src/nuiitivet/layout/spacer.py
def preferred_size(self, max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]:
    """Return preferred size based on Sizings.

    - fixed: return the fixed value
    - auto/weight: return 0 (minimum size, parent will allocate)
    """
    w_dim = self.width_sizing
    h_dim = self.height_sizing

    pref_w = int(w_dim.value) if w_dim.kind == "fixed" else 0
    pref_h = int(h_dim.value) if h_dim.kind == "fixed" else 0

    _ = (max_width, max_height)

    return (pref_w, pref_h)

CrossAligned

CrossAligned(child: Optional[Widget], alignment: str, *, key: Optional[str] = None)

Bases: Widget

Layout wrapper that overrides cross-axis alignment in Row/Column.

This sets cross_align metadata on the wrapper. Row/Column may read this to override their cross_alignment for this child only.

Initialize the CrossAligned wrapper.

Parameters:

Name Type Description Default
child Optional[Widget]

The child widget to wrap.

required
alignment str

The cross-axis alignment to apply to this child. Common values: "start", "center", "end", "stretch".

required
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/cross_aligned.py
def __init__(
    self,
    child: Optional[Widget],
    alignment: str,
    *,
    key: Optional[str] = None,
) -> None:
    """Initialize the CrossAligned wrapper.

    Args:
        child: The child widget to wrap.
        alignment: The cross-axis alignment to apply to this child.
            Common values: "start", "center", "end", "stretch".
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        width=child.width_sizing if child is not None else None,
        height=child.height_sizing if child is not None else None,
        padding=0,
        max_children=1,
        overflow_policy="replace_last",
        key=key,
    )
    self.cross_align = str(alignment)
    if child is not None:
        self.add_child(child)

Deck

Deck(children: Optional[Sequence[Widget]] = None, index: Union[int, ObservableBase[int]] = 0, *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, key: Optional[str] = None)

Bases: Widget

Display only one child at a time.

All children remain mounted (state preserved), but only the selected child is visible and rendered.

Usage

Deck(children=[HomeTab(), SearchTab(), ProfileTab()], index=0)

For type-safe index, use IntEnum: class Section(IntEnum): HOME = 0 SEARCH = 1 Deck(children=[...], index=Section.HOME)

Initialize the Deck layout.

Parameters:

Name Type Description Default
children Optional[Sequence[Widget]]

A list of child widgets. All are mounted, but only one is visible.

None
index Union[int, ObservableBase[int]]

The index of the child to display. Can be an integer or any read-observable of int — a plain Observable[int] or a derived one (e.g. some_observable.map(...)). Defaults to 0.

0
width SizingLike

The preferred width of the container. Defaults to None.

None
height SizingLike

The preferred height of the container. Defaults to None.

None
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Padding to apply around the visible child. Defaults to 0.

0
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/deck.py
def __init__(
    self,
    children: Optional[Sequence[Widget]] = None,
    index: Union[int, ObservableBase[int]] = 0,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize the Deck layout.

    Args:
        children: A list of child widgets. All are mounted, but only one is visible.
        index: The index of the child to display. Can be an integer or any
            read-observable of int — a plain ``Observable[int]`` or a derived
            one (e.g. ``some_observable.map(...)``). Defaults to 0.
        width: The preferred width of the container. Defaults to None.
        height: The preferred height of the container. Defaults to None.
        padding: Padding to apply around the visible child. Defaults to 0.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, padding=padding, key=key)

    # Add all children
    if children:
        for child in children:
            self.add_child(child)

    # Handle index (Observable or plain int). The subscription itself is
    # taken on mount, not here -- see on_mount.
    self._index_observable: Optional[ObservableBase[int]] = None
    if isinstance(index, ObservableBase):
        self._index_observable = index
        self._current_index = index.value
    else:
        self._current_index = int(index)

    # Validate initial index
    self._validate_index()

current_index property

current_index: int

The selected child index, clamped to the children that exist.

Resolved from the observable on every read rather than served from the cache the subscription maintains. The subscription is mount-scoped -- it exists to call mark_needs_layout, which only means anything in a tree -- so without this a Deck that had not been mounted yet, or had been taken out, would report a stale index to code that is entitled to ask.

on_mount

on_mount() -> None

Follow the index observable for as long as this Deck is in the tree.

The observable belongs to the app and outlives this widget, so a bare subscribe in the constructor left it holding _on_index_changed forever -- the leak this framework's bind() convention exists to prevent. A dispose() method used to sit here doing the cleanup, and never ran: Widget has no dispose hook for anything to call it from.

Source code in src/nuiitivet/layout/deck.py
def on_mount(self) -> None:
    """Follow the index observable for as long as this Deck is in the tree.

    The observable belongs to the app and outlives this widget, so a bare
    ``subscribe`` in the constructor left it holding ``_on_index_changed``
    forever -- the leak this framework's ``bind()`` convention exists to
    prevent. A ``dispose()`` method used to sit here doing the cleanup, and
    never ran: ``Widget`` has no ``dispose`` hook for anything to call it
    from.
    """
    super().on_mount()
    if self._index_observable is None:
        return
    self.bind(self._index_observable.subscribe(self._on_index_changed))
    # The index may have moved while this Deck was out of the tree.
    self._on_index_changed(self._index_observable.value)

set_index

set_index(index: int) -> None

Set the selected child index (for non-Observable usage).

Source code in src/nuiitivet/layout/deck.py
def set_index(self, index: int) -> None:
    """Set the selected child index (for non-Observable usage)."""
    if self._index_observable is not None:
        raise ValueError("Cannot set_index when using Observable index")
    old_index = self._current_index
    self._current_index = index
    self._validate_index()
    if old_index != self._current_index:
        self.mark_needs_layout()

focus_traversal_children

focus_traversal_children() -> List[Widget]

Return only the selected page, so Tab skips the pages behind it.

The index addresses the post-expansion list — the same list paint and hit_test work on — so it is resolved on every call and stays correct when a ForEach child changes its item count.

Source code in src/nuiitivet/layout/deck.py
def focus_traversal_children(self) -> List[Widget]:
    """Return only the selected page, so Tab skips the pages behind it.

    The index addresses the **post-expansion** list — the same list
    ``paint`` and ``hit_test`` work on — so it is resolved on every call and
    stays correct when a ``ForEach`` child changes its item count.
    """
    children = expand_layout_children(self.children_snapshot())
    if not children or not (0 <= self._current_index < len(children)):
        return []
    return [children[self._current_index]]

layout

layout(width: int, height: int) -> None

Layout all children (to preserve state), position selected child.

Source code in src/nuiitivet/layout/deck.py
def layout(self, width: int, height: int) -> None:
    """Layout all children (to preserve state), position selected child."""
    super().layout(width, height)
    children = expand_layout_children(self.children_snapshot())
    if not children:
        return

    pad_left, pad_top, pad_right, pad_bottom = self.padding
    available_w = max(0, width - pad_left - pad_right)
    available_h = max(0, height - pad_top - pad_bottom)

    # Layout ALL children so they maintain state
    for child in children:
        child.layout(available_w, available_h)

    # Position selected child (others will not be painted)
    if 0 <= self._current_index < len(children):
        selected_child = children[self._current_index]
        selected_child.set_layout_rect(pad_left, pad_top, available_w, available_h)

paint

paint(canvas, x: int, y: int, width: int, height: int) -> None

Paint only the selected child.

Source code in src/nuiitivet/layout/deck.py
def paint(self, canvas, x: int, y: int, width: int, height: int) -> None:
    """Paint only the selected child."""
    children = expand_layout_children(self.children_snapshot())
    if not children or self._current_index >= len(children):
        return

    # Auto-layout fallback for Tests or direct paint calls
    if any(c.layout_rect is None for c in children):
        self.layout(width, height)

    # Only paint the selected child
    selected_child = children[self._current_index]
    rect = selected_child.layout_rect
    if rect is None:
        return

    rel_x, rel_y, w, h = rect
    abs_x = x + rel_x
    abs_y = y + rel_y

    selected_child.set_last_rect(abs_x, abs_y, w, h)
    selected_child.paint(canvas, abs_x, abs_y, w, h)

hit_test

hit_test(x: int, y: int)

Only allow hit testing on the selected child.

The Deck itself never becomes the hit target (S = none); it defers to the selected child only (C descends into one child), routed through the shared hit-participation helper.

Source code in src/nuiitivet/layout/deck.py
def hit_test(self, x: int, y: int):
    """Only allow hit testing on the selected child.

    The Deck itself never becomes the hit target (S = none); it defers to
    the selected child only (C descends into one child), routed through the
    shared hit-participation helper.
    """
    children = expand_layout_children(self.children_snapshot())
    if not children or self._current_index >= len(children):
        return None

    selected_child = children[self._current_index]
    return self._resolve_hit(x, y, child_hit=selected_child.hit_test(x, y), self_opaque=False)

Collapsible

Collapsible(child: Optional[Widget] = None, *, opened: Union[bool, ObservableBase[bool]] = True, motion: Motion = _DEFAULT_MOTION, motion_out: Optional[Motion] = None, axis: Axis = 'both', alignment: Union[str, Tuple[str, str]] = 'top-left', key: Optional[str] = None)

Bases: FocusTraversalBlocker, Widget

Single-child widget that animates its layout size open and closed.

The child is always mounted and laid out at its own natural size; the allocated rectangle reported to the parent is interpolated per axis. The animated rectangle is clipped internally, so the child never overflows its allocated bounds.

preferred_size only reports that interpolated rectangle: measuring is speculative and repeated, so it never retargets. layout owns the animation -- both the retarget and the first-layout snap to the initial state.

Initialize a Collapsible.

Parameters:

Name Type Description Default
child Optional[Widget]

The Widget whose layout size is animated.

None
opened Union[bool, ObservableBase[bool]]

bool / Observable[bool]. When False the child collapses to zero size along the animated axes; when True it expands to the child's natural size.

True
motion Motion

Base motion used for both open (enter) and close (exit).

_DEFAULT_MOTION
motion_out Optional[Motion]

Optional motion that overrides only the close (exit) direction. When omitted, motion is used for both.

None
axis Axis

Which axis/axes to animate ("both", "horizontal", "vertical"). Axes that are not animated pass the child's natural size through unchanged.

'both'
alignment Union[str, Tuple[str, str]]

Alignment of the child within the animated rectangle.

'top-left'
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/collapsible.py
def __init__(
    self,
    child: Optional[Widget] = None,
    *,
    opened: Union[bool, ObservableBase[bool]] = True,
    motion: Motion = _DEFAULT_MOTION,
    motion_out: Optional[Motion] = None,
    axis: Axis = "both",
    alignment: Union[str, Tuple[str, str]] = "top-left",
    key: Optional[str] = None,
) -> None:
    """Initialize a Collapsible.

    Args:
        child: The ``Widget`` whose layout size is animated.
        opened: ``bool`` / ``Observable[bool]``. When ``False`` the child
            collapses to zero size along the animated axes; when ``True``
            it expands to the child's natural size.
        motion: Base motion used for both open (enter) and close (exit).
        motion_out: Optional motion that overrides only the close (exit)
            direction. When omitted, ``motion`` is used for both.
        axis: Which axis/axes to animate (``"both"``, ``"horizontal"``,
            ``"vertical"``). Axes that are not animated pass the child's
            natural size through unchanged.
        alignment: Alignment of the child within the animated rectangle.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(max_children=1, overflow_policy="replace_last", key=key)
    self._opened: Union[bool, ObservableBase[bool]] = opened
    self._motion_in = motion
    self._motion_out = motion_out if motion_out is not None else motion
    self._axis: Axis = axis
    self._align = normalize_alignment(alignment, default=("start", "start"))

    self._width_anim: Animatable[float] = Animatable(0.0, motion=self._motion_in)
    self._height_anim: Animatable[float] = Animatable(0.0, motion=self._motion_in)
    self._width_sub: Optional["Disposable"] = None
    self._height_sub: Optional["Disposable"] = None
    self._initialized = False
    # Constraints of the most recent measure pass. ``layout`` reuses them
    # so the child is measured identically on both paths and the animation
    # target cannot depend on which pass asked.
    self._measure_constraints: Tuple[Optional[int], Optional[int]] = (None, None)

    if child is not None:
        self.add_child(child)

blocks_focus_traversal property

blocks_focus_traversal: bool

Keep the child out of the Tab sequence while the collapsible is closed.

The child stays mounted and laid out at its natural size while closed, so without this its focusable widgets would remain Tab stops behind the clip. Traversal follows the opened flag rather than the size animation: the content becomes reachable as soon as it starts expanding (it is on screen by then) and unreachable as soon as it starts collapsing.

preferred_size

preferred_size(max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]

Report the current (possibly interpolated) outer size.

Measuring never touches the animation: the reported size follows whatever layout last resolved. The constraints are remembered so the following layout measures the child the same way.

Source code in src/nuiitivet/layout/collapsible.py
def preferred_size(
    self,
    max_width: Optional[int] = None,
    max_height: Optional[int] = None,
) -> Tuple[int, int]:
    """Report the current (possibly interpolated) outer size.

    Measuring never touches the animation: the reported size follows
    whatever ``layout`` last resolved. The constraints are remembered so
    the following ``layout`` measures the child the same way.
    """
    self._measure_constraints = (max_width, max_height)
    natural_w, natural_h = self._natural_size(*self._child_constraints())
    return self._resolve_size(natural_w, natural_h)

VerticalScrollable

VerticalScrollable(child: Widget, *, controller: Optional[ScrollController] = None, scrollbar_visible: ScrollbarVisibleLike = True, width: SizingLike = None, height: SizingLike = None, scrollbar_behavior: Optional[ScrollbarBehavior] = None, scrollbar_style: Optional[ScrollbarStyle] = None, style: Optional[ScrollableStyle] = None, key: Optional[str] = None)

Bases: _ScrollableBase

Scrolls its child along the vertical axis.

Source code in src/nuiitivet/layout/scrollable.py
def __init__(
    self,
    child: Widget,
    *,
    controller: Optional[ScrollController] = None,
    scrollbar_visible: ScrollbarVisibleLike = True,
    width: SizingLike = None,
    height: SizingLike = None,
    scrollbar_behavior: Optional[ScrollbarBehavior] = None,
    scrollbar_style: Optional[ScrollbarStyle] = None,
    style: Optional[ScrollableStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize the scrollable.

    The three configuration concerns are kept separate: ``scrollbar_style``
    owns the bar's appearance, ``style`` owns placement (viewport padding,
    bar offset, overlay vs. inline), and ``scrollbar_behavior`` owns
    temporal behavior (auto-hide, track clicks…).

    Args:
        child: The widget to make scrollable.
        controller: External :class:`ScrollController` (auto-created when
            omitted). ``physics`` and ``scroll_multiplier`` are read from it.
        scrollbar_visible: Whether the scrollbar is shown. Accepts a ``bool``
            or an ``Observable[bool]`` for reactive visibility.
        width: Width sizing override.
        height: Height sizing override.
        scrollbar_behavior: Scrollbar interaction behavior (auto-hide,
            track clicks…).
        scrollbar_style: Scrollbar appearance (thickness, min thumb length).
        style: Placement (viewport padding, scrollbar padding, overlay).
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, key=key)

    if child is None:
        raise ValueError("Scrollable requires a child widget")

    self._child = child
    self.direction = self._direction
    self._apply_axis_sizing_defaults()

    if controller is None:
        self._controller = ScrollController(axes=(self.direction,), primary_axis=self.direction)
        self._owns_controller = True
    else:
        if not controller.has_axis(self.direction):
            raise ValueError(f"ScrollController does not support required axis {self.direction}")
        self._controller = controller
        self._owns_controller = False

    # Scroll-engine configuration is owned by the controller.
    self.physics = self._controller.physics
    self.scroll_multiplier = self._controller.scroll_multiplier

    self._scrollbar_behavior = scrollbar_behavior or ScrollbarBehavior()
    self._scrollbar_style = scrollbar_style or ScrollbarStyle()
    self._scrollable_style = style or ScrollableStyle()
    #: Bar offset from the viewport edge, parsed to (left, top, right, bottom).
    self._scrollbar_padding = parse_padding(self._scrollable_style.scrollbar_padding)
    self._scrollbar_visible: ScrollbarVisibleLike = scrollbar_visible

    self._viewport = ScrollViewport(
        child=child,
        controller=self._controller,
        direction=self.direction,
        padding=parse_padding(self._scrollable_style.viewport_padding),
    )
    self.add_child(self._viewport)

    self._scrollbar = self._scrollbar_class(
        self._controller,
        behavior=self._scrollbar_behavior,
        style=self._scrollbar_style,
    )
    self.add_child(self._scrollbar)

    # Allow the scrollbar to coordinate with this container (e.g. cancel an
    # active content drag when the user begins dragging the thumb).
    try:
        setattr(self._scrollbar, "_scroll_container", self)
    except Exception:
        exception_once(logger, "scrollable_set_scroll_container_exc", "Failed to set scrollbar._scroll_container")

    # Drag-scroll state
    self._is_dragging = False
    self._drag_start_pos = 0.0
    self._drag_start_offset = 0.0
    self._content_pointer_id: Optional[int] = None

    # Scrollbar regions (for hit-testing)
    self._scrollbar_rect: Optional[Tuple[int, int, int, int]] = None
    self._scrollbar_thumb_rect: Optional[Tuple[int, int, int, int]] = None

    # Listener disposal handle (may be a Disposable or a callable)
    self._scroll_unsubscribe: Optional[object] = None

HorizontalScrollable

HorizontalScrollable(child: Widget, *, controller: Optional[ScrollController] = None, scrollbar_visible: ScrollbarVisibleLike = True, width: SizingLike = None, height: SizingLike = None, scrollbar_behavior: Optional[ScrollbarBehavior] = None, scrollbar_style: Optional[ScrollbarStyle] = None, style: Optional[ScrollableStyle] = None, key: Optional[str] = None)

Bases: _ScrollableBase

Scrolls its child along the horizontal axis.

Source code in src/nuiitivet/layout/scrollable.py
def __init__(
    self,
    child: Widget,
    *,
    controller: Optional[ScrollController] = None,
    scrollbar_visible: ScrollbarVisibleLike = True,
    width: SizingLike = None,
    height: SizingLike = None,
    scrollbar_behavior: Optional[ScrollbarBehavior] = None,
    scrollbar_style: Optional[ScrollbarStyle] = None,
    style: Optional[ScrollableStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize the scrollable.

    The three configuration concerns are kept separate: ``scrollbar_style``
    owns the bar's appearance, ``style`` owns placement (viewport padding,
    bar offset, overlay vs. inline), and ``scrollbar_behavior`` owns
    temporal behavior (auto-hide, track clicks…).

    Args:
        child: The widget to make scrollable.
        controller: External :class:`ScrollController` (auto-created when
            omitted). ``physics`` and ``scroll_multiplier`` are read from it.
        scrollbar_visible: Whether the scrollbar is shown. Accepts a ``bool``
            or an ``Observable[bool]`` for reactive visibility.
        width: Width sizing override.
        height: Height sizing override.
        scrollbar_behavior: Scrollbar interaction behavior (auto-hide,
            track clicks…).
        scrollbar_style: Scrollbar appearance (thickness, min thumb length).
        style: Placement (viewport padding, scrollbar padding, overlay).
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, key=key)

    if child is None:
        raise ValueError("Scrollable requires a child widget")

    self._child = child
    self.direction = self._direction
    self._apply_axis_sizing_defaults()

    if controller is None:
        self._controller = ScrollController(axes=(self.direction,), primary_axis=self.direction)
        self._owns_controller = True
    else:
        if not controller.has_axis(self.direction):
            raise ValueError(f"ScrollController does not support required axis {self.direction}")
        self._controller = controller
        self._owns_controller = False

    # Scroll-engine configuration is owned by the controller.
    self.physics = self._controller.physics
    self.scroll_multiplier = self._controller.scroll_multiplier

    self._scrollbar_behavior = scrollbar_behavior or ScrollbarBehavior()
    self._scrollbar_style = scrollbar_style or ScrollbarStyle()
    self._scrollable_style = style or ScrollableStyle()
    #: Bar offset from the viewport edge, parsed to (left, top, right, bottom).
    self._scrollbar_padding = parse_padding(self._scrollable_style.scrollbar_padding)
    self._scrollbar_visible: ScrollbarVisibleLike = scrollbar_visible

    self._viewport = ScrollViewport(
        child=child,
        controller=self._controller,
        direction=self.direction,
        padding=parse_padding(self._scrollable_style.viewport_padding),
    )
    self.add_child(self._viewport)

    self._scrollbar = self._scrollbar_class(
        self._controller,
        behavior=self._scrollbar_behavior,
        style=self._scrollbar_style,
    )
    self.add_child(self._scrollbar)

    # Allow the scrollbar to coordinate with this container (e.g. cancel an
    # active content drag when the user begins dragging the thumb).
    try:
        setattr(self._scrollbar, "_scroll_container", self)
    except Exception:
        exception_once(logger, "scrollable_set_scroll_container_exc", "Failed to set scrollbar._scroll_container")

    # Drag-scroll state
    self._is_dragging = False
    self._drag_start_pos = 0.0
    self._drag_start_offset = 0.0
    self._content_pointer_id: Optional[int] = None

    # Scrollbar regions (for hit-testing)
    self._scrollbar_rect: Optional[Tuple[int, int, int, int]] = None
    self._scrollbar_thumb_rect: Optional[Tuple[int, int, int, int]] = None

    # Listener disposal handle (may be a Disposable or a callable)
    self._scroll_unsubscribe: Optional[object] = None

ForEach

ForEach(items: ItemsLike, builder: BuilderFn, *, key_fn: Optional[Callable[[Any, int], Any]] = None, key: Optional[str] = None)

Bases: ComposableWidget

Initialize the ForEach data provider.

Parameters:

Name Type Description Default
items ItemsLike

The source data collection. Can be an Iterable, an Observable, or an object with a .value attribute (checking value first).

required
builder BuilderFn

A function that takes (item, index) and returns a Widget.

required
key_fn Optional[Callable[[Any, int], Any]]

An optional function (item, index) -> Any to identify items uniquely. Using keys improves performance by recycling widgets when items are reordered. If None, the index is used as the key.

None
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload (this ForEach node itself — not the per-item identity, which is key_fn's job).

None
Source code in src/nuiitivet/layout/for_each.py
def __init__(
    self,
    items: ItemsLike,
    builder: BuilderFn,
    *,
    key_fn: Optional[Callable[[Any, int], Any]] = None,
    key: Optional[str] = None,
):
    """Initialize the ForEach data provider.

    Args:
        items: The source data collection. Can be an Iterable, an Observable,
            or an object with a .value attribute (checking value first).
        builder: A function that takes (item, index) and returns a Widget.
        key_fn: An optional function `(item, index) -> Any` to identify items
            uniquely. Using keys improves performance by recycling widgets
            when items are reordered. If None, the index is used as the key.
        key: Stable widget identity for dev-bridge targeting and hot reload
            (this ForEach node itself — not the per-item identity, which is
            ``key_fn``'s job).
    """
    super().__init__(key=key)
    self.items = items
    self._items_handle = self._capture_items_handle(items)
    self.builder = builder
    self.key_fn = key_fn
    self._items_unsub: Optional[Callable[[], None]] = None
    self._entries_by_token: Dict[str, _ForEachEntry] = {}
    self._ordered_entries: List[_ForEachEntry] = []
    self._provider_children: List[Widget] = []
    self._pending_tokens: Optional[List[_TokenInfo]] = None

provide_layout_children

provide_layout_children() -> List[Widget]

Expose built children so parent layouts can measure/paint them.

Source code in src/nuiitivet/layout/for_each.py
def provide_layout_children(self) -> List[Widget]:
    """Expose built children so parent layouts can measure/paint them."""

    if not self._provider_children:
        self._rebuild_children()
    if self._provider_children:
        return list(self._provider_children)
    return list(self.children_snapshot())

ScrollbarBehavior dataclass

ScrollbarBehavior(auto_hide: bool = True, hide_delay: float = 1.0, fade_duration: float = 0.15, hide_threshold: float = 0.25, track_click_behavior: str = 'jump', interactive: bool = True, hit_slop: Optional[int] = None)

Immutable behavior configuration for scrollbar widgets.

MenuBar

MenuBar(items: Sequence[MenuEntry], *, style: Optional[MenuBarStyle] = None)

The application menu bar model: the root of the declarative menu tree.

Registered on the App (App(menu=nv.MenuBar([...]))) and replaced wholesale via app.menu = .... Structure is not observable — entry properties (label / enabled / checked) may be Observables, but adding or removing entries means assigning a new model.

Parameters:

Name Type Description Default
items Sequence[MenuEntry]

Top-level entries. Bar entries are usually submenus (MenuEntry("File", submenu=[...])); a plain action entry is allowed and activates directly on click.

required
style Optional[MenuBarStyle]

Optional per-instance style; None follows the theme.

None
Source code in src/nuiitivet/menubar/model.py
def __init__(
    self,
    items: Sequence[MenuEntry],
    *,
    style: Optional[MenuBarStyle] = None,
) -> None:
    entries = tuple(items)
    for entry in entries:
        if not isinstance(entry, MenuEntry):
            raise TypeError("MenuBar items must be MenuEntry instances.")
    self.items: Tuple[MenuEntry, ...] = entries
    self.style = style

MenuBarArea

MenuBarArea(*, key: Optional[str] = None)

Bases: MenuBarSlotBase

Marks where the registered menu bar renders, instead of the default spot.

Place it anywhere in the tree — typically inside a CustomChrome header row — and the App-registered menu model renders there; the App's automatic insertion below the chrome is suppressed. Menu definitions, callbacks and shortcuts are unaffected by placement.

With no menu registered it renders nothing, so conditional menus are fine. If several MenuBarArea widgets are mounted at once, only the first one renders (a warning is logged).

On macOS the menu goes to the global menu bar (NSMenu) and the area collapses to zero size — a chrome written around a MenuBarArea degrades to a plain title bar with no platform branching.

Source code in src/nuiitivet/menubar/slots.py
def __init__(self, *, key: Optional[str] = None) -> None:
    super().__init__(key=key)
    self._active = False
    self._model: Optional[MenuBar] = None
    self._controller = None

MenuBarStyle dataclass

MenuBarStyle(bar_height: int = 34, bar_horizontal_padding: int = 4, item_horizontal_padding: int = 10, item_gap: int = 2, item_corner_radius: int = 6, label_size: int = 13, popup_corner_radius: int = 8, popup_min_width: int = 180, bar_background: Optional[ColorSpec] = None, bar_foreground: Optional[ColorSpec] = None, bar_disabled_foreground: Optional[ColorSpec] = None, bar_open_background: Optional[ColorSpec] = None, bar_state_layer: Optional[ColorSpec] = None, popup_background: Optional[ColorSpec] = None, popup_foreground: Optional[ColorSpec] = None, popup_accelerator: Optional[ColorSpec] = None, popup_disabled_foreground: Optional[ColorSpec] = None, popup_state_layer: Optional[ColorSpec] = None, popup_divider: Optional[ColorSpec] = None)

Immutable visual style for the in-app menu bar.

Holds geometry plus optional per-instance color overrides, mirroring :class:~nuiitivet.scrolling.ScrollbarStyle: the app-wide default palette is supplied by the design system via :class:~nuiitivet.menubar.MenuBarThemeData, and the nullable ColorSpec fields here override it per instance. A None color falls back to the theme. Attach it to the model root: MenuBar(items, style=...).

On macOS none of this applies — the global menu bar is rendered by the OS (see :mod:nuiitivet.menubar.nsmenu).

Attributes:

Name Type Description
bar_height int

Height of the horizontal bar in pixels.

bar_horizontal_padding int

Padding at the left/right ends of the bar.

item_horizontal_padding int

Horizontal padding inside each top-level item.

item_gap int

Gap between top-level items.

item_corner_radius int

Corner radius of the top-level item highlight.

label_size int

Font size of top-level item labels.

popup_corner_radius int

Corner radius of popup surfaces.

popup_min_width int

Minimum popup width in pixels.

bar_background Optional[ColorSpec]

Per-instance override (None → theme). The remaining color fields override the same-named :class:~nuiitivet.menubar.MenuBarThemeData slots.

copy_with

copy_with(**changes) -> 'MenuBarStyle'

Return a copy of this style with the given fields overridden.

Source code in src/nuiitivet/menubar/style.py
def copy_with(self, **changes) -> "MenuBarStyle":
    """Return a copy of this style with the given fields overridden."""
    return replace(self, **changes)

merged_palette

merged_palette(theme_data: Optional[MenuBarThemeData]) -> MenuBarThemeData

The effective palette: theme data with this style's overrides applied.

Parameters:

Name Type Description Default
theme_data Optional[MenuBarThemeData]

App-wide palette from the active theme, or None when no design system registered one (the neutral defaults are used then).

required

Returns:

Name Type Description
A MenuBarThemeData

class:MenuBarThemeData where every slot named by a non-None

MenuBarThemeData

color field on this style is replaced by that override.

Source code in src/nuiitivet/menubar/style.py
def merged_palette(self, theme_data: Optional[MenuBarThemeData]) -> MenuBarThemeData:
    """The effective palette: theme data with this style's overrides applied.

    Args:
        theme_data: App-wide palette from the active theme, or ``None``
            when no design system registered one (the neutral defaults are
            used then).

    Returns:
        A :class:`MenuBarThemeData` where every slot named by a non-``None``
        color field on this style is replaced by that override.
    """
    base = theme_data or MenuBarThemeData()
    overrides = {}
    for field in fields(MenuBarThemeData):
        value = getattr(self, field.name, None)
        if value is not None:
            overrides[field.name] = value
    return base.copy_with(**overrides) if overrides else base

MenuBarThemeData dataclass

MenuBarThemeData(bar_background: ColorSpec = ('#000000', 0.04), bar_foreground: ColorSpec = ('#000000', 0.87), bar_disabled_foreground: ColorSpec = ('#000000', 0.38), bar_open_background: ColorSpec = ('#000000', 0.12), bar_state_layer: ColorSpec = '#000000', popup_background: ColorSpec = '#FFFFFF', popup_foreground: ColorSpec = ('#000000', 0.87), popup_accelerator: ColorSpec = ('#000000', 0.6), popup_disabled_foreground: ColorSpec = '#000000', popup_state_layer: ColorSpec = '#000000', popup_divider: ColorSpec = ('#000000', 0.12))

App-wide themeable colors for the menu bar, resolved at paint time.

The defaults are neutral, design-system-agnostic literals so that a bare MenuBarThemeData() is usable even when no design system is registered.

Attributes:

Name Type Description
bar_background ColorSpec

Background of the horizontal bar.

bar_foreground ColorSpec

Label color of top-level bar items.

bar_disabled_foreground ColorSpec

Label color of disabled top-level items.

bar_open_background ColorSpec

Background of the top-level item whose menu is open.

bar_state_layer ColorSpec

Hover/press state-layer color for bar items.

popup_background ColorSpec

Popup container background.

popup_foreground ColorSpec

Popup item label color.

popup_accelerator ColorSpec

Accelerator text color in popup items.

popup_disabled_foreground ColorSpec

Base color for disabled popup items.

popup_state_layer ColorSpec

Hover/press/focus state-layer color for popup items.

popup_divider ColorSpec

Separator line color inside popups.

copy_with

copy_with(**changes: Any) -> 'MenuBarThemeData'

Return a copy of this theme data with the given fields overridden.

Source code in src/nuiitivet/menubar/theme_data.py
def copy_with(self, **changes: Any) -> "MenuBarThemeData":
    """Return a copy of this theme data with the given fields overridden."""
    return replace(self, **changes)

MenuEntry

MenuEntry(label: ObservableStr = '', *, on_select: Optional[VoidCallback] = None, shortcut: Optional[ShortcutLike] = None, enabled: ObservableBool = True, checked: Optional[MutableObservableBase[bool]] = None, submenu: Optional[Sequence['MenuEntry']] = None, _role: MenuRole = NONE, _separator: bool = False)

One entry in a declarative menu model.

A single type covers all roles:

  • Action: MenuEntry("Open...", on_select=..., shortcut="Accel+O")
  • Submenu: MenuEntry("File", submenu=[...]) — top-level bar entries are simply entries with a submenu; nesting is unlimited.
  • Separator: MenuEntry.separator()
  • Standard item: MenuEntry.quit() and friends — prebuilt entries whose activation calls a built-in App/Window method.

label and enabled may be Observables and propagate live to whichever surface renders the model. checked (presence makes the entry checkable) must be a writable Observable: activation toggles it before on_select runs.

Parameters:

Name Type Description Default
label ObservableStr

Entry label; a plain string or an Observable.

''
on_select Optional[VoidCallback]

Called with no arguments when the entry is activated. May be sync or async. Exactly one of on_select / submenu / a standard-item role is required for a non-separator entry.

None
shortcut Optional[ShortcutLike]

Accelerator gesture, as a spec string ("Accel+S") or a :class:~nuiitivet.input.shortcut.Shortcut. The menu system both displays it and registers it; do not register the same gesture separately via key_shortcut().

None
enabled ObservableBool

Whether the entry can be activated; a bool or an Observable.

True
checked Optional[MutableObservableBase[bool]]

Writable Observable holding the check state. Presence makes the entry checkable; activation toggles the value, then calls on_select.

None
submenu Optional[Sequence['MenuEntry']]

Child entries. Mutually exclusive with on_select / shortcut / checked.

None

Raises:

Type Description
ValueError

If the combination of arguments is invalid.

Source code in src/nuiitivet/menus/model.py
def __init__(
    self,
    label: ObservableStr = "",
    *,
    on_select: Optional[VoidCallback] = None,
    shortcut: Optional[ShortcutLike] = None,
    enabled: ObservableBool = True,
    checked: Optional[MutableObservableBase[bool]] = None,
    submenu: Optional[Sequence["MenuEntry"]] = None,
    _role: MenuRole = MenuRole.NONE,
    _separator: bool = False,
) -> None:
    self.label = label
    self.on_select = on_select
    self.shortcut = to_shortcut(shortcut) if shortcut is not None else None
    self.enabled = enabled
    self.checked = checked
    self.submenu = tuple(submenu) if submenu is not None else None
    self.role = _role
    self.is_separator = bool(_separator)
    self._validate()

resolved_label

resolved_label() -> str

The current label text (reads the Observable if there is one).

Source code in src/nuiitivet/menus/model.py
def resolved_label(self) -> str:
    """The current label text (reads the Observable if there is one)."""
    return str(read_value(self.label))

resolved_enabled

resolved_enabled() -> bool

The current enabled state (reads the Observable if there is one).

Source code in src/nuiitivet/menus/model.py
def resolved_enabled(self) -> bool:
    """The current enabled state (reads the Observable if there is one)."""
    return bool(read_value(self.enabled))

separator classmethod

separator() -> 'MenuEntry'

A horizontal separator line between entries.

Source code in src/nuiitivet/menus/model.py
@classmethod
def separator(cls) -> "MenuEntry":
    """A horizontal separator line between entries."""
    return cls(_separator=True)

quit classmethod

quit(*, label: Optional[ObservableStr] = None, shortcut: Optional[ShortcutLike] = None, enabled: ObservableBool = True) -> 'MenuEntry'

Exit the application (calls app.exit()).

Source code in src/nuiitivet/menus/model.py
@classmethod
def quit(
    cls,
    *,
    label: Optional[ObservableStr] = None,
    shortcut: Optional[ShortcutLike] = None,
    enabled: ObservableBool = True,
) -> "MenuEntry":
    """Exit the application (calls ``app.exit()``)."""
    if label is None:
        label = "Quit" if sys.platform == "darwin" else "Exit"
    if shortcut is None and sys.platform == "darwin":
        shortcut = "Accel+Q"
    return cls(label, shortcut=shortcut, enabled=enabled, _role=MenuRole.QUIT)

close_window classmethod

close_window(*, label: ObservableStr = 'Close Window', shortcut: Optional[ShortcutLike] = 'Accel+W', enabled: ObservableBool = True) -> 'MenuEntry'

Close the window (calls window.close()).

Source code in src/nuiitivet/menus/model.py
@classmethod
def close_window(
    cls,
    *,
    label: ObservableStr = "Close Window",
    shortcut: Optional[ShortcutLike] = "Accel+W",
    enabled: ObservableBool = True,
) -> "MenuEntry":
    """Close the window (calls ``window.close()``)."""
    return cls(label, shortcut=shortcut, enabled=enabled, _role=MenuRole.CLOSE_WINDOW)

minimize classmethod

minimize(*, label: ObservableStr = 'Minimize', shortcut: Optional[ShortcutLike] = None, enabled: ObservableBool = True) -> 'MenuEntry'

Minimize the window (calls window.minimize()).

Source code in src/nuiitivet/menus/model.py
@classmethod
def minimize(
    cls,
    *,
    label: ObservableStr = "Minimize",
    shortcut: Optional[ShortcutLike] = None,
    enabled: ObservableBool = True,
) -> "MenuEntry":
    """Minimize the window (calls ``window.minimize()``)."""
    if shortcut is None and sys.platform == "darwin":
        shortcut = "Accel+M"
    return cls(label, shortcut=shortcut, enabled=enabled, _role=MenuRole.MINIMIZE)

maximize classmethod

maximize(*, label: Optional[ObservableStr] = None, shortcut: Optional[ShortcutLike] = None, enabled: ObservableBool = True) -> 'MenuEntry'

Maximize / zoom the window (calls window.maximize()).

Source code in src/nuiitivet/menus/model.py
@classmethod
def maximize(
    cls,
    *,
    label: Optional[ObservableStr] = None,
    shortcut: Optional[ShortcutLike] = None,
    enabled: ObservableBool = True,
) -> "MenuEntry":
    """Maximize / zoom the window (calls ``window.maximize()``)."""
    if label is None:
        label = "Zoom" if sys.platform == "darwin" else "Maximize"
    return cls(label, shortcut=shortcut, enabled=enabled, _role=MenuRole.MAXIMIZE)

restore classmethod

restore(*, label: ObservableStr = 'Restore', shortcut: Optional[ShortcutLike] = None, enabled: ObservableBool = True) -> 'MenuEntry'

Restore the window (calls window.restore()).

The way back from :meth:full_screen, :meth:maximize, and :meth:minimize: exits full screen, or restores the pre-maximize size, or brings a minimized window back.

Source code in src/nuiitivet/menus/model.py
@classmethod
def restore(
    cls,
    *,
    label: ObservableStr = "Restore",
    shortcut: Optional[ShortcutLike] = None,
    enabled: ObservableBool = True,
) -> "MenuEntry":
    """Restore the window (calls ``window.restore()``).

    The way back from :meth:`full_screen`, :meth:`maximize`, and
    :meth:`minimize`: exits full screen, or restores the pre-maximize
    size, or brings a minimized window back.
    """
    return cls(label, shortcut=shortcut, enabled=enabled, _role=MenuRole.RESTORE)

full_screen classmethod

full_screen(*, label: ObservableStr = 'Full Screen', shortcut: Optional[ShortcutLike] = None, enabled: ObservableBool = True) -> 'MenuEntry'

Toggle full screen (calls window.full_screen()).

Source code in src/nuiitivet/menus/model.py
@classmethod
def full_screen(
    cls,
    *,
    label: ObservableStr = "Full Screen",
    shortcut: Optional[ShortcutLike] = None,
    enabled: ObservableBool = True,
) -> "MenuEntry":
    """Toggle full screen (calls ``window.full_screen()``)."""
    return cls(label, shortcut=shortcut, enabled=enabled, _role=MenuRole.FULL_SCREEN)

MenuRole

Bases: Enum

Built-in command a standard :class:MenuEntry invokes.

A role entry needs no on_select: activating it calls the mapped built-in method (see nuiitivet.menubar.controller). Roles are also what the macOS bridge will use to relocate items to their conventional places (e.g. Quit into the application menu).

Widget

Widget(*, width: Union[SizingLike, ReadOnlyObservableProtocol] = None, height: Union[SizingLike, ReadOnlyObservableProtocol] = None, padding: Union[PaddingLike, ReadOnlyObservableProtocol] = None, max_children: Optional[int] = None, overflow_policy: str = 'none', key: Optional[str] = None)

Bases: BindingHostMixin, LifecycleHostMixin, InputHubMixin, ChildContainerMixin, WidgetKernel

Leaf-friendly widget base composed from mixins.

This class intentionally does not participate in build/recomposition. Widgets that need build()/rebuild()/scope() must inherit ComposableWidget.

Source code in src/nuiitivet/widgeting/widget.py
def __init__(
    self,
    *,
    width: Union[SizingLike, ReadOnlyObservableProtocol] = None,
    height: Union[SizingLike, ReadOnlyObservableProtocol] = None,
    padding: Union[PaddingLike, ReadOnlyObservableProtocol] = None,
    max_children: Optional[int] = None,
    overflow_policy: str = "none",
    key: Optional[str] = None,
) -> None:
    self._layout_cache_token = 0
    self._needs_layout = True
    # A stable, layout-independent identity (a "testID"). It serves two
    # purposes: the dev action bridge targets widgets by ``key`` instead of
    # brittle pixel coordinates, and hot reload uses it as a stable
    # anchor so state survives a structural edit.
    self.key: Optional[str] = str(key) if key is not None else None
    super().__init__(
        width=width,
        height=height,
        padding=padding,
        max_children=max_children,
        overflow_policy=overflow_policy,
    )

mark_needs_layout

mark_needs_layout() -> None

Mark this widget as needing layout recalculation.

Source code in src/nuiitivet/widgeting/widget.py
def mark_needs_layout(self) -> None:
    """Mark this widget as needing layout recalculation."""
    already_dirty = self._needs_layout
    self._needs_layout = True
    self._measure_cache = None
    parent = getattr(self, "_parent", None)
    if isinstance(parent, Widget):
        # Always propagate to root.  An early-return guard ("if already
        # dirty, skip") would be valid only if the invariant "every dirty
        # node's ancestors are also dirty" were globally maintained.
        # However, clear_needs_layout() is called only on AppScope (the
        # root), leaving intermediate nodes dirty.  When the next animation
        # tick fires, a selective guard would stop propagation at the first
        # already-dirty intermediate, never reaching the cleared root.
        # Walking all the way to the root on every call is O(tree-depth)
        # – the same cost as the original code when no ancestor was dirty.
        try:
            parent.mark_needs_layout()
        except Exception:
            exception_once(
                logger,
                f"widget_mark_needs_layout_parent_exc:{type(parent).__name__}",
                "Widget.mark_needs_layout() failed for parent=%s",
                type(parent).__name__,
            )
    if not already_dirty:
        self.invalidate()

find_ancestor

find_ancestor(widget_type: Type[T]) -> Optional[T]

Find the nearest ancestor of the specified type.

Traverses up the widget tree looking for an ancestor that matches the given type. This is used to implement context lookup patterns like Navigator.of(context).

Parameters:

Name Type Description Default
widget_type Type[T]

The type of widget to find.

required

Returns:

Type Description
Optional[T]

The nearest ancestor of the specified type, or None if not found.

Example

navigator = self.find_ancestor(Navigator) if navigator: navigator.push(...)

Source code in src/nuiitivet/widgeting/widget.py
def find_ancestor(self, widget_type: Type[T]) -> Optional[T]:
    """Find the nearest ancestor of the specified type.

    Traverses up the widget tree looking for an ancestor that matches the given type.
    This is used to implement context lookup patterns like Navigator.of(context).

    Args:
        widget_type: The type of widget to find.

    Returns:
        The nearest ancestor of the specified type, or None if not found.

    Example:
        navigator = self.find_ancestor(Navigator)
        if navigator:
            navigator.push(...)
    """
    current = self._parent
    while current is not None:
        if isinstance(current, widget_type):
            return current  # type: ignore
        current = getattr(current, "_parent", None)
    return None

modifier

modifier(modifier: Union[Modifier, ModifierElement]) -> Widget

Apply a modifier to this widget, returning the wrapped result.

Parameters:

Name Type Description Default
modifier Union[Modifier, ModifierElement]

The modifier (or modifier element) to apply.

required

Returns:

Type Description
Widget

The wrapped widget (e.g. ModifierBox) or the widget itself if modified in-place.

Source code in src/nuiitivet/widgeting/widget.py
def modifier(self, modifier: Union[Modifier, ModifierElement]) -> Widget:
    """Apply a modifier to this widget, returning the wrapped result.

    Args:
        modifier: The modifier (or modifier element) to apply.

    Returns:
        The wrapped widget (e.g. ModifierBox) or the widget itself if modified in-place.
    """
    return modifier.apply(self)

ComposableWidget

ComposableWidget(*args, **kwargs)

Bases: BuilderHostMixin, Widget

Widget base that participates in build/recomposition.

Composition widgets can override build() and use scope() / render_scope() for fine-grained recomposition.

Layout metadata is transparent: a value declared on this widget wins, and an undeclared one is derived from the widget its build() returned — so extracting a subtree into a composable does not change how the tree lays out.

Source code in src/nuiitivet/widgeting/widget_builder.py
def __init__(self, *args, **kwargs) -> None:  # type: ignore[override]
    super().__init__(*args, **kwargs)
    self._built = None
    self._scope_root: Optional[RecomposeScope] = None
    self._build_ctx: Optional[BuildScopeContext] = None
    self._scope_registry: Dict[str, RecomposeScope] = {}
    self._scope_nodes: Dict[str, Any] = {}
    self._active_scope_ids: Set[str] = set()
    self._scope_metadata: Dict[str, ScopeMetadata] = {}
    self._dependency_scope_index: Dict[str, Set[str]] = {}
    # Scopes whose inputs were invalidated and must be rebuilt on the next
    # render. A scope rebuilds only when it is new, unbuilt, or dirty — not
    # whenever the host's build() re-runs (idempotent recomposition).
    self._dirty_scopes: Set[str] = set()

Box

Box(child: Optional[Widget] = None, width: SizingLike = None, height: SizingLike = None, padding: PaddingLike = 0, background_color: Optional[ColorSpec] = None, border_width: float = 0, border_color: Optional[ColorSpec] = None, corner_radius: Union[float, Tuple[float, float, float, float]] = 0, shadows: ShadowLike = None, alignment: Union[str, Tuple[str, str]] = 'center', *, key: Optional[str] = None)

Bases: CachedPaintMixin, Widget

(Advanced) Low-level drawing primitive.

Note

Prefer using Modifiers (e.g. .background(), .border(), .shadows()) on a Container or other widgets. This widget is used internally to implement those modifiers.

A widget that draws a box with optional background, border, and shadow.

Source code in src/nuiitivet/widgets/box.py
def __init__(
    self,
    child: Optional[Widget] = None,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: PaddingLike = 0,
    # Visual properties
    background_color: Optional[ColorSpec] = None,
    border_width: float = 0,
    border_color: Optional[ColorSpec] = None,
    corner_radius: Union[float, Tuple[float, float, float, float]] = 0,
    shadows: ShadowLike = None,
    # Alignment for the child (if present)
    alignment: Union[str, Tuple[str, str]] = "center",
    *,
    key: Optional[str] = None,
):
    super().__init__(width=width, height=height, padding=padding, key=key)
    self._theme_state_ready = False
    self._bgcolor: Optional[ColorSpec] = None
    self._border_color: Optional[ColorSpec] = None
    self._shadows: Shadows = ()
    if child:
        self.add_child(child)

    self.bgcolor = background_color
    self.border_width = border_width
    self.border_color = border_color
    self.corner_radius = corner_radius
    self.shadows = shadows
    self.alignment = alignment
    self.clip_content = False

    self._theme_state_ready = True

    self._renderer = BackgroundRenderer(self)
    self._layout = LayoutEngine(self)

shadows property writable

shadows: Shadows

The shadow layers this box draws, ordered back to front.

visual_clip_rect

visual_clip_rect() -> Optional[Tuple[float, float, float, float]]

Return the rect content is clipped to, in this widget's local coords.

None when the box clips nothing, which is the common case.

This publishes, under the name geometric readers probe for (see nuiitivet._interaction.perception), the same answer paint and :meth:hit_test already act on. Without it a child laid out outside the box -- an oversized decorative shape trimmed to a corner, the idiom that fakes a gradient -- keeps its full layout rect as far as anything reading the tree geometrically is concerned, and so looks reachable at coordinates where none of it is painted.

Derived from layout state rather than last_rect: an action settles the tree by laying it out without painting, so paint state is stale exactly when this is needed.

Source code in src/nuiitivet/widgets/box.py
def visual_clip_rect(self) -> Optional[Tuple[float, float, float, float]]:
    """Return the rect content is clipped to, in this widget's local coords.

    ``None`` when the box clips nothing, which is the common case.

    This publishes, under the name geometric readers probe for (see
    ``nuiitivet._interaction.perception``), the same answer ``paint`` and
    :meth:`hit_test` already act on. Without it a child laid out *outside*
    the box -- an oversized decorative shape trimmed to a corner, the idiom
    that fakes a gradient -- keeps its full layout rect as far as anything
    reading the tree geometrically is concerned, and so looks reachable at
    coordinates where none of it is painted.

    Derived from layout state rather than ``last_rect``: an action settles
    the tree by laying it out without painting, so paint state is stale
    exactly when this is needed.
    """
    if not self.clip_content:
        return None
    rect = self.layout_rect
    if rect is None:
        return None
    _x, _y, w, h = rect
    return (0.0, 0.0, float(w), float(h))

corner_radii_pixels

corner_radii_pixels(width: int, height: int) -> Tuple[float, float, float, float]

Return the resolved corner radii in pixels for the given size.

Source code in src/nuiitivet/widgets/box.py
def corner_radii_pixels(self, width: int, height: int) -> Tuple[float, float, float, float]:
    """Return the resolved corner radii in pixels for the given size."""
    return self._renderer.corner_radii_pixels(width, height)

InputFilter

Bases: ABC

A rule applied to text as the user types it.

Subclasses implement :meth:apply, which receives the value before the edit and the value the edit proposes, and returns the value to accept. Returning old rejects the edit outright; returning new accepts it unchanged.

apply abstractmethod

apply(old: TextEditingValue, new: TextEditingValue) -> TextEditingValue

Return the value to accept for an edit from old to new.

Source code in src/nuiitivet/widgets/input_filter.py
@abstractmethod
def apply(self, old: TextEditingValue, new: TextEditingValue) -> TextEditingValue:
    """Return the value to accept for an edit from ``old`` to ``new``."""

Geometry

Geometry(child: Widget, *, width: SizingLike = None, height: SizingLike = None, key: Optional[str] = None)

Bases: Widget

Publishes this widget's own measured geometry to its subtree.

Transparent to layout: the single child receives this widget's own size. Each layout pass publishes that size as an Observable[Size], read by descendants via :meth:Geometry.of — bind it (mapped); a .value read at build time is a one-time snapshot. Map it into a value widget, or drive a Deck index for a structural switch::

class Panel(ComposableWidget):
    def build(self) -> Widget:
        size = Geometry.of(self).size
        return Deck(
            children=[_NarrowLayout(...), _WideLayout(...)],
            index=size.map(lambda s: 1 if s.width >= 600 else 0),
        )

Geometry(Panel())

The nearest ancestor provider wins, so wrapping a panel makes descendants react to the panel, not the window; with no nearer provider, reads fall back to the root Geometry the app installs at the window and track the window size. The size is measured during layout and published between frames: consumers see it one frame later, never a torn mix of old and new.

Wrap child, publishing this widget's measured size to its subtree.

Parameters:

Name Type Description Default
child Widget

The single child laid out at this widget's own size.

required
width SizingLike

Sizing for this widget (None shrink-wraps the child; "wt" / Sizing.weight(...) fills the space the parent offers). Use a filling size to measure the space available to a content pane, not just the child's intrinsic size.

None
height SizingLike

Sizing for this widget; see width.

None
key Optional[str]

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/layout/geometry.py
def __init__(
    self,
    child: Widget,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    key: Optional[str] = None,
) -> None:
    """Wrap *child*, publishing this widget's measured size to its subtree.

    Args:
        child: The single child laid out at this widget's own size.
        width: Sizing for this widget (``None`` shrink-wraps the child;
            ``"wt"`` / ``Sizing.weight(...)`` fills the space the parent
            offers). Use a filling size to measure the space *available* to a
            content pane, not just the child's intrinsic size.
        height: Sizing for this widget; see ``width``.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, max_children=1, overflow_policy="replace_last", key=key)
    # A single atomic Observable[Size]: width and height update together so
    # consumers never read a torn (new width, old height) pair. The
    # Observable de-dupes equal values, so an unchanged size performs no
    # write and triggers no dependent recomposition (the oscillation guard).
    self._size: Observable[Size] = Observable(Size(0, 0))
    self.add_size_callback(self._publish_size)
    self.add_child(child)

size property

This widget's resolved (width, height), published between frames.

add_child

add_child(w: Widget) -> None

Keep at most one child; bypass overrides like :class:Container.

Source code in src/nuiitivet/layout/geometry.py
def add_child(self, w: Widget) -> None:
    """Keep at most one child; bypass overrides like :class:`Container`."""
    ChildContainerMixin.add_child(self, w)

of classmethod

of(context: Widget) -> GeometryT

Return the nearest ancestor :class:Geometry (nearest provider wins).

Parameters:

Name Type Description Default
context Widget

A widget in the subtree from which to search upward.

required

Raises:

Type Description
RuntimeError

If called before context is mounted (typically from __init__), or if no Geometry ancestor exists.

Source code in src/nuiitivet/layout/geometry.py
@classmethod
def of(cls: Type[GeometryT], context: Widget) -> GeometryT:
    """Return the nearest ancestor :class:`Geometry` (nearest provider wins).

    Args:
        context: A widget in the subtree from which to search upward.

    Raises:
        RuntimeError: If called before ``context`` is mounted (typically from
            ``__init__``), or if no ``Geometry`` ancestor exists.
    """
    geometry = find_provider(context, cls)
    if geometry is None:
        raise_if_premature_lookup(f"{cls.__name__}.of", context)
        raise RuntimeError("Geometry not found in ancestors")
    return geometry

preferred_size

preferred_size(max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]

Report preferred size: own fixed sizing wins, else the child's.

A weight / "wt" sizing is not fixed, so the child's intrinsic size is reported here and the parent's weight distribution then stretches this widget to fill — which is what lets a filling Geometry measure the space available to a content pane.

Source code in src/nuiitivet/layout/geometry.py
def preferred_size(self, max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]:
    """Report preferred size: own fixed sizing wins, else the child's.

    A ``weight`` / ``"wt"`` sizing is not fixed, so the child's intrinsic
    size is reported here and the parent's weight distribution then stretches
    this widget to fill — which is what lets a filling ``Geometry`` measure
    the space available to a content pane.
    """
    if self.children:
        child_w, child_h = measure_preferred_size(self.children[0], max_width=max_width, max_height=max_height)
    else:
        child_w, child_h = 0, 0
    w = int(self.width_sizing.value) if self.width_sizing.kind == "fixed" else int(child_w)
    h = int(self.height_sizing.value) if self.height_sizing.kind == "fixed" else int(child_h)
    return (w, h)

layout

layout(width: int, height: int) -> None

Lay the child out at this widget's own size, then queue its publish.

Source code in src/nuiitivet/layout/geometry.py
def layout(self, width: int, height: int) -> None:
    """Lay the child out at this widget's own size, then queue its publish."""
    super().layout(width, height)
    if self.children:
        child = self.children[0]
        child.layout(width, height)
        child.set_layout_rect(0, 0, width, height)
    # Queue, don't write: an Observable write here would propagate to
    # consumers mid-pass. The queue delivers the final measurement to
    # _publish_size between frames, de-duped against the last report.
    queue_size_change(self, Size(int(width), int(height)))

paint

paint(canvas, x: int, y: int, width: int, height: int) -> None

Paint the child at this widget's own rect (transparent to paint).

Source code in src/nuiitivet/layout/geometry.py
def paint(self, canvas, x: int, y: int, width: int, height: int) -> None:
    """Paint the child at this widget's own rect (transparent to paint)."""
    self.set_last_rect(x, y, width, height)
    if not self.children:
        return
    child = self.children[0]
    if child.layout_rect is None:
        self.layout(width, height)
    child.set_last_rect(x, y, width, height)
    child.paint(canvas, x, y, width, height)

Size

Bases: NamedTuple

An immutable (width, height) pair in logical pixels.

A NamedTuple so it is compared by value: two equal sizes are ==, which lets a size report de-dupe an unchanged measurement without a custom comparator. It also unpacks like a tuple (w, h = size).

Shadow dataclass

Shadow(color: ColorSpec, blur_radius: float = 0.0, offset: Tuple[float, float] = (0.0, 0.0), spread_radius: float = 0.0)

A single shadow layer, in CSS box-shadow terms.

A widget's shadow is one or more of these layers, stacked back to front.

Parameters:

Name Type Description Default
color ColorSpec

Shadow color. Supports ColorRole, hex string, RGBA tuple, or a (ColorRole, alpha) pair where alpha is 0.0-1.0.

required
blur_radius float

CSS blur-radius, in pixels. A value of 0.0 means a hard-edged shadow.

0.0
offset Tuple[float, float]

(dx, dy) translation of the shadow relative to the widget.

(0.0, 0.0)
spread_radius float

CSS spread-radius, in pixels: outward inflation of the shadow rect, applied before the blur. Corner radii grow by the same amount. A negative value shrinks the rect.

0.0

is_visible property

is_visible: bool

Return True when the shadow will produce any visible output.

Navigator

Navigator(screen: Route | Widget | None = None, *, layer_composer: NavigationLayerComposer | None = None, key: str | None = None)

Bases: ComposableWidget

A minimal navigation stack.

Initialization forms
  • Navigator(screen): start with a single screen (Route or Widget).
  • Navigator.routes([...]): pre-populated stack (e.g. deep linking).
  • Navigator.intents(initial_route=..., routes={...}): Intent-based routing.
Features
  • push/pop
  • of(context) / of(context, root=True)
  • optional fade-in on push

Initialize a Navigator with a single initial screen.

Parameters:

Name Type Description Default
screen Route | Widget | None

The initial screen as a Route or Widget. If None, the navigator starts with an empty stack (use :meth:routes or :meth:intents factories for alternative initialization).

None
layer_composer NavigationLayerComposer | None

Optional custom layer composer.

None
key str | None

Stable widget identity for dev-bridge targeting and hot reload.

None
Source code in src/nuiitivet/navigation/navigator.py
def __init__(
    self,
    screen: Route | Widget | None = None,
    *,
    layer_composer: NavigationLayerComposer | None = None,
    key: str | None = None,
) -> None:
    """Initialize a Navigator with a single initial screen.

    Args:
        screen: The initial screen as a ``Route`` or ``Widget``. If ``None``,
            the navigator starts with an empty stack (use :meth:`routes` or
            :meth:`intents` factories for alternative initialization).
        layer_composer: Optional custom layer composer.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(key=key)
    self._intent_routes: Mapping[type[Any], Callable[[Any], Route | Widget]] = {}
    self._transition: _NavTransition | None = None
    self._transition_handle: TransitionHandle | None = None
    self._transition_engine = TransitionEngine()
    self._pending_pop_requests: int = 0
    # Back requests that have been made but have not finished. Distinct from
    # ``_pending_pop_requests``, which only counts pops queued *behind* a
    # running transition and is incremented inside ``request_back()`` -- a
    # coroutine, so it is still 0 for the whole window between ``pop()``
    # returning and the spawned task getting its turn. This one is
    # incremented synchronously, so ``in_transition`` covers that window.
    self._back_requests_in_flight: int = 0
    self._exiting_route: Route | None = None
    self._layer_composer: NavigationLayerComposer = layer_composer or _DefaultNavigationLayerComposer()
    # Ordered restore descriptors for routes added via ``push`` (not the
    # initial construction stack, which a hot reload rebuilds from the
    # factory). One entry per pushed route: a ``_PushDescriptor`` for a
    # declarative (intent) push, or ``None`` for an opaque, non-restorable
    # push (a raw ``Route``/``Widget`` instance). See :meth:`snapshot_stack`.
    self._restore_log: list[_PushDescriptor | None] = []

    initial_routes: list[Route] = []
    if screen is not None:
        initial_routes.append(self._to_initial_route(screen))
    self._stack = RouteStackRuntime(initial_routes=initial_routes)

stack property

stack: tuple[Route, ...]

The route stack, bottom to top.

A route that is being animated out is still here, and stays until its exit transition finalizes and the route is disposed. Filtering it out would report a pop as done while the outgoing screen is still mounted, still laid out and still painted — a caller waiting on the depth would go through on a transition that has not happened. So this reports what the stack runtime holds, and code that wants "the pop finished" waits for it::

navigator.pop()
await app.wait_for(lambda: len(navigator.stack) == 1)

Reading this never builds a widget: the routes are handed out as they are, and Route.build_widget() constructs on demand. Asking what is on the stack must not change what is on screen.

Not to be confused with :meth:snapshot_stack, which is the hot-reload restore log.

in_transition property

in_transition: bool

Whether a navigation is in flight — not merely whether one animates.

True from the moment a back navigation is requested, through the spawned task, through any push or pop transition, until the stack has settled. The wider definition is the load-bearing part: :meth:pop runs the pop as a task, so on the narrow "is a transition object alive" reading this would be False for the whole window between pop() returning and the task starting, and await wait_for(lambda: not nav.in_transition) would go through immediately, having waited for nothing.

Prefer waiting on what actually changed — :attr:stack, or the screen on top — and reach for this when the depth is not what moved.

routes classmethod

routes(screens: Sequence[Route | Widget], *, layer_composer: NavigationLayerComposer | None = None) -> Navigator

Create a Navigator with a pre-populated stack.

Use this when the navigator should start with multiple screens already on the stack (e.g. deep linking, state restoration).

Parameters:

Name Type Description Default
screens Sequence[Route | Widget]

Sequence of Route or Widget instances. The last item becomes the top of the stack.

required
layer_composer NavigationLayerComposer | None

Optional custom layer composer.

None
Source code in src/nuiitivet/navigation/navigator.py
@classmethod
def routes(
    cls,
    screens: Sequence[Route | Widget],
    *,
    layer_composer: NavigationLayerComposer | None = None,
) -> Navigator:
    """Create a Navigator with a pre-populated stack.

    Use this when the navigator should start with multiple screens already
    on the stack (e.g. deep linking, state restoration).

    Args:
        screens: Sequence of ``Route`` or ``Widget`` instances. The last item
            becomes the top of the stack.
        layer_composer: Optional custom layer composer.
    """
    if not screens:
        raise ValueError("Navigator.routes(...) requires at least one screen")
    instance = cls(layer_composer=layer_composer)
    initial_routes = [instance._to_initial_route(s) for s in screens]
    instance._stack = RouteStackRuntime(initial_routes=initial_routes)
    return instance

intents classmethod

intents(*, initial_route: Any, routes: Mapping[type[Any], Callable[[Any], Route | Widget]], layer_composer: NavigationLayerComposer | None = None) -> Navigator

Create a Navigator configured for Intent-based routing.

Parameters:

Name Type Description Default
initial_route Any

The initial Intent instance used to resolve the first route.

required
routes Mapping[type[Any], Callable[[Any], Route | Widget]]

Mapping of Intent types to route builder functions. Each builder returns a Route or Widget.

required
layer_composer NavigationLayerComposer | None

Optional custom layer composer.

None
Source code in src/nuiitivet/navigation/navigator.py
@classmethod
def intents(
    cls,
    *,
    initial_route: Any,
    routes: Mapping[type[Any], Callable[[Any], Route | Widget]],
    layer_composer: NavigationLayerComposer | None = None,
) -> Navigator:
    """Create a Navigator configured for Intent-based routing.

    Args:
        initial_route: The initial Intent instance used to resolve the first route.
        routes: Mapping of Intent types to route builder functions. Each
            builder returns a ``Route`` or ``Widget``.
        layer_composer: Optional custom layer composer.
    """
    instance = cls(layer_composer=layer_composer)
    instance._intent_routes = dict(routes)
    initial = instance._resolve_intent_to_route(initial_route)
    instance._stack = RouteStackRuntime(initial_routes=[initial])
    return instance

of classmethod

of(context: Widget, root: bool = False) -> NavigatorT

Return the Navigator that navigation from context should drive.

The nearest ancestor wins, so a nested navigator keeps its own history. With no ancestor the answer is the App's own navigator, which makes this the single entry point for both the nested and the top-level case.

Parameters:

Name Type Description Default
context Widget

A widget in the subtree from which to resolve.

required
root bool

Skip the ancestor search and return the App's navigator, to drive a whole-window transition from inside a nested navigator.

False

Raises:

Type Description
RuntimeError

If called before context is mounted (typically from __init__), or if no navigator can be resolved at all.

Source code in src/nuiitivet/navigation/navigator.py
@classmethod
def of(cls: type[NavigatorT], context: Widget, root: bool = False) -> NavigatorT:
    """Return the ``Navigator`` that navigation from ``context`` should drive.

    The nearest ancestor wins, so a nested navigator keeps its own history.
    With no ancestor the answer is the App's own navigator, which makes this
    the single entry point for both the nested and the top-level case.

    Args:
        context: A widget in the subtree from which to resolve.
        root: Skip the ancestor search and return the App's navigator, to
            drive a whole-window transition from inside a nested navigator.

    Raises:
        RuntimeError: If called before ``context`` is mounted (typically from
            ``__init__``), or if no navigator can be resolved at all.
    """
    if not root:
        navigator = find_provider(context, cls)
        if navigator is not None:
            return navigator

    window = find_window(context)
    window_navigator = window._navigator if window is not None else None
    if window_navigator is None:
        raise_if_premature_lookup(f"{cls.__name__}.of", context)
        raise RuntimeError(
            f"No {cls.__name__} found for {context.__class__.__name__}: it has no "
            f"{cls.__name__} ancestor and is not attached to a Window."
        )
    if not isinstance(window_navigator, cls):
        raise RuntimeError(
            f"The Window's navigator is a {type(window_navigator).__name__}, not a {cls.__name__}. "
            f"Pass a {cls.__name__} as the window's content, or nest one in the subtree."
        )
    return window_navigator

snapshot_stack

snapshot_stack() -> list[_PushDescriptor | None]

Capture the restorable descriptors of routes pushed onto this navigator.

Returns an ordered list, one entry per route added via :meth:push (bottom to top): a :class:_PushDescriptor for a declarative (intent) push, or None for an opaque, non-restorable push. Routes from the initial construction stack are excluded — a hot reload rebuilds those from the factory. Pair with :meth:restore_stack across a reload.

This is not the route stack. It is the restore log: one entry per declarative push, None for a raw widget push, and nothing at all for the routes the navigator was constructed with. For the stack, use :attr:stack.

Source code in src/nuiitivet/navigation/navigator.py
def snapshot_stack(self) -> list[_PushDescriptor | None]:
    """Capture the restorable descriptors of routes pushed onto this navigator.

    Returns an ordered list, one entry per route added via :meth:`push`
    (bottom to top): a :class:`_PushDescriptor` for a declarative (intent)
    push, or ``None`` for an opaque, non-restorable push. Routes from the
    initial construction stack are excluded — a hot reload rebuilds those
    from the factory. Pair with :meth:`restore_stack` across a reload.

    **This is not the route stack.** It is the restore log: one entry per
    *declarative* push, ``None`` for a raw widget push, and nothing at all
    for the routes the navigator was constructed with. For the stack, use
    :attr:`stack`.
    """
    return list(self._restore_log)

restore_stack

restore_stack(descriptors: Sequence[_PushDescriptor | None]) -> int

Replay pushed routes from descriptors onto the freshly built navigator.

Each restorable descriptor is resolved against the current route table (by intent qualified name) and pushed without animation, rebuilding the stack the author had before a reload. Replay stops at the first entry that cannot be restored — an opaque (None) push or an intent whose route is no longer registered — leaving the remainder collapsed, the documented degradation analogous to unmatched Observable paths.

Parameters:

Name Type Description Default
descriptors Sequence[_PushDescriptor | None]

The list returned by :meth:snapshot_stack before the reload rebuilt the tree.

required

Returns:

Type Description
int

The number of routes restored (pushed) onto the stack.

Source code in src/nuiitivet/navigation/navigator.py
def restore_stack(self, descriptors: Sequence[_PushDescriptor | None]) -> int:
    """Replay pushed routes from descriptors onto the freshly built navigator.

    Each restorable descriptor is resolved against the current route table
    (by intent qualified name) and pushed without animation, rebuilding the
    stack the author had before a reload. Replay stops at the first entry
    that cannot be restored — an opaque (``None``) push or an intent whose
    route is no longer registered — leaving the remainder collapsed, the
    documented degradation analogous to unmatched ``Observable`` paths.

    Args:
        descriptors: The list returned by :meth:`snapshot_stack` before the
            reload rebuilt the tree.

    Returns:
        The number of routes restored (pushed) onto the stack.
    """
    restored = 0
    for descriptor in descriptors:
        if descriptor is None:
            break
        route = self._resolve_descriptor_to_route(descriptor)
        if route is None:
            break
        self._restore_log.append(descriptor)
        self._stack.push(route)
        self._stack.mark_active(route)
        self._route_widget(route)
        restored += 1
    if restored:
        self.mark_needs_layout()
        self.invalidate()
    return restored

pop

pop() -> None

Request a back navigation. The pop itself runs as a task.

Source code in src/nuiitivet/navigation/navigator.py
def pop(self) -> None:
    """Request a back navigation. The pop itself runs as a task."""
    # Counted here rather than in the coroutine: this call returns before the
    # task has run a single line, and ``in_transition`` has to be true for
    # that window too. See ``_back_requests_in_flight``.
    self._back_requests_in_flight += 1
    scheduled = False
    try:
        task = spawn_task(self._tracked_request_back(), owner_name=f"{type(self).__name__}.pop")
        scheduled = task is not None
    finally:
        # With no running loop ``spawn_task`` closes the coroutine and either
        # raises or returns None. A coroutine closed before it ever started
        # runs no ``finally``, so the release has to happen here or
        # ``in_transition`` would stay true for the rest of the process.
        if not scheduled:
            self._back_requests_in_flight -= 1

request_back async

request_back() -> bool

Request a single back action.

This API is designed for user back inputs (Esc/back button). If a pop transition is already running, the request is queued and the current transition is completed immediately.

Queue consumption policy: - Intermediate queued pops are performed without animation. - The last queued pop (if any) uses the normal pop behavior.

Source code in src/nuiitivet/navigation/navigator.py
async def request_back(self) -> bool:
    """Request a single back action.

    This API is designed for user back inputs (Esc/back button).
    If a pop transition is already running, the request is queued and the
    current transition is completed immediately.

    Queue consumption policy:
    - Intermediate queued pops are performed without animation.
    - The last queued pop (if any) uses the normal pop behavior.
    """
    self._back_requests_in_flight += 1
    return await self._tracked_request_back()

focus_traversal_children

focus_traversal_children() -> list[Widget]

Return only the top route, so Tab never reaches a covered one.

Every route stays mounted — that is how a screen keeps its state while another one sits on top of it — and only the top one is painted. The Tab sequence has to stop at the same boundary.

Source code in src/nuiitivet/navigation/navigator.py
def focus_traversal_children(self) -> list[Widget]:
    """Return only the top route, so Tab never reaches a covered one.

    Every route stays mounted — that is how a screen keeps its state while
    another one sits on top of it — and only the top one is painted. The Tab
    sequence has to stop at the same boundary.
    """
    routes = self._stack.routes
    if not routes:
        return []
    try:
        return [self._route_widget(routes[-1])]
    except Exception:
        exception_once(_logger, "navigator_focus_traversal_children_exc", "Top route widget build failed")
        return []

NavigatorProtocol

Bases: Protocol

The navigation surface a ViewModel depends on.

Annotate an injected navigator with this protocol so the ViewModel stays independent of the widget tree::

class CartViewModel:
    def __init__(self, navigator: NavigatorProtocol) -> None:
        self._navigator = navigator

    def checkout(self) -> None:
        self._navigator.push(OrderCompleteIntent())

:class:~nuiitivet.navigation.navigator.Navigator and :class:~nuiitivet.material.navigator.MaterialNavigator satisfy it structurally, and a hand-written fake needs only these three methods -- no widget tree and no App.

push

push(route_or_widget_or_intent: Route | Widget | Any) -> None

Push a route, a widget, or an intent onto the navigation stack.

Source code in src/nuiitivet/navigation/protocols.py
def push(self, route_or_widget_or_intent: Route | Widget | Any) -> None:
    """Push a route, a widget, or an intent onto the navigation stack."""
    ...

pop

pop() -> None

Pop the topmost route.

Source code in src/nuiitivet/navigation/protocols.py
def pop(self) -> None:
    """Pop the topmost route."""
    ...

can_pop

can_pop() -> bool

Return whether a route below the topmost one exists.

Source code in src/nuiitivet/navigation/protocols.py
def can_pop(self) -> bool:
    """Return whether a route below the topmost one exists."""
    ...

Route dataclass

Route(builder: Callable[[], Widget], transition_spec: TransitionSpec = (lambda: empty())(), _widget: Widget | None = None)

A unit of navigation.

Notes

This is intentionally minimal for Phase 3.

OverlayAware

Bases: Generic[T]

Mixin that lets a widget receive its own :class:OverlayHandle.

When a widget inheriting OverlayAware is displayed through any :class:Overlay show API (show, dialog, side_sheet, bottom_sheet, snackbar, loading), the framework injects the created handle into the widget instance before mounting. The widget (or its ViewModel) can then close itself via self.overlay_handle.close(value) without requiring the caller to wire the handle manually.

Type parameter T represents the result type returned from handle.close(value) / await handle. Note that the return type of the Overlay show APIs stays OverlayHandle[Any]; only the widget-side view is typed.

Example

class MyDialog(ComposableWidget, OverlayAware[str]): ... def on_save(self) -> None: ... self.overlay_handle.close("saved") ... result = await overlay.dialog(MyDialog())

overlay_handle property

overlay_handle: OverlayHandle[T]

Return the handle injected by the overlay framework.

Raises:

Type Description
RuntimeError

If accessed before the widget is displayed via an Overlay show API.

OverlayProtocol

Bases: Protocol

The core overlay surface a ViewModel depends on.

Core :class:~nuiitivet.overlay.overlay.Overlay offers no scenario-specific presentation APIs -- dialog, snackbar, and the sheets live on :class:~nuiitivet.material.overlay.MaterialOverlay. A ViewModel that only dismisses overlays can depend on this protocol; one that presents them should use nuiitivet.material.OverlayProtocol (:class:~nuiitivet.material.protocols.MaterialOverlayProtocol), which extends this one.

close

close(value: Any = None, target: Widget | Route | None = None) -> None

Close an overlay entry, optionally with a result value.

Parameters:

Name Type Description Default
value Any

Result delivered to the awaiting caller.

None
target Widget | Route | None

Entry to close, identified by its route or a widget inside it. Defaults to the topmost entry.

None
Source code in src/nuiitivet/overlay/protocols.py
def close(self, value: Any = None, target: Widget | Route | None = None) -> None:
    """Close an overlay entry, optionally with a result value.

    Args:
        value: Result delivered to the awaiting caller.
        target: Entry to close, identified by its route or a widget inside
            it. Defaults to the topmost entry.
    """
    ...

CancelToken

CancelToken()

Tells a run whether its result is still wanted.

One token per run, created and set by the operator; the run only reads it. That is the whole difference from the hand-rolled threading.Event this replaces: there is no clear() to misuse and no way to mistake another run's flag for your own.

Checking is optional and cooperative -- a superseded run's result is discarded whether or not it ever looks. Python cannot interrupt a thread from outside, so this only helps work with a seam to check at::

def load(query: str, cancel: CancelToken) -> list[Row]:
    rows: list[Row] = []
    for page in range(PAGE_COUNT):
        if cancel.superseded:
            return []          # stop paying for an answer nobody wants
        rows += fetch(query, page)
    return rows

A single blocking call has no such seam, and gains nothing from the token.

Source code in src/nuiitivet/observable/switched.py
def __init__(self) -> None:
    # An Event rather than a bool: set on the UI thread, read on the worker.
    self._flag = threading.Event()

superseded property

superseded: bool

Whether a newer run has started, making this run's result unwanted.

Clock

Bases: Protocol

Clock API compatible with pyglet.clock.

Install an implementation with :func:set_clock to control when scheduled callbacks run — a test that drives a cross-thread observable write or a debounced observable needs this, since the default fallback clock fires on a background thread at wall-clock time.

Implementations must identify scheduled callbacks by equality, the rule pyglet.clock uses: unschedule(obj.method) has to cancel a timer armed with obj.method, even though each attribute access produces a distinct bound-method object. Comparing by id() instead silently leaks timers.

Observable

Observable(default: T, *, compare: Optional[CompareFunc[T]] = None, dispatch: bool = True)

Bases: _ObservableValue[T]

Descriptor for a per-instance observable that can also be used standalone.

Writes from a thread other than the UI thread are marshalled onto it and coalesced, so a subscriber may safely touch widgets whichever thread set the value. Pass dispatch=False for an observable no widget will ever bind to: notification then stays synchronous on the writing thread, and every intermediate value is delivered rather than only the latest per tick.

Source code in src/nuiitivet/observable/value.py
def __init__(
    self,
    default: T,
    *,
    compare: Optional[CompareFunc[T]] = None,
    dispatch: bool = True,
):
    super().__init__(initial=default, owner=None, name=None, compare=compare, dispatch=dispatch)
    self.default = default
    self.name: Optional[str] = None
    self.compare = compare
    self.dispatch = dispatch

Clocks

The runtime's installed clock (read and replace).

get staticmethod

get() -> Clock

Return the clock currently installed.

Read the clock through this call every time instead of keeping a reference: the backend installs its own clock during App.run(), so a saved reference goes stale. Save and restore around a test with this.

Source code in src/nuiitivet/observable/clocks.py
@staticmethod
def get() -> Clock:
    """Return the clock currently installed.

    Read the clock through this call every time instead of keeping a
    reference: the backend installs its own clock during ``App.run()``,
    so a saved reference goes stale. Save and restore around a test with
    this.
    """
    return get_clock()

set staticmethod

set(new_clock: Clock) -> None

Install new_clock as the clock every scheduled callback runs on.

Source code in src/nuiitivet/observable/clocks.py
@staticmethod
def set(new_clock: Clock) -> None:
    """Install ``new_clock`` as the clock every scheduled callback runs on."""
    set_clock(new_clock)

FileDropEvent dataclass

FileDropEvent(paths: tuple[Path, ...], x: float, y: float, local_x: Optional[float] = None, local_y: Optional[float] = None)

OS file paths dropped onto the window, delivered to a widget.

x / y are window coordinates of the drop point (top-left origin, logical pixels); local_x / local_y are relative to the receiving widget's top-left and are populated at delivery time.

PointerEvent dataclass

PointerEvent(id: int, type: PointerEventType, x: float, y: float, pointer_type: PointerType = UNKNOWN, dx: float = 0.0, dy: float = 0.0, scroll_x: float = 0.0, scroll_y: float = 0.0, button: Optional[int] = None, buttons: int = 0, timestamp: float = 0.0, is_primary: bool = True, modifier_keys: int = 0, local_x: float = 0.0, local_y: float = 0.0)

Immutable pointer event payload delivered to widgets.

Button semantics

button is the single button that caused this event — set on PRESS and RELEASE (and carried through the synthesized CANCEL) to a backend-neutral BUTTON_* code, and None otherwise (MOVE/HOVER/ENTER/LEAVE/SCROLL, or a synthetic/non-mouse event that carries no button).

buttons is a bit mask of the buttons currently held down, using the same BUTTON_* codes OR-ed together. It is populated on MOVE (drag) events so a consumer can tell a right-drag from a left-drag; it is 0 when no button is held.

Coordinate semantics

x / y are screen (window) coordinates. local_x / local_y are relative to the top-left of the widget that receives the event and are populated at dispatch time; on an event that has not been routed to a widget they default to the screen coordinates' offset of 0.0.

PointerEventType

Bases: str, Enum

Pointer event actions.

Shortcut dataclass

Shortcut(key: str, modifiers: int = 0)

A key gesture: one key plus a mask of the modifiers held with it.

Parameters:

Name Type Description Default
key str

The key name, normalized via :func:normalize_key_name (e.g. "s", "enter", "f1").

required
modifiers int

A bitmask of MOD_* values, which may include MOD_ACCEL — the logical primary modifier, resolved to Cmd on macOS and Ctrl elsewhere when the gesture is matched.

0

display property

display: str

Human-readable accelerator label for this gesture, per platform.

macOS uses the compact symbol form in Apple's canonical modifier order (⌃⌥⇧⌘S); other platforms use the Ctrl+Shift+S form. MOD_ACCEL renders as its resolved physical modifier (⌘ on macOS, Ctrl elsewhere), so one :class:Shortcut yields the right label everywhere — this is what menu items show next to their command.

conflicts_with_text_input property

conflicts_with_text_input: bool

Return True if this gesture is one a focused text field would type.

Such a gesture does not fire while a text field holds focus — the field turns it into a character instead. Alt gestures count as conflicting on every platform, so they are unavailable there too; see :func:produces_text for why that cannot be narrowed down safely.

parse classmethod

parse(spec: str) -> 'Shortcut'

Parse a "Accel+Shift+S"-style spec into a :class:Shortcut.

Modifier names are case-insensitive and accept the usual spellings: Accel/Primary, Ctrl/Control, Alt/Option, Meta/Cmd/Command/Super/Win, Shift. The last token is the key. A literal + is written as the final token ("Accel++").

Raises:

Type Description
ValueError

If the spec is empty, names an unknown modifier, or carries no key.

Source code in src/nuiitivet/input/shortcut.py
@classmethod
def parse(cls, spec: str) -> "Shortcut":
    """Parse a ``"Accel+Shift+S"``-style spec into a :class:`Shortcut`.

    Modifier names are case-insensitive and accept the usual spellings:
    ``Accel``/``Primary``, ``Ctrl``/``Control``, ``Alt``/``Option``,
    ``Meta``/``Cmd``/``Command``/``Super``/``Win``, ``Shift``. The last
    token is the key. A literal ``+`` is written as the final token
    (``"Accel++"``).

    Raises:
        ValueError: If the spec is empty, names an unknown modifier, or
            carries no key.
    """
    tokens = [t.strip() for t in spec.split("+")]
    if tokens and tokens[-1] == "" and len(tokens) > 1:
        # Trailing empty token means the key itself is "+" ("Accel++").
        tokens = tokens[:-2] + ["+"]

    if not tokens or not tokens[-1]:
        raise ValueError(f"Shortcut spec has no key: {spec!r}")

    modifiers = 0
    for token in tokens[:-1]:
        mod = _MODIFIER_NAMES.get(token.lower())
        if mod is None:
            raise ValueError(f"Unknown modifier {token!r} in shortcut spec {spec!r}")
        modifiers |= mod

    return cls(key=tokens[-1], modifiers=modifiers)

matches

matches(key: str, modifier_keys: int) -> bool

Return True if a key press of key with modifier_keys is this gesture.

MOD_ACCEL is resolved to the platform's physical modifier here, and the modifier mask must match exactly — Accel+S does not fire on Accel+Shift+S.

Source code in src/nuiitivet/input/shortcut.py
def matches(self, key: str, modifier_keys: int) -> bool:
    """Return True if a key press of ``key`` with ``modifier_keys`` is this gesture.

    ``MOD_ACCEL`` is resolved to the platform's physical modifier here, and
    the modifier mask must match exactly — ``Accel+S`` does not fire on
    ``Accel+Shift+S``.
    """
    return normalize_key_name(key) == self.key and modifier_keys == resolve_modifiers(self.modifiers)

ShortcutBinding dataclass

ShortcutBinding(shortcut: Shortcut, on_trigger: VoidCallback, scope: ShortcutScope = FOREGROUND)

A gesture bound to the callback it triggers, and the scope it is live in.

This is the unit the shortcut dispatch tier stores, kept as a type rather than a bare callable so richer command semantics (can_execute, menu binding) can be added without changing every call site.

Parameters:

Name Type Description Default
shortcut Shortcut

The gesture that triggers the binding.

required
on_trigger VoidCallback

Called with no arguments when the gesture fires. May be sync or async.

required
scope ShortcutScope

When the binding is live. Defaults to :attr:ShortcutScope.FOREGROUND.

FOREGROUND

ShortcutScope

Bases: Enum

When a :class:ShortcutBinding is live.

The members widen: each is a superset of the one before it.

Attributes:

Name Type Description
FOCUS

Live only while the subtree contains the focused node. Needed only when the same command has several targets on screen at once (a dual-pane file manager, a split-view editor), so nothing but focus can decide which one acts.

FOREGROUND

Live while the subtree is on the topmost interactable layer — not hidden, not on a covered route, not behind a blocking overlay. The default, and the right answer for almost everything.

MOUNT

Live while the subtree is in the widget tree at all, displayed or not. How an app-wide command is expressed: bind it on the content root, which stays mounted across navigation.

FocusSource

Bases: str, Enum

Indicates how a :class:FocusNode acquired focus.

Attributes:

Name Type Description
KEYBOARD

Focus acquired via keyboard navigation (Tab / Shift-Tab).

POINTER

Focus acquired via a pointer interaction (click-to-focus).

Theme dataclass

Theme(mode: str, extensions: List[ThemeExtension] = list(), name: str = '')

Theme container holding design system extensions.

The Theme class itself is design-agnostic. It holds a list of ThemeExtension objects (like MaterialThemeData, CupertinoThemeData) that define the actual look and feel.

extension

extension(type: Type[T]) -> T | None

Get an extension by type.

Source code in src/nuiitivet/theme/theme.py
def extension(self, type: Type[T]) -> T | None:
    """Get an extension by type."""
    for ext in self.extensions:
        if isinstance(ext, type):
            return ext
    return None

of staticmethod

of(context: Any) -> 'Theme'

Return the current :class:Theme from the nearest :class:AppScope.

Reading registers a dependency. The reader is recorded, and a theme change invalidates it: a composable is rebuilt, a leaf is re-measured and repainted. Nothing subscribes and nothing has to unsubscribe.

Read where the value is consumed. A widget with a build() reads there; a leaf has no build(), so it reads in paint() or preferred_size(). Never read in __init__ or on_mount: what is resolved at mount and kept on a field is never corrected again.

A detached context — a widget deliberately measured outside an App, as tests and offscreen sizing do — resolves no AppScope and quietly falls back to the light default rather than raising. Paint code runs this on every frame, so a raising lookup there would turn a cosmetic problem into a crash. Under a pull that fallback is self-correcting: the next read, once the widget is attached, resolves the real theme.

Parameters:

Name Type Description Default
context Any

A widget in the subtree from which to search upward.

required

Returns:

Type Description
'Theme'

The app's current theme, or Theme(mode="light") when no

'Theme'

AppScope is reachable.

Raises:

Type Description
RuntimeError

If context has not run Widget.__init__ yet, so it has no parent link to resolve against and no identity to attribute a dependency to. Reading in __init__ after super().__init__() is indistinguishable at runtime from measuring an unattached widget, so it is not rejected here; what makes it a bug is keeping the result, which the "read, never hold" rule forbids.

Source code in src/nuiitivet/theme/theme.py
@staticmethod
def of(context: Any) -> "Theme":
    """Return the current :class:`Theme` from the nearest :class:`AppScope`.

    **Reading registers a dependency.** The reader is recorded, and a theme
    change invalidates it: a composable is rebuilt, a leaf is re-measured and
    repainted. Nothing subscribes and nothing has to unsubscribe.

    Read where the value is consumed. A widget with a ``build()`` reads
    there; a leaf has no ``build()``, so it reads in ``paint()`` or
    ``preferred_size()``. Never read in ``__init__`` or ``on_mount``: what is
    resolved at mount and kept on a field is never corrected again.

    A detached context — a widget deliberately measured outside an App, as
    tests and offscreen sizing do — resolves no ``AppScope`` and quietly
    falls back to the light default rather than raising. Paint code runs this
    on every frame, so a raising lookup there would turn a cosmetic problem
    into a crash. Under a pull that fallback is self-correcting: the next
    read, once the widget is attached, resolves the real theme.

    Args:
        context: A widget in the subtree from which to search upward.

    Returns:
        The app's current theme, or ``Theme(mode="light")`` when no
        ``AppScope`` is reachable.

    Raises:
        RuntimeError: If ``context`` has not run ``Widget.__init__`` yet, so
            it has no parent link to resolve against and no identity to
            attribute a dependency to. Reading in ``__init__`` *after*
            ``super().__init__()`` is indistinguishable at runtime from
            measuring an unattached widget, so it is not rejected here;
            what makes it a bug is keeping the result, which the "read,
            never hold" rule forbids.
    """
    scope = find_app_scope(context)
    if scope is None:
        if is_uninitialized_context(context):
            raise RuntimeError(
                f"Theme.of() was called on {type(context).__name__} before super().__init__() "
                f"had run, so it has no parent link yet and cannot resolve a theme. "
                f"Read the theme where its value is used: in build() if the widget has one, "
                f"otherwise in paint() or preferred_size()."
            )
        return Theme(mode="light", extensions=[])
    register_theme_dependency(context)
    return scope.theme_manager.current

TypeScale

The 15 baseline Material Design 3 type-scale roles.

Values follow the MD3 2021 baseline type scale (https://m3.material.io/styles/typography/type-scale-tokens): (font_size, line_height, weight, tracking).

TypeScaleToken dataclass

TypeScaleToken(font_size: float, line_height: float, weight: int = 400, tracking: float = 0.0)

Immutable typographic metrics for a single type-scale role.

Attributes:

Name Type Description
font_size float

Glyph size in px.

line_height float

Absolute line height in px (faithful to MD3 tokens, not a multiplier). Only affects multi-line layout.

weight int

Font weight (100-900); MD3 roles use 400 (Regular) or 500 (Medium).

tracking float

Letter spacing in px; may be negative (e.g. Display Large).

copy_with

copy_with(**changes: Any) -> 'TypeScaleToken'

Return a new token with the given fields replaced.

Example

TypeScale.TITLE_MEDIUM.copy_with(weight=700)

Source code in src/nuiitivet/theme/type_scale.py
def copy_with(self, **changes: Any) -> "TypeScaleToken":
    """Return a new token with the given fields replaced.

    Example:
        TypeScale.TITLE_MEDIUM.copy_with(weight=700)
    """
    return replace(self, **changes)

from_size classmethod

from_size(font_size: float, *, line_height: float | None = None, weight: int = 400, tracking: float = 0.0) -> 'TypeScaleToken'

Build a token from a raw size when no semantic role applies.

Intended for config-driven numeric sizes (e.g. a widget's *Style exposing label_font_size), not for public typography. line_height defaults to font_size * 1.25 to preserve historical spacing.

Source code in src/nuiitivet/theme/type_scale.py
@classmethod
def from_size(
    cls,
    font_size: float,
    *,
    line_height: float | None = None,
    weight: int = 400,
    tracking: float = 0.0,
) -> "TypeScaleToken":
    """Build a token from a raw size when no semantic role applies.

    Intended for config-driven numeric sizes (e.g. a widget's ``*Style``
    exposing ``label_font_size``), not for public typography. ``line_height``
    defaults to ``font_size * 1.25`` to preserve historical spacing.
    """
    lh = line_height if line_height is not None else float(font_size) * _DEFAULT_LINE_HEIGHT_RATIO
    return cls(font_size=float(font_size), line_height=lh, weight=weight, tracking=tracking)

ThemeManager

ThemeManager(initial: Optional[Theme] = None)

Holds the current Theme and notifies its owner on changes.

Source code in src/nuiitivet/theme/manager.py
def __init__(
    self,
    initial: Optional[Theme] = None,
) -> None:
    self._lock = threading.RLock()
    self._current = initial if initial is not None else Theme(mode="light", extensions=[])
    self._generation = 0
    #: Set by the owning provider. Not a subscriber list: exactly one owner.
    self.on_change: Optional[Callable[[Theme], None]] = None

generation property

generation: int

Count of theme changes so far.

Bumped before :attr:on_change runs, so anything deriving a value from the theme can tell whether what it holds is still current.

set_theme

set_theme(theme: Theme) -> None

Replace the current theme and notify the owner.

Parameters:

Name Type Description Default
theme Theme

The theme to make current.

required
Source code in src/nuiitivet/theme/manager.py
def set_theme(self, theme: Theme) -> None:
    """Replace the current theme and notify the owner.

    Args:
        theme: The theme to make current.
    """
    with self._lock:
        self._current = theme
        self._generation += 1
        handler = self.on_change
    if handler is None:
        return
    try:
        handler(theme)
    except Exception:
        logger.exception("ThemeManager.on_change handler raised")

ThemeExtension

Bases: Protocol

Protocol for theme extensions (design system specific data).

copy_with

copy_with(**kwargs: Any) -> 'ThemeExtension'

Create a copy of this extension with the given fields replaced.

Source code in src/nuiitivet/theme/types.py
def copy_with(self, **kwargs: Any) -> "ThemeExtension":
    """Create a copy of this extension with the given fields replaced."""
    ...

Animatable

Animatable(initial_value: float, motion: Optional[Motion] = None)

Bases: ObservableBase[T]

Declarative, retargetable animation value.

Assign to target to retarget motion without reversing. value is read-only and reflects the current animated value.

Subclasses :class:ObservableBase so it is recognised by the pure-C isinstance fast path used on the widget construction hot path; it also structurally satisfies :class:ReadOnlyObservableProtocol.

Initialize float animatable value.

Parameters:

Name Type Description Default
initial_value float

Initial float value.

required
motion Optional[Motion]

Optional motion model. If omitted, target assignment updates immediately.

None
Source code in src/nuiitivet/animation/animatable.py
def __init__(self: "Animatable[float]", initial_value: float, motion: Optional[Motion] = None) -> None:
    """Initialize float animatable value.

    Args:
        initial_value: Initial float value.
        motion: Optional motion model. If omitted, target assignment updates immediately.
    """
    value = float(initial_value)
    converter: VectorConverter[float] = FloatConverter()
    self._initialize(initial_value=value, converter=converter, motion=motion)

vector staticmethod

vector(initial_value: V, converter: VectorConverter[V], motion: Optional[Motion] = None) -> 'Animatable[V]'

Initialize vector-converted animatable value.

Parameters:

Name Type Description Default
initial_value V

Initial value.

required
converter VectorConverter[V]

Value/vector converter.

required
motion Optional[Motion]

Optional motion model. If omitted, target assignment updates immediately.

None
Source code in src/nuiitivet/animation/animatable.py
@staticmethod
def vector(
    initial_value: V,
    converter: VectorConverter[V],
    motion: Optional[Motion] = None,
) -> "Animatable[V]":
    """Initialize vector-converted animatable value.

    Args:
        initial_value: Initial value.
        converter: Value/vector converter.
        motion: Optional motion model. If omitted, target assignment updates immediately.
    """
    instance = cast("Animatable[V]", object.__new__(Animatable))
    instance._initialize(initial_value=initial_value, converter=converter, motion=motion)
    return instance

snap_to

snap_to(value: T) -> None

Immediately set both value and target without animating.

Stops any active motion and resets the resolved value and target to value. Useful for establishing an initial state before the first animated transition.

Parameters:

Name Type Description Default
value T

New current and target value.

required
Source code in src/nuiitivet/animation/animatable.py
def snap_to(self, value: T) -> None:
    """Immediately set both value and target without animating.

    Stops any active motion and resets the resolved value and target to
    ``value``. Useful for establishing an initial state before the first
    animated transition.

    Args:
        value: New current and target value.
    """
    self._stop_ticking()
    self._target = value
    self._value.value = value
    if self._motion is not None:
        vector = self._converter.to_vector(value)
        if self._state is None:
            self._state = self._motion.create_state(vector, vector)
        else:
            self._state.value = vector.copy()
            self._state.start = vector.copy()
            self._state.target = vector.copy()
            self._state.velocity = [0.0 for _ in vector]
            self._state.done = True

set_motion

set_motion(motion: Motion) -> None

Replace the active motion model.

Useful when the same animatable must switch motions depending on direction (e.g. distinct enter/exit timing). Any in-flight motion continues from its current value/velocity using the new motion on the next target assignment.

Parameters:

Name Type Description Default
motion Motion

New motion model to drive subsequent retargets.

required
Source code in src/nuiitivet/animation/animatable.py
def set_motion(self, motion: Motion) -> None:
    """Replace the active motion model.

    Useful when the same animatable must switch motions depending on
    direction (e.g. distinct enter/exit timing). Any in-flight motion
    continues from its current value/velocity using the new motion on the
    next ``target`` assignment.

    Args:
        motion: New motion model to drive subsequent retargets.
    """
    self._motion = motion
    if self._state is None:
        current_vector = self._converter.to_vector(self.value)
        target_vector = self._converter.to_vector(self._target)
        self._state = motion.create_state(current_vector, target_vector)

stop

stop() -> None

Stop any active motion and keep the current value.

Source code in src/nuiitivet/animation/animatable.py
def stop(self) -> None:
    """Stop any active motion and keep the current value."""
    self._stop_ticking()
    self._target = self.value
    if self._state is not None:
        current_vector = self._converter.to_vector(self.value)
        self._state.value = current_vector.copy()
        self._state.start = current_vector.copy()
        self._state.target = current_vector.copy()
        self._state.velocity = [0.0 for _ in current_vector]
        self._state.done = True

Motion

Bases: Protocol

Protocol for declarative motion.

Motion specs are responsible for advancing MotionState over time and handling retargeting without pausing.

LinearMotion

LinearMotion(duration: float)

Linear time-based motion.

Parameters:

Name Type Description Default
duration float

Duration in seconds for a full transition.

required
Source code in src/nuiitivet/animation/motion.py
def __init__(self, duration: float) -> None:
    self.duration = max(0.0, float(duration))

BezierMotion

BezierMotion(x1: float, y1: float, x2: float, y2: float, duration: float)

Bezier time-based motion.

Parameters:

Name Type Description Default
x1 float

Control point 1 x.

required
y1 float

Control point 1 y.

required
x2 float

Control point 2 x.

required
y2 float

Control point 2 y.

required
duration float

Duration in seconds for a full transition.

required
Source code in src/nuiitivet/animation/motion.py
def __init__(self, x1: float, y1: float, x2: float, y2: float, duration: float) -> None:
    self.duration = max(0.0, float(duration))
    self._curve = _CubicBezier(float(x1), float(y1), float(x2), float(y2))

SpringMotion

SpringMotion(stiffness: float, damping: float, mass: float, *, initial_velocity: float | Sequence[float] = 0.0, tolerance: float = 0.001)

Spring-based motion.

The integrator is sub-stepped so stiff springs stay stable when the frame rate (and thus dt) drops: a single large semi-implicit Euler step can diverge once dt approaches 2 / sqrt(stiffness / mass) (e.g. a stiffness of 1400 diverges below ~30 fps). Each step splits dt into fixed-size sub-steps so the result is stable and frame-rate independent.

Parameters:

Name Type Description Default
stiffness float

Spring stiffness constant.

required
damping float

Damping coefficient.

required
mass float

Mass attached to the spring.

required
initial_velocity float | Sequence[float]

Initial velocity in units per second.

0.0
Source code in src/nuiitivet/animation/motion.py
def __init__(
    self,
    stiffness: float,
    damping: float,
    mass: float,
    *,
    initial_velocity: float | Sequence[float] = 0.0,
    tolerance: float = 1e-3,
) -> None:
    self.stiffness = float(stiffness)
    self.damping = float(damping)
    self.mass = max(1e-9, float(mass))
    self.initial_velocity = initial_velocity
    self.tolerance = max(0.0, float(tolerance))

TransitionDefinition dataclass

TransitionDefinition(motion: Motion, pattern: TransitionPattern)

Defines a complete transition with motion and pattern.

FadePattern

FadePattern(start_alpha: float = 0.0, end_alpha: float = 1.0, *, start_progress: float = 0.0, end_progress: float = 1.0)

Controls opacity based on progress.

The optional start_progress / end_progress window confines the fade to a sub-range of the overall progress, holding start_alpha before it and end_alpha after it. This expresses the MD3 "fade through" timing, where the outgoing element fades out over the first part of the transition and the incoming element fades in over the later part, so the two never sit at half opacity simultaneously. The defaults (0.01.0) keep a plain linear fade over the full progress.

Source code in src/nuiitivet/animation/transition_pattern.py
def __init__(
    self,
    start_alpha: float = 0.0,
    end_alpha: float = 1.0,
    *,
    start_progress: float = 0.0,
    end_progress: float = 1.0,
) -> None:
    self.start_alpha = start_alpha
    self.end_alpha = end_alpha
    self.start_progress = start_progress
    self.end_progress = end_progress

SlidePattern

SlidePattern(start_x: float = 0.0, start_y: float = 0.0, end_x: float = 0.0, end_y: float = 0.0)

Controls translation based on progress.

Source code in src/nuiitivet/animation/transition_pattern.py
def __init__(
    self,
    start_x: float = 0.0,
    start_y: float = 0.0,
    end_x: float = 0.0,
    end_y: float = 0.0,
) -> None:
    self.start_x = start_x
    self.start_y = start_y
    self.end_x = end_x
    self.end_y = end_y

ScalePattern

ScalePattern(start_scale_x: float = 0.8, start_scale_y: float = 0.8, end_scale_x: float = 1.0, end_scale_y: float = 1.0)

Controls scale based on progress.

Source code in src/nuiitivet/animation/transition_pattern.py
def __init__(
    self,
    start_scale_x: float = 0.8,
    start_scale_y: float = 0.8,
    end_scale_x: float = 1.0,
    end_scale_y: float = 1.0,
) -> None:
    self.start_scale_x = start_scale_x
    self.start_scale_y = start_scale_y
    self.end_scale_x = end_scale_x
    self.end_scale_y = end_scale_y

FractionalSlidePattern

FractionalSlidePattern(start_x: float = 0.0, start_y: float = 0.0, end_x: float = 0.0, end_y: float = 0.0)

Controls translation as a fraction of the widget's allocated size.

A value of +1.0 means one full width (for x) or height (for y) offset. This allows sheet transitions to slide in/out by exactly their own size, regardless of the actual pixel dimensions.

Source code in src/nuiitivet/animation/transition_pattern.py
def __init__(
    self,
    start_x: float = 0.0,
    start_y: float = 0.0,
    end_x: float = 0.0,
    end_y: float = 0.0,
) -> None:
    self.start_x = start_x
    self.start_y = start_y
    self.end_x = end_x
    self.end_y = end_y

Desktop

Operating-system desktop integration (notifications, ...).

notify staticmethod

notify(title: str, body: str = '') -> None

Raise a desktop notification with title and an optional body.

Fire-and-forget: returns immediately, never raises, and is safe to call from an event handler or a worker thread alike. Failures are logged once per process instead of surfacing — a notification must never take the app down. Delivery is best-effort: the OS may still suppress it (permissions, focus modes) without an error.

Source code in src/nuiitivet/platform/desktop.py
@staticmethod
def notify(title: str, body: str = "") -> None:
    """Raise a desktop notification with ``title`` and an optional ``body``.

    Fire-and-forget: returns immediately, never raises, and is safe to
    call from an event handler or a worker thread alike. Failures are
    logged once per process instead of surfacing — a notification must
    never take the app down. Delivery is best-effort: the OS may still
    suppress it (permissions, focus modes) without an error.
    """
    _notify(title, body)

FileDialog

Native open-file / save-file / open-directory dialogs.

All methods are coroutines: the dialog runs in an OS helper process and is awaited from a worker thread, so the UI keeps painting while it is open. Call them from an async event handler::

class Editor(nv.ComposableWidget):
    async def _open(self) -> None:
        path = await nv.FileDialog.open_file(file_types=["txt", "md"])
        if path is None:
            return  # cancelled
        self.text.value = path.read_text()

Each method returns the selected :class:~pathlib.Path, or None when the user cancelled. :class:FileDialogError is raised when the dialog cannot be shown at all (e.g. no zenity/kdialog on Linux).

open_file async staticmethod

open_file(*, title: Optional[str] = None, initial_dir: Union[Path, str, None] = None, file_types: Optional[Sequence[str]] = None) -> Optional[Path]

Pick an existing file to open.

file_types restricts the picker to extensions given without the leading dot (e.g. ["png", "jpg"]); None allows any file. A leading ~ in initial_dir is expanded to the home directory.

Source code in src/nuiitivet/platform/file_dialog.py
@staticmethod
async def open_file(
    *,
    title: Optional[str] = None,
    initial_dir: Union[Path, str, None] = None,
    file_types: Optional[Sequence[str]] = None,
) -> Optional[Path]:
    """Pick an existing file to open.

    ``file_types`` restricts the picker to extensions given without the
    leading dot (e.g. ``["png", "jpg"]``); ``None`` allows any file.
    A leading ``~`` in ``initial_dir`` is expanded to the home directory.
    """
    return await _run_backend(
        lambda backend: backend.open_file(
            title=title,
            initial_dir=_normalize_dir(initial_dir),
            file_types=file_types,
        )
    )

open_files async staticmethod

open_files(*, title: Optional[str] = None, initial_dir: Union[Path, str, None] = None, file_types: Optional[Sequence[str]] = None) -> list[Path]

Pick one or more existing files to open.

Like :meth:open_file with multiple selection. Cancelling returns an empty list — the dialog cannot return zero selections otherwise.

Source code in src/nuiitivet/platform/file_dialog.py
@staticmethod
async def open_files(
    *,
    title: Optional[str] = None,
    initial_dir: Union[Path, str, None] = None,
    file_types: Optional[Sequence[str]] = None,
) -> list[Path]:
    """Pick one or more existing files to open.

    Like :meth:`open_file` with multiple selection. Cancelling returns an
    empty list — the dialog cannot return zero selections otherwise.
    """
    return await _run_backend(
        lambda backend: backend.open_files(
            title=title,
            initial_dir=_normalize_dir(initial_dir),
            file_types=file_types,
        )
    )

save_file async staticmethod

save_file(*, title: Optional[str] = None, initial_dir: Union[Path, str, None] = None, default_name: Optional[str] = None, file_types: Optional[Sequence[str]] = None) -> Optional[Path]

Pick a destination path to save to.

The returned path may not exist yet; writing the file is the caller's job. The native dialog asks for overwrite confirmation where the platform does so. file_types restricts the saved name's extension where the platform's save dialog supports it; the macOS osascript fallback has no type filter and ignores it.

Source code in src/nuiitivet/platform/file_dialog.py
@staticmethod
async def save_file(
    *,
    title: Optional[str] = None,
    initial_dir: Union[Path, str, None] = None,
    default_name: Optional[str] = None,
    file_types: Optional[Sequence[str]] = None,
) -> Optional[Path]:
    """Pick a destination path to save to.

    The returned path may not exist yet; writing the file is the caller's
    job. The native dialog asks for overwrite confirmation where the
    platform does so. ``file_types`` restricts the saved name's extension
    where the platform's save dialog supports it; the macOS ``osascript``
    fallback has no type filter and ignores it.
    """
    return await _run_backend(
        lambda backend: backend.save_file(
            title=title,
            initial_dir=_normalize_dir(initial_dir),
            default_name=default_name,
            file_types=file_types,
        )
    )

open_directory async staticmethod

open_directory(*, title: Optional[str] = None, initial_dir: Union[Path, str, None] = None) -> Optional[Path]

Pick an existing directory.

Source code in src/nuiitivet/platform/file_dialog.py
@staticmethod
async def open_directory(
    *,
    title: Optional[str] = None,
    initial_dir: Union[Path, str, None] = None,
) -> Optional[Path]:
    """Pick an existing directory."""
    return await _run_backend(
        lambda backend: backend.open_directory(
            title=title,
            initial_dir=_normalize_dir(initial_dir),
        )
    )

FileDialogError

Bases: RuntimeError

A dialog could not be shown (helper missing or helper failed).

Distinct from cancellation: the user cancelling a dialog returns None, never raises.

TrayIcon

TrayIcon(*, icon: Optional[Union[str, Path]] = None, tooltip: ObservableStr = '', menu: Optional[Sequence[MenuEntry]] = None, on_activate: Optional[VoidCallback] = None, dock_visibility: str = 'always')

A system tray icon: image, tooltip, menu, and an activate callback.

Parameters:

Name Type Description Default
icon Optional[Union[str, Path]]

Path to the icon image file (PNG recommended). On macOS a filename stem ending in Template is loaded as a template image, so the system recolors it for light/dark menu bars. Without an icon the tray shows the tooltip text (macOS) or a neutral placeholder — real apps should always ship an icon.

None
tooltip ObservableStr

Hover text; a plain string or an Observable one.

''
menu Optional[Sequence[MenuEntry]]

The tray menu as :class:MenuEntry entries — actions, separators, submenus and checkable items, exactly as in the menu bar. MenuEntry.quit() works (a resident app should include it: while no window is visible the tray menu is the only exit path). Window-scoped standard items (close/minimize/...) have no target window here and are ignored with a warning.

None
on_activate Optional[VoidCallback]

Called when the icon itself is activated the platform's conventional way. Support varies: on macOS only without a menu (a menu owns the click there); on Windows the gesture is a double-click; a Linux AppIndicator host cannot deliver it at all (a pystray limitation). Treat it as an optional shortcut and keep an equivalent entry in menu.

None
dock_visibility str

macOS Dock presence: "always" (default), "auto" (in the Dock only while some window is visible — the close-to-tray convention), or "never" (a pure menu-bar-extra app; the process gets no Dock icon or Cmd+Tab entry). Ignored on Windows/Linux, where the taskbar entry follows window visibility by itself.

'always'
Source code in src/nuiitivet/platform/tray.py
def __init__(
    self,
    *,
    icon: Optional[Union[str, Path]] = None,
    tooltip: ObservableStr = "",
    menu: Optional[Sequence[MenuEntry]] = None,
    on_activate: Optional[VoidCallback] = None,
    dock_visibility: str = "always",
) -> None:
    entries: Tuple[MenuEntry, ...] = tuple(menu) if menu is not None else ()
    for entry in entries:
        if not isinstance(entry, MenuEntry):
            raise TypeError("TrayIcon menu entries must be MenuEntry instances.")
    if dock_visibility not in _DOCK_VISIBILITIES:
        raise ValueError(
            f"dock_visibility must be one of {_DOCK_VISIBILITIES}, got {dock_visibility!r}."
        )
    self._icon_path: Optional[Path] = Path(icon) if icon is not None else None
    self._tooltip = tooltip
    self._menu = entries
    self._on_activate = on_activate
    self._dock_visibility = dock_visibility
    self._installed = Observable(False)
    self._bridge: Any = None
    self._app_ref: Optional["weakref.ref[App]"] = None

icon_path property

icon_path: Optional[Path]

Path to the icon image, or None.

tooltip property

tooltip: ObservableStr

The hover text, as given (plain or Observable).

menu property

menu: Tuple[MenuEntry, ...]

The tray menu entries (empty when no menu was given).

dock_visibility property

dock_visibility: str

The macOS Dock policy: "always", "auto", or "never".

installed property

installed: ObservableBase[bool]

Whether the icon is actually showing in the system tray.

False until the backend installs it, and again after removal or when the platform cannot host one (no pystray, no tray area). Apps adapt through this — e.g. bind a window's close_action to it, or exit when a tray they depend on is unavailable.

AppScope

AppScope(app: 'App', child: Widget, *, key: Optional[str] = None)

Bases: Widget

Inherited widget that provides access to the App instance.

Also the theme provider: Theme.of resolves against the nearest one of these. Theme changes are fanned out by the App itself (which owns the :class:ThemeManager) to every open window; this scope only serves reads.

Source code in src/nuiitivet/runtime/app.py
def __init__(self, app: "App", child: Widget, *, key: Optional[str] = None) -> None:
    super().__init__(key=key)
    self.theme_manager = app._theme_manager
    self._app_ref = weakref.ref(app)
    self.add_child(child)

app property

app: Optional['App']

The App this scope belongs to, or None once it has been collected.

The App-scoped half of X.of(context) resolves through here — see :func:nuiitivet.widgeting.context_lookup.find_app.

ExitPolicy

Bases: Enum

When App.run() returns.

Attributes:

Name Type Description
LAST_WINDOW_CLOSED

The default — the app exits once no window remains open.

MAIN_WINDOW_CLOSED

Closing the main window closes every other window and exits, regardless of what else is open.

EXPLICIT

Only app.exit() exits; the app keeps running with zero open windows, so some window must be reopenable from app-held state.

OSChrome dataclass

OSChrome(variant: OSChromeVariant = 'default')

OS-managed window decoration.

Maps variant directly to pyglet.window.Window.WINDOW_STYLE_*.

Parameters:

Name Type Description Default
variant OSChromeVariant

Window style. One of: "default", "dialog", "tool", "borderless", "transparent".

'default'

CustomChrome dataclass

CustomChrome(header: 'Widget', corner_radius: float = 0.0, border: Optional[Border] = None)

Custom (app-drawn) window decoration.

Always uses a borderless OS window (no OS title bar). Wraps header in :class:~nuiitivet.runtime.title_bar.WindowDragArea so the user can drag the window by the header.

Parameters:

Name Type Description Default
header 'Widget'

Widget rendered as the window's title-bar area.

required
corner_radius float

Corner radius in logical pixels applied by the render layer. Content is clipped to a rounded rectangle; pixels outside the rounded corners are cleared to transparent. Note that true desktop-transparency in the corners requires platform-level window compositing support.

0.0
border Optional[Border]

Optional border drawn around the window edge inside the rounded-corner boundary.

None

Border dataclass

Border(color: 'ColorSpec', width: float = 1.0)

Border specification for CustomChrome.

Attributes:

Name Type Description
color 'ColorSpec'

Border color (any ColorSpec accepted by the theme system).

width float

Border width in logical pixels.

AppProtocol

Bases: Protocol

The application surface a ViewModel depends on.

Annotate the app a ViewModel receives with this protocol so the ViewModel stays independent of the runtime. Pass it per method call -- the View's event handler resolves App.of(context) and hands it over::

class SettingsViewModel:
    def apply_dark_mode(self, app: AppProtocol) -> None:
        app.set_theme("dark")

App.of(context) returns the running app typed as this protocol, and a hand-written fake needs only these three methods -- no widget tree and no event loop.

exit

exit(exit_code: int = 0) -> None

Exit the application: close every window and stop the loop.

Source code in src/nuiitivet/runtime/protocols.py
def exit(self, exit_code: int = 0) -> None:
    """Exit the application: close every window and stop the loop."""
    ...

set_theme

set_theme(theme: 'str | Theme') -> None

Switch the app-wide theme: a registered name, "light" / "dark", or a :class:~nuiitivet.theme.theme.Theme instance.

Source code in src/nuiitivet/runtime/protocols.py
def set_theme(self, theme: "str | Theme") -> None:
    """Switch the app-wide theme: a registered name, ``"light"`` /
    ``"dark"``, or a :class:`~nuiitivet.theme.theme.Theme` instance."""
    ...

register_themes

register_themes(themes: 'dict[str, Theme]') -> None

Register named themes for later :meth:set_theme calls by name.

Source code in src/nuiitivet/runtime/protocols.py
def register_themes(self, themes: "dict[str, Theme]") -> None:
    """Register named themes for later :meth:`set_theme` calls by name."""
    ...

WindowProtocol

Bases: Protocol

The window surface a ViewModel depends on.

Annotate the window a ViewModel receives with this protocol so it can command its window -- close it, hide it, resize it -- without holding the full :class:~nuiitivet.runtime.window.Window. Pass it per method call -- the View's event handler resolves Window.of(context) and hands it over::

class LauncherViewModel:
    def send_to_background(self, window: WindowProtocol) -> None:
        window.hide()

Window.of(context) returns an object satisfying it, and a fake needs only these members. Every method is a no-op when the OS window does not exist (not realized yet, or already closed).

is_open property

is_open: 'ObservableBase[bool]'

Observable open state: True between open and close.

is_visible property

is_visible: 'ObservableBase[bool]'

Observable visibility: False while hidden or minimized.

closed property

closed: Awaitable[None]

An awaitable that resolves once the window has closed.

close

close() -> None

Close the window permanently.

Source code in src/nuiitivet/runtime/protocols.py
def close(self) -> None:
    """Close the window permanently."""
    ...

hide

hide() -> None

Hide the window without closing it; :meth:show brings it back.

Source code in src/nuiitivet/runtime/protocols.py
def hide(self) -> None:
    """Hide the window without closing it; :meth:`show` brings it back."""
    ...

show

show() -> None

Show the window and bring it to the front, focused.

Source code in src/nuiitivet/runtime/protocols.py
def show(self) -> None:
    """Show the window and bring it to the front, focused."""
    ...

minimize

minimize() -> None

Minimize the window.

Source code in src/nuiitivet/runtime/protocols.py
def minimize(self) -> None:
    """Minimize the window."""
    ...

maximize

maximize() -> None

Maximize the window.

Source code in src/nuiitivet/runtime/protocols.py
def maximize(self) -> None:
    """Maximize the window."""
    ...

restore

restore() -> None

Restore the window from maximized/minimized state.

Source code in src/nuiitivet/runtime/protocols.py
def restore(self) -> None:
    """Restore the window from maximized/minimized state."""
    ...

full_screen

full_screen() -> None

Request full screen mode.

Source code in src/nuiitivet/runtime/protocols.py
def full_screen(self) -> None:
    """Request full screen mode."""
    ...

center

center() -> None

Center the window on its screen.

Source code in src/nuiitivet/runtime/protocols.py
def center(self) -> None:
    """Center the window on its screen."""
    ...

move_to

move_to(x: int, y: int) -> None

Move the window to a specific position.

Source code in src/nuiitivet/runtime/protocols.py
def move_to(self, x: int, y: int) -> None:
    """Move the window to a specific position."""
    ...

resize

resize(width: int, height: int) -> None

Resize the window.

Source code in src/nuiitivet/runtime/protocols.py
def resize(self, width: int, height: int) -> None:
    """Resize the window."""
    ...

Window

Window(content: Widget | RootFactory, width: WindowSizingLike = 'auto', height: WindowSizingLike = 'auto', *, title: str | None | ObservableBase[str | None] = None, chrome: OSChrome | CustomChrome | None = _UNSET, background: ColorSpec = SURFACE, overlay_factory: Callable[[], Overlay] | None = None, window_position: WindowPositionLike | None = None, resizable: bool = True, accepts_first_mouse: bool = True, menu: MenuBar | None = None, parent: Window | None = None, modal: bool = False, close_action: str | ObservableBase[str] = 'close')

One OS window: its widget tree, services, and lifecycle.

Construction builds a model only — no OS window, no mounted tree. :meth:open realizes it (and registers it with the running :class:~nuiitivet.runtime.app.App); :meth:close destroys it. One object is one window lifetime: a closed Window is finished, and showing the same content again means constructing a new one. State that must survive a window lives in app-layer Observables passed into the content.

Initialize the Window model. :meth:open realizes it.

Parameters:

Name Type Description Default
content Widget | RootFactory

The root content. Accepts either a ready Widget instance or a root factory — a zero-argument callable returning the root Widget. Passing a factory is what enables hot reload under python -m nuiitivet.dev: the runner re-invokes it to rebuild the tree after a module reload. The resulting root may be a Navigator (used directly as the root Navigator) or any other Widget, in which case a default root Navigator is created implicitly.

required
width WindowSizingLike

Window width specification.

'auto'
height WindowSizingLike

Window height specification.

'auto'
title str | None | ObservableBase[str | None]

OS window title. Accepts a plain string or an :class:~nuiitivet.observable.protocols.ObservableBase for dynamic updates. Pass None for no title.

None
chrome OSChrome | CustomChrome | None

Window decoration. Pass an :class:OSChrome instance to use OS-managed decorations with an optional style variant, :class:CustomChrome for an app-drawn header, or None for a bare borderless window. Omitting this parameter (the default) is equivalent to OSChrome().

_UNSET
background ColorSpec

Window background color.

SURFACE
overlay_factory Callable[[], Overlay] | None

Optional overlay factory.

None
window_position WindowPositionLike | None

Initial window position. Accepts a 9-point alignment string (e.g. "center", "top-right") or a :class:WindowPosition for offsets and screen selection.

None
resizable bool

Whether the window can be resized.

True
accepts_first_mouse bool

macOS only. When True (default), the click that activates this window while it is inactive is also delivered to the app, matching Windows/Linux and today's platform norm (Finder, Preview). Pass False to restore activate-only behavior for windows where an accidental first click could commit something. No effect on other platforms — they always deliver the click.

True
menu MenuBar | None

The menu bar model (:class:~nuiitivet.menubar.MenuBar), or None for no menu bar. Replace it wholesale via window.menu = ....

None
parent Window | None

The parent window, or None for a top-level window. A child stacks with its parent and closes when it closes.

None
modal bool

Whether this window blocks input to its parent chain while open (framework modal). Requires parent.

False
close_action str | ObservableBase[str]

What the OS close button does: "close" (default) destroys the window; "hide" parks it — :meth:hide — so a tray-resident app can be summoned back. Accepts an Observable so the choice can follow live state; the resident recipe binds it to TrayIcon.installed (hide only while the tray is actually showing). Programmatic :meth:close is unaffected.

'close'
Source code in src/nuiitivet/runtime/window.py
def __init__(
    self,
    content: "Widget | RootFactory",
    width: WindowSizingLike = "auto",
    height: WindowSizingLike = "auto",
    *,
    title: "str | None | ObservableBase[str | None]" = None,
    chrome: "OSChrome | CustomChrome | None" = _UNSET,  # type: ignore[assignment]
    background: ColorSpec = PlainColorRole.SURFACE,
    overlay_factory: Callable[[], "Overlay"] | None = None,
    window_position: WindowPositionLike | None = None,
    resizable: bool = True,
    accepts_first_mouse: bool = True,
    menu: "MenuBar | None" = None,
    parent: "Window | None" = None,
    modal: bool = False,
    close_action: "str | ObservableBase[str]" = "close",
):
    """Initialize the Window model. :meth:`open` realizes it.

    Args:
        content: The root content. Accepts either a ready ``Widget``
            instance or a **root factory** — a zero-argument callable
            returning the root ``Widget``. Passing a factory is what
            enables hot reload under ``python -m nuiitivet.dev``: the
            runner re-invokes it to rebuild the tree after a module
            reload. The resulting root may be a ``Navigator`` (used
            directly as the root Navigator) or any other ``Widget``, in
            which case a default root ``Navigator`` is created implicitly.
        width: Window width specification.
        height: Window height specification.
        title: OS window title. Accepts a plain string or an
            :class:`~nuiitivet.observable.protocols.ObservableBase` for
            dynamic updates. Pass ``None`` for no title.
        chrome: Window decoration. Pass an :class:`OSChrome` instance to
            use OS-managed decorations with an optional style variant,
            :class:`CustomChrome` for an app-drawn header, or ``None``
            for a bare borderless window. Omitting this parameter (the
            default) is equivalent to ``OSChrome()``.
        background: Window background color.
        overlay_factory: Optional overlay factory.
        window_position: Initial window position. Accepts a 9-point
            alignment string (e.g. ``"center"``, ``"top-right"``) or a
            :class:`WindowPosition` for offsets and screen selection.
        resizable: Whether the window can be resized.
        accepts_first_mouse: macOS only. When ``True`` (default), the
            click that activates this window while it is inactive is
            also delivered to the app, matching Windows/Linux and
            today's platform norm (Finder, Preview). Pass ``False`` to
            restore activate-only behavior for windows where an
            accidental first click could commit something. No effect
            on other platforms — they always deliver the click.
        menu: The menu bar model (:class:`~nuiitivet.menubar.MenuBar`),
            or ``None`` for no menu bar. Replace it wholesale via
            ``window.menu = ...``.
        parent: The parent window, or ``None`` for a top-level window.
            A child stacks with its parent and closes when it closes.
        modal: Whether this window blocks input to its parent chain
            while open (framework modal). Requires ``parent``.
        close_action: What the OS close button does: ``"close"`` (default)
            destroys the window; ``"hide"`` parks it — :meth:`hide` —
            so a tray-resident app can be summoned back. Accepts an
            Observable so the choice can follow live state; the resident
            recipe binds it to ``TrayIcon.installed`` (hide only while
            the tray is actually showing). Programmatic :meth:`close`
            is unaffected.
    """
    # Normalize ``content`` to a root factory. A Widget instance is wrapped
    # in a factory that always returns that same instance (so hot reload is
    # a no-op for it); a callable is stored as-is. ``self._root_factory`` is
    # the single source of truth the reload path re-invokes.
    if callable(content) and not isinstance(content, Widget):
        self._root_factory: RootFactory = content
        self._hot_reload_inert = False
    elif isinstance(content, Widget):
        instance = content
        self._root_factory = lambda: instance
        # The wrapping lambda can never be re-fetched from a reloaded
        # module, so every rebuild returns this same object: hot reload
        # can never change this window's tree.
        self._hot_reload_inert = True
    else:
        raise TypeError(
            "'content' must be a Widget instance or a callable returning a Widget."
        )
    # Overlay factory is retained so the reload path can rebuild the
    # Navigator/Overlay stack identically. See :meth:`_rebuild_content_root`.
    self._overlay_factory = overlay_factory

    if parent is not None and not isinstance(parent, Window):
        raise TypeError("'parent' must be a Window instance or None.")
    if modal and parent is None:
        raise ValueError("modal=True requires a parent window.")
    self._parent: "Window | None" = parent
    self._modal = bool(modal)

    # Stable per-process identity, used by tooling (the dev bridge's
    # window selector) and useful in logs. Never reused within a process.
    self.id: int = next(_window_ids)

    # Warn once, at the moment the app shape can still be fixed. Only under
    # the dev runner: in production there is no hot reload to be inert for.
    if self._hot_reload_inert and _under_dev_session():
        root_name = type(content).__name__
        logger.warning(
            "Window id=%d content is a widget instance (%s); hot reload "
            "cannot apply edits to this window. Pass a factory instead: "
            "Window(content=%s) or Window(content=lambda: %s(...)).",
            self.id,
            root_name,
            root_name,
            root_name,
        )

    if not isinstance(close_action, ObservableBase) and close_action not in ("close", "hide"):
        raise ValueError('close_action must be "close", "hide", or an Observable of one.')
    self._close_action: "str | ObservableBase[str]" = close_action

    # Lifecycle: created -> open -> closed, one way. Visibility is
    # orthogonal: a hidden window is still open (and still counts for the
    # App's exit policy).
    self._app_ref: Any = None
    self._lifecycle_state: str = "created"
    self._is_open_obs: Observable[bool] = Observable(False)
    self._visible_obs: Observable[bool] = Observable(True)
    self._closed_event: Any = None
    self._os_active = False
    # Per-window IME geometry (cursor rect, window location). Written by
    # this window's focused text field and its backend window, read by the
    # platform IME hook installed on this OS window. See design doc 8.6.
    self._ime = IMEManager()

    self._width_spec: WindowSizingLike = width
    self._height_spec: WindowSizingLike = height

    self.chrome: OSChrome | CustomChrome | None = OSChrome() if chrome is _UNSET else chrome
    # Reset the drag-area reference (class default is None); a CustomChrome
    # rebuilds it in :meth:`_wrap_with_chrome_and_scope`.
    self._window_drag_area = None

    # Menu bar: the controller owns the registered model and the slots
    # rendering it. The default slot is inserted below the chrome (see
    # :meth:`_wrap_with_chrome_and_scope`) only when a menu was registered
    # at construction; a MenuBarArea in the tree takes over regardless.
    from nuiitivet.menubar.controller import MenuBarController

    self._menubar_controller: MenuBarController = MenuBarController(self, menu)

    # Provisional window size. An ``auto`` dimension is resolved at the end
    # of :meth:`open`, once the tree is mounted and can be measured against
    # the real theme; until then ``on_mount`` code that reads window.width /
    # window.height must still see a number rather than an AttributeError.
    self.width = self._resolve_window_sizing(width, preferred=0, fallback=640)
    self.height = self._resolve_window_sizing(height, preferred=0, fallback=480)
    self.window_position = None if window_position is None else parse_window_position(window_position)
    self.resizable = resizable
    self.accepts_first_mouse = bool(accepts_first_mouse)

    self._title_value: str | None | ObservableBase[str | None] = title
    self._title_disposable: Optional[Disposable] = None

    self._scale = 1.0
    self._dirty = False
    # Content dirtiness is distinct from ``_dirty``: ``_dirty`` means "a frame
    # was requested", while ``_paint_dirty`` means "the widget tree changed and
    # must be re-painted". A surface-loss redraw (window show/activate) requests
    # a frame without changing content, letting the GPU path re-blit its cached
    # full frame instead of re-walking the tree. See ``draw_gpu_frame``.
    self._paint_dirty = True
    self._window = None
    self._event_loop: Any = None
    self._last_hover_target = None
    self._focused_target: Optional[InteractionHostMixin] = None
    self._focused_node: Optional[FocusNode] = None
    # Open blocking overlay entries, innermost last, each paired with the node
    # that held focus when it opened. A modal takes focus with it and hands it
    # back on close; the invoker cannot be looked up from the tree afterwards,
    # because by then the dialog is detached. See :meth:`_sync_overlay_focus_trap`.
    self._overlay_focus_trap: list[Tuple[Widget, Optional[FocusNode]]] = []
    # How the user is driving the app right now. A widget that takes focus on
    # its own (a menu focusing its first item when it opens) inherits it, so a
    # mouse-opened menu does not come up wearing a keyboard focus ring.
    self._last_input_source: FocusSource = FocusSource.KEYBOARD
    self._modifier_keys: int = 0
    # Dev-only observer for the interaction journal. The dev runner
    # attaches an ``InteractionRecorder`` here so the human's coarse UI
    # actions can be recorded for an AI pair to pull; ``None`` -- and zero
    # overhead -- in production.
    self._interaction_recorder: Optional[Any] = None
    # Dev-only designation mode. The dev runner attaches an
    # ``InspectMode`` here so the human can point at a widget for an AI pair
    # to read; ``None`` -- and zero overhead -- in production.
    self._inspect_mode: Optional[Any] = None
    # Last known pointer position / held buttons (screen coords), used to
    # synthesize the pointer event delivered on a modifier-key mask change.
    self._last_pointer_pos: Optional[Tuple[float, float]] = None
    self._last_pointer_buttons: int = 0
    self._pointer_capture_manager = PointerCaptureManager()
    self._pointer_capture_manager.set_cancel_callback(self._handle_pointer_cancel)
    self._primary_pointer_id = 1
    self._background_value: ColorSpec = background
    # Resolved lazily (and re-resolved on theme change): resolution needs
    # the App's theme, which this Window meets at :meth:`open`.
    self._background_color: Any = None
    self._last_layout_size: Optional[tuple[int, int]] = None
    self._saved_window_rect: Optional[tuple[int, int, int, int]] = None

    def _env_flag(name: str, default: bool = False) -> bool:
        raw = os.environ.get(name)
        if raw is None:
            return default
        value = str(raw).strip().lower()
        if value in ("", "0", "false", "no", "off", "disable", "disabled"):
            return False
        if value in ("1", "true", "yes", "on", "enable", "enabled"):
            return True
        return True

    self._debug_invalidate = _env_flag("NUIITIVET_DEBUG_INVALIDATE", default=False)
    self._invalidate_report_every_s = float(os.environ.get("NUIITIVET_DEBUG_INVALIDATE_EVERY", "1.0"))
    self._invalidate_report_every_s = max(0.1, self._invalidate_report_every_s)
    self._invalidate_interval_counts: dict[str, int] = {}
    self._invalidate_total_counts: dict[str, int] = {}
    self._invalidate_last_report = time.perf_counter()

navigator property

navigator: Navigator

This App's root :class:~nuiitivet.navigation.Navigator.

Raises:

Type Description
RuntimeError

If the App has no content root yet.

overlay property

overlay: Overlay

This App's root :class:~nuiitivet.overlay.Overlay.

Raises:

Type Description
RuntimeError

If the App has no content root yet.

menu property writable

menu: MenuBar | None

The registered application menu bar model, or None.

Assigning replaces the model wholesale and rebuilds the rendered bar; item properties (label / enabled / checked) may be Observables and propagate live without replacement.

ime property

ime: IMEManager

This window's IME geometry (cursor rect and window location).

One instance per window, so two windows never race each other's candidate-window positioning.

app property

app: App

The owning :class:~nuiitivet.runtime.app.App.

Raises:

Type Description
RuntimeError

If the window is not attached to an App (it attaches at :meth:open, or when passed to the App constructor).

parent property

parent: Window | None

The parent window, or None for a top-level window.

modal property

modal: bool

Whether this window blocks input to its parent chain while open.

is_open property

is_open: ObservableBase[bool]

Observable open state: True between :meth:open and :meth:close.

is_main property

is_main: bool

Whether this is the App's main window.

closed property

closed: Any

An awaitable that resolves once the window has closed.

is_visible property

is_visible: ObservableBase[bool]

Whether the window is visible (or will be, once realized).

Hidden is not closed: the object, its widget tree, and its geometry stay alive, and the window still counts for the App's exit policy. On Windows/Linux the taskbar entry follows this by itself.

title property

title: str | None

The window title's current value, or None if unset.

Resolves the title given at construction: a plain string is returned as is; an :class:~nuiitivet.observable.protocols.ObservableBase is unwrapped to its current value. Exposed for dev tooling -- the dev bridge's status reports it so an assistant can confirm which app is running -- and never raises: an observable whose read fails reports None rather than propagating.

modifier_keys property

modifier_keys: int

The keyboard-modifier keys currently held down.

A bitmask of MOD_SHIFT/MOD_CTRL/MOD_ALT/MOD_META. This is the single authoritative source of "which modifier keys are down", maintained by the backend on every key press and release and cleared on window deactivation. It is exposed for framework internals only and must not be treated as mutable application state.

of staticmethod

of(context: Widget) -> Window

Return the Window whose tree contains context.

The returned object is the same Window the opener holds — there is no proxy type. A ViewModel should receive it typed as :class:~nuiitivet.runtime.protocols.WindowProtocol. Valid from on_mount, not from __init__, like every .of() lookup.

Parameters:

Name Type Description Default
context Widget

The widget context.

required

Returns:

Type Description
Window

The owning Window.

Raises:

Type Description
RuntimeError

If called before context is mounted (typically from __init__), or if the widget is not attached to a Window.

Source code in src/nuiitivet/runtime/window.py
@staticmethod
def of(context: Widget) -> "Window":
    """Return the Window whose tree contains ``context``.

    The returned object is the same ``Window`` the opener holds — there is
    no proxy type. A ViewModel should receive it typed as
    :class:`~nuiitivet.runtime.protocols.WindowProtocol`. Valid from
    ``on_mount``, not from ``__init__``, like every ``.of()`` lookup.

    Args:
        context: The widget context.

    Returns:
        The owning Window.

    Raises:
        RuntimeError: If called before ``context`` is mounted (typically
            from ``__init__``), or if the widget is not attached to a
            Window.
    """
    scope = find_provider(context, WindowScope)
    window = scope.window if scope is not None else None
    if window is None:
        raise_if_premature_lookup("Window.of", context)
        raise RuntimeError("WindowScope not found. Is the widget attached to a Window?")
    return window

can_handle_back_event

can_handle_back_event() -> bool

Return True if a back action would be handled.

This is a non-mutating check used by backends to decide whether to consume the OS/back key or let default handlers run (e.g. ESC-to-exit).

Source code in src/nuiitivet/runtime/window.py
def can_handle_back_event(self) -> bool:
    """Return True if a back action would be handled.

    This is a non-mutating check used by backends to decide whether to
    consume the OS/back key or let default handlers run (e.g. ESC-to-exit).
    """

    overlay = self._overlay
    if overlay is not None:
        try:
            if overlay.has_entries():
                return True
        except Exception:
            exception_once(logger, "app_overlay_has_entries_exc", "overlay.has_entries() failed")

    navigator = self._navigator
    if navigator is not None:
        try:
            return bool(navigator.can_pop())
        except Exception:
            exception_once(logger, "app_navigator_can_pop_exc", "navigator.can_pop() failed")
            return False

    return False

handle_back_event async

handle_back_event() -> bool

Handle a user back action (e.g. Esc).

Priority: - Overlay: close topmost entry if any - Navigator: pop one route if possible

A back action never reaches past a blocking layer. If the overlay had nothing left to close but is still painting one -- a dialog already dismissed and animating out is the case that reaches here -- the event stops, rather than popping the screen the user can still see behind it. This is the keyboard half of what Overlay.hit_test does for the pointer; a pass-through layer (toast, banner) blocks neither.

Source code in src/nuiitivet/runtime/window.py
async def handle_back_event(self) -> bool:
    """Handle a user back action (e.g. Esc).

    Priority:
    - Overlay: close topmost entry if any
    - Navigator: pop one route if possible

    A back action never reaches past a blocking layer. If the overlay had
    nothing left to close but is still painting one -- a dialog already
    dismissed and animating out is the case that reaches here -- the event
    stops, rather than popping the screen the user can still see behind it.
    This is the keyboard half of what ``Overlay.hit_test`` does for the
    pointer; a pass-through layer (toast, banner) blocks neither.
    """

    overlay = self._overlay
    if overlay is not None:
        try:
            has_entries = bool(overlay.has_entries())
            if has_entries:
                handled = bool(await overlay.async_request_close_topmost())
                if handled:
                    return True
                if overlay.occluding_content_widget() is not None:
                    return True
        except Exception:
            exception_once(logger, "app_overlay_close_topmost_exc", "overlay.close_topmost() failed")

    navigator = self._navigator
    if navigator is not None:
        try:
            request_back = getattr(navigator, "request_back", None)
            if callable(request_back):
                handled = bool(await request_back())
                return handled
            if navigator.can_pop():
                navigator.pop()
                return True
        except Exception:
            exception_once(logger, "app_navigator_back_exc", "Navigator back handling failed")
    return False

open

open() -> Window

Realize the window: build and mount the tree, register with the App.

The OS window itself is created by the running backend — immediately when the loop is already running, or when app.run() starts for windows opened before it.

Returns:

Type Description
Window

self, for chaining.

Raises:

Type Description
RuntimeError

If the window is already open, is already closed (one object is one window lifetime), no App exists yet, or the parent window is not open.

Source code in src/nuiitivet/runtime/window.py
def open(self) -> "Window":
    """Realize the window: build and mount the tree, register with the App.

    The OS window itself is created by the running backend — immediately
    when the loop is already running, or when ``app.run()`` starts for
    windows opened before it.

    Returns:
        ``self``, for chaining.

    Raises:
        RuntimeError: If the window is already open, is already closed
            (one object is one window lifetime), no App exists yet, or
            the parent window is not open.
    """
    if self._lifecycle_state == "open":
        raise RuntimeError("Window is already open.")
    if self._lifecycle_state == "closed":
        raise RuntimeError(
            "A closed Window is finished; construct a new Window to show its content again."
        )
    app = self._app_ref() if self._app_ref is not None else None
    if app is None:
        from nuiitivet.runtime.app import current_app

        app = current_app()
        if app is None:
            raise RuntimeError("Window.open() requires an App; construct the App first.")
        self._app_ref = weakref.ref(app)
    if self._parent is not None and self._parent._lifecycle_state != "open":
        raise RuntimeError("Window.open(): the parent window is not open.")

    content_root = self._root_factory()
    if not isinstance(content_root, Widget):
        raise TypeError("root factory must return a Widget instance.")

    from nuiitivet.navigation import Navigator

    if isinstance(content_root, Navigator):
        navigator = content_root
    else:
        navigator = self._build_default_navigator(content_root)

    built = self._build_root_navigation_stack(
        navigator=navigator,
        overlay_factory=self._overlay_factory,
    )
    # Nothing to unmount on the initial path, so adopt straight away; the
    # reload path defers this to :meth:`_commit_content_root`.
    self._navigator = built.navigator
    self._overlay = built.overlay

    # Apply the chrome decoration and scope wrapping. This must precede the
    # auto-size measurement below: the AppScope installed here is what
    # ``Theme.of`` resolves against, so a tree measured before it exists is
    # measured against the default theme.
    self.root = self._wrap_with_chrome_and_scope(built.widget)

    self._update_background_color()
    self._subscribe_title_updates()

    self._lifecycle_state = "open"
    self._is_open_obs.value = True

    # Mounting comes after every attribute a lifecycle hook might touch is
    # initialized (see ``__init__``): ``mount()`` runs on_mount for the
    # whole tree, and that user code can call straight back into the Window.
    try:
        self.root.mount(self)
    except Exception:
        exception_once(logger, "window_open_root_mount_exc", "root.mount(self) raised during Window.open()")

    width_sizing = parse_window_sizing(self._width_spec)
    height_sizing = parse_window_sizing(self._height_spec)
    if width_sizing.kind == "auto" or height_sizing.kind == "auto":
        self._apply_auto_window_size(
            width=self._width_spec,
            height=self._height_spec,
            target=built.initial_route_widget,
            chrome=self.chrome,
        )

    app._register_window(self)
    self._notify_visibility_changed()
    return self

close

close() -> None

Destroy the window: unmount the tree and close the OS window.

Closing is one-way — the object is finished afterwards. Children close first, transitively. Closing a window that is not open is a no-op.

Source code in src/nuiitivet/runtime/window.py
def close(self) -> None:
    """Destroy the window: unmount the tree and close the OS window.

    Closing is one-way — the object is finished afterwards. Children close
    first, transitively. Closing a window that is not open is a no-op.
    """
    if self._lifecycle_state != "open":
        return
    app = self._app_ref() if self._app_ref is not None else None

    if app is not None:
        for child in [w for w in app.windows if w.parent is self]:
            try:
                child.close()
            except Exception:
                exception_once(logger, "window_close_child_exc", "Closing a child window raised")

    self._lifecycle_state = "closed"
    self._is_open_obs.value = False

    self._unsubscribe_title_updates()

    try:
        root = getattr(self, "root", None)
        if root is not None:
            root.unmount()
    except Exception:
        exception_once(logger, "window_close_root_unmount_exc", "root.unmount raised")

    self._reset_interaction_state()

    os_window = self._window
    self._window = None
    self._event_loop = None
    if os_window is not None:
        try:
            os_window.close()
        except Exception:
            exception_once(logger, "window_close_os_window_exc", "Backend window close raised")

    event = self._closed_event
    if event is not None:
        try:
            event.set()
        except Exception:
            exception_once(logger, "window_closed_event_set_exc", "closed event set raised")

    if app is not None:
        app._unregister_window(self)
    # After unregistration, so the menu bar coordinator re-resolves
    # against the open set without this window.
    self._menubar_controller.window_closed()
    self._notify_visibility_changed()

hide

hide() -> None

Hide the window, keeping the object and its widget tree alive.

The counterpart of :meth:show — the pair a tray-resident app parks and summons its window with. Before the backend realizes the OS window this just records the desired state (so a window can start hidden); hiding a window that is not open is a no-op.

Source code in src/nuiitivet/runtime/window.py
def hide(self) -> None:
    """Hide the window, keeping the object and its widget tree alive.

    The counterpart of :meth:`show` — the pair a tray-resident app parks
    and summons its window with. Before the backend realizes the OS
    window this just records the desired state (so a window can start
    hidden); hiding a window that is not open is a no-op.
    """
    if self._lifecycle_state != "open" or not self._visible_obs.value:
        return
    self._visible_obs.value = False
    window = self._window
    if window is not None:
        try:
            window.set_visible(False)
        except Exception:
            exception_once(logger, "window_hide_exc", "Window.hide failed")
    self._notify_visibility_changed()

show

show() -> None

Make the window visible and bring it to the front, focused.

Also the "summon" action for an already-visible window: it raises and refocuses. Showing a window that is not open is a no-op (a closed Window is finished — construct a new one).

Source code in src/nuiitivet/runtime/window.py
def show(self) -> None:
    """Make the window visible and bring it to the front, focused.

    Also the "summon" action for an already-visible window: it raises
    and refocuses. Showing a window that is not open is a no-op (a
    closed Window is finished — construct a new one).
    """
    if self._lifecycle_state != "open":
        return
    was_hidden = not self._visible_obs.value
    self._visible_obs.value = True
    if was_hidden:
        # Before the OS window reappears, so a dock_visibility="auto"
        # tray restores the regular activation policy first.
        self._notify_visibility_changed()
    window = self._window
    if window is not None:
        try:
            window.set_visible(True)
            window.activate()
        except Exception:
            exception_once(logger, "window_show_exc", "Window.show failed")
        if was_hidden:
            self.invalidate(immediate=True)

render_to_png

render_to_png(path: str)

Render the current UI to a PNG file.

Settles first: an interactive app reaches its final layout over the next frame or two (see :meth:_settle_pending_size_changes), which a single render would otherwise never draw.

Source code in src/nuiitivet/runtime/window.py
def render_to_png(self, path: str):
    """Render the current UI to a PNG file.

    Settles first: an interactive app reaches its final layout over the next
    frame or two (see :meth:`_settle_pending_size_changes`), which a single
    render would otherwise never draw.
    """
    img = self._render_snapshot(scale=1.0, settle=True)
    save_png(img, path)

invalidate

invalidate(immediate: bool = False, content: bool = True)

Request that the next frame be redrawn.

This sets an internal dirty flag which the render loop checks to decide whether to re-render the UI.

Parameters:

Name Type Description Default
immediate bool

If True and running in pyglet, bypass FPS throttle for next draw

False
content bool

If True (default), mark the widget tree as changed so the next frame is fully re-painted. Pass False for surface-loss redraws (window show/activate) where the tree is unchanged and the GPU path may re-blit its cached full frame instead of re-walking the tree.

True
Source code in src/nuiitivet/runtime/window.py
def invalidate(self, immediate: bool = False, content: bool = True):
    """Request that the next frame be redrawn.

    This sets an internal dirty flag which the render loop checks to
    decide whether to re-render the UI.

    Args:
        immediate: If True and running in pyglet, bypass FPS throttle for next draw
        content: If True (default), mark the widget tree as changed so the next
            frame is fully re-painted. Pass False for surface-loss redraws
            (window show/activate) where the tree is unchanged and the GPU path
            may re-blit its cached full frame instead of re-walking the tree.
    """
    self._dirty = True
    if content:
        self._paint_dirty = True
    self._debug_record_invalidate()
    loop = self._event_loop
    if loop is not None:
        try:
            loop.request_draw(immediate=immediate)
        except Exception:
            exception_once(logger, "app_request_draw_exc", "Event loop request_draw raised")

request_focus

request_focus(node: Optional[FocusNode], source: FocusSource = KEYBOARD) -> None

Set focus to the given FocusNode. Pass None to clear focus.

Source code in src/nuiitivet/runtime/window.py
def request_focus(self, node: Optional[FocusNode], source: FocusSource = FocusSource.KEYBOARD) -> None:
    """Set focus to the given FocusNode. Pass ``None`` to clear focus."""
    if self._focused_node is node:
        # Same node, possibly a different source: a pointer press on the widget
        # that Tab already focused still has to hide its focus ring.
        if node is not None:
            node.notify_focus_source(source)
        return

    # Blur previous node
    if self._focused_node:
        self._focused_node._set_focused(False)

    # Focus new node
    self._focused_node = node
    if node:
        node._set_focused(True, source)
        # Also update legacy target if the node belongs to a widget
        if node.region:
            self._focused_target = node.region
    else:
        self._focused_target = None

center

center() -> None

Center the window on its screen.

Source code in src/nuiitivet/runtime/window.py
def center(self) -> None:
    """Center the window on its screen."""
    window = self._window
    if window is None:
        return
    try:
        screen = window.screen
        if screen:
            x = (screen.width - window.width) // 2
            y = (screen.height - window.height) // 2
            window.set_location(x, y)
    except Exception:
        exception_once(logger, "window_center_exc", "Window.center failed")

maximize

maximize() -> None

Maximize the window.

Source code in src/nuiitivet/runtime/window.py
def maximize(self) -> None:
    """Maximize the window."""
    window = self._window
    if window is None:
        return
    try:
        # Save current window rect before maximizing
        try:
            wx, wy = window.get_location()
            ww, wh = window.width, window.height
            self._saved_window_rect = (wx, wy, ww, wh)
        except Exception:
            self._saved_window_rect = None

        if sys.platform == "darwin":
            try:
                import ctypes
                from pyglet.libs.darwin import cocoapy
                from pyglet.libs.darwin.cocoapy import cocoatypes

                ns_window = window._nswindow
                screen = ns_window.screen()
                # visibleFrame returns NSRect
                visible_frame = cocoapy.send_message(screen, "visibleFrame", restype=cocoatypes.NSRect)
                # setFrame:display:
                # void setFrame:(NSRect)frameRect display:(BOOL)flag
                cocoapy.send_message(
                    ns_window,
                    "setFrame:display:",
                    visible_frame,
                    True,
                    argtypes=[cocoatypes.NSRect, ctypes.c_bool],
                )
            except Exception:
                # Fallback if something goes wrong (e.g. older pyglet)
                window.maximize()
        else:
            window.maximize()
    except Exception:
        exception_once(logger, "window_maximize_exc", "Window.maximize failed")

minimize

minimize() -> None

Minimize the window.

Source code in src/nuiitivet/runtime/window.py
def minimize(self) -> None:
    """Minimize the window."""
    window = self._window
    if window is None:
        return
    try:
        window.minimize()
    except Exception:
        exception_once(logger, "window_minimize_exc", "Window.minimize failed")

restore

restore() -> None

Restore the window from maximized/minimized/full-screen state.

Source code in src/nuiitivet/runtime/window.py
def restore(self) -> None:
    """Restore the window from maximized/minimized/full-screen state."""
    window = self._window
    if window is None:
        return
    try:
        if window.fullscreen:
            window.set_fullscreen(False)
            return

        try:
            if sys.platform == "win32":
                import ctypes

                SW_RESTORE = 9
                hwnd = getattr(window, "_hwnd", None)
                if hwnd:
                    ctypes.windll.user32.ShowWindow(hwnd, SW_RESTORE)
        except Exception:
            exception_once(logger, "window_restore_win32_exc", "Windows restore fallback failed")

        # Try to activate (restore from minimize on some platforms)
        if hasattr(window, "activate"):
            window.activate()

        # Restore from maximize if we have saved state
        if self._saved_window_rect is not None:
            try:
                rx, ry, rw, rh = self._saved_window_rect
                window.set_location(rx, ry)
                window.set_size(rw, rh)
                self._saved_window_rect = None
            except Exception:
                exception_once(logger, "window_restore_rect_exc", "Failed to restore window rect")

        # Note: Pyglet doesn't have a direct 'unmaximize' or 'restore' from maximize
        # that is consistent across platforms.
    except Exception:
        exception_once(logger, "window_restore_exc", "Window.restore failed")

full_screen

full_screen() -> None

Enter full screen mode (no toggle; :meth:restore is the way back).

Source code in src/nuiitivet/runtime/window.py
def full_screen(self) -> None:
    """Enter full screen mode (no toggle; :meth:`restore` is the way back)."""
    window = self._window
    if window is None:
        return
    try:
        window.set_fullscreen(True)
    except Exception:
        exception_once(logger, "window_full_screen_exc", "Window.full_screen failed")

move_to

move_to(x: int, y: int) -> None

Move the window to a specific screen position.

Source code in src/nuiitivet/runtime/window.py
def move_to(self, x: int, y: int) -> None:
    """Move the window to a specific screen position."""
    window = self._window
    if window is None:
        return
    try:
        window.set_location(int(x), int(y))
    except Exception:
        exception_once(logger, "window_move_to_exc", "Window.move_to failed")

resize

resize(width: int, height: int) -> None

Resize the window.

Source code in src/nuiitivet/runtime/window.py
def resize(self, width: int, height: int) -> None:
    """Resize the window."""
    window = self._window
    if window is None:
        return
    try:
        window.set_size(int(width), int(height))
    except Exception:
        exception_once(logger, "window_resize_exc", "Window.resize failed")

WindowScope

WindowScope(window: Window, child: Widget, *, key: str | None = None)

Bases: Widget

Inherited widget that provides access to the owning :class:Window.

Every window's root is wrapped in one of these (inside the app-wide AppScope), so Window.of(context) — and the window-scoped fallback of Overlay.of / Navigator.of — resolves to the window the context belongs to, never to a process-wide default.

Source code in src/nuiitivet/runtime/window.py
def __init__(self, window: "Window", child: Widget, *, key: "str | None" = None) -> None:
    super().__init__(key=key)
    self._window_ref = weakref.ref(window)
    self.add_child(child)

window property

window: Optional[Window]

The Window this scope belongs to, or None once collected.

WindowPosition dataclass

WindowPosition(alignment_key: str, offset: Tuple[float, float] = (0.0, 0.0), screen_index: int = 0)

Represents how the OS window is positioned within a screen.

Coordinates: - alignment uses the 9-point vocabulary from the layout system. - offset is applied after alignment in logical pixels. The offset uses UI coordinates: $+x$ is right, $+y$ is down.

WindowSizing dataclass

WindowSizing(kind: WindowSizingKind, value: float = 0.0)

Represents how a window requests its initial size along an axis.

Fonts

Application-wide font configuration (default family, bundled fonts).

set_default_family staticmethod

set_default_family(family_name: Optional[str]) -> None

Set the application-wide default font family.

The family is prioritized over locale-based defaults wherever no explicit font_family is given. Pass None to reset to automatic locale detection.

Source code in src/nuiitivet/rendering/fonts.py
@staticmethod
def set_default_family(family_name: Optional[str]) -> None:
    """Set the application-wide default font family.

    The family is prioritized over locale-based defaults wherever no
    explicit ``font_family`` is given. Pass ``None`` to reset to automatic
    locale detection.
    """
    _set_default_font_family(family_name)

register staticmethod

register(path: str, family_name: str) -> None

Register a font file under a custom family name.

Call at application startup, before any widget is rendered. Once registered, the family name can be used wherever a font_family is accepted (e.g. TextStyle(font_family=...), Icon(..., font_family=...)). The file is loaded lazily on first use and cached.

Parameters:

Name Type Description Default
path str

Absolute or relative path to a .ttf or .otf file.

required
family_name str

The name to associate with this font.

required
Source code in src/nuiitivet/rendering/fonts.py
@staticmethod
def register(path: str, family_name: str) -> None:
    """Register a font file under a custom family name.

    Call at application startup, before any widget is rendered. Once
    registered, the family name can be used wherever a ``font_family`` is
    accepted (e.g. ``TextStyle(font_family=...)``,
    ``Icon(..., font_family=...)``). The file is loaded lazily on first
    use and cached.

    Args:
        path: Absolute or relative path to a ``.ttf`` or ``.otf`` file.
        family_name: The name to associate with this font.
    """
    _register_font(path, family_name)

allow

allow(pattern: Union[str, Pattern[str]]) -> InputFilter

Keep only the characters that match pattern, dropping the rest.

The pattern is matched against one character at a time, so allow(r"[A-Za-z ]") yields a field that letters and spaces can be typed into and nothing else. Use :func:matching for a rule about the text as a whole.

Source code in src/nuiitivet/widgets/input_filter.py
def allow(pattern: Union[str, Pattern[str]]) -> InputFilter:
    """Keep only the characters that match ``pattern``, dropping the rest.

    The pattern is matched against one character at a time, so
    ``allow(r"[A-Za-z ]")`` yields a field that letters and spaces can be typed
    into and nothing else. Use :func:`matching` for a rule about the text as a
    whole.
    """
    return _CharacterFilter(_compile(pattern), keep_matching=True)

deny

deny(pattern: Union[str, Pattern[str]]) -> InputFilter

Drop the characters that match pattern, keeping the rest.

The inverse of :func:allow, and the better fit when only a few characters are unwanted: deny(r"[\s]") for a field that must not contain spaces.

Source code in src/nuiitivet/widgets/input_filter.py
def deny(pattern: Union[str, Pattern[str]]) -> InputFilter:
    """Drop the characters that match ``pattern``, keeping the rest.

    The inverse of :func:`allow`, and the better fit when only a few characters
    are unwanted: ``deny(r"[\\s]")`` for a field that must not contain spaces.
    """
    return _CharacterFilter(_compile(pattern), keep_matching=False)

digits_only

digits_only() -> InputFilter

Keep only ASCII digits. Equivalent to allow(r"[0-9]").

Source code in src/nuiitivet/widgets/input_filter.py
def digits_only() -> InputFilter:
    """Keep only ASCII digits. Equivalent to ``allow(r"[0-9]")``."""
    return allow(r"[0-9]")

matching

matching(pattern: Union[str, Pattern[str]]) -> InputFilter

Reject an edit unless the resulting text matches pattern in full.

Unlike :func:allow, which filters character by character and always accepts something, this rejects the keystroke outright when the result would not match -- the rule it enforces is about the text as a whole, so there is no partial result to keep. It is what expresses "at most one decimal point"::

input_filter=matching(r"[0-9]*\.?[0-9]*")

The pattern must accept every string the user has to be able to pass through on the way to a finished value, including "" if the field is to be clearable.

Source code in src/nuiitivet/widgets/input_filter.py
def matching(pattern: Union[str, Pattern[str]]) -> InputFilter:
    """Reject an edit unless the resulting text matches ``pattern`` in full.

    Unlike :func:`allow`, which filters character by character and always
    accepts something, this rejects the keystroke outright when the result
    would not match -- the rule it enforces is about the text as a whole, so
    there is no partial result to keep. It is what expresses "at most one
    decimal point"::

        input_filter=matching(r"[0-9]*\\.?[0-9]*")

    The pattern must accept every string the user has to be able to pass
    through on the way to a finished value, including ``""`` if the field is to
    be clearable.
    """
    return _MatchingFilter(_compile(pattern))

max_length

max_length(max_chars: int) -> InputFilter

Limit the text to max_chars characters, truncating from the end.

Source code in src/nuiitivet/widgets/input_filter.py
def max_length(max_chars: int) -> InputFilter:
    """Limit the text to ``max_chars`` characters, truncating from the end."""
    return _MaxLengthFilter(max_chars)

batch

batch() -> BatchContext

Context manager for batching Observable updates.

Source code in src/nuiitivet/observable/batching.py
def batch() -> BatchContext:
    """Context manager for batching Observable updates."""
    current: Optional[Any] = _batch_context.get()
    if current is not None:
        return current
    return BatchContext()

focusable

focusable(enabled: bool = True, on_focus_change: Optional[FocusChangeCallback] = None, on_key: Optional[Callable[[str, int], bool]] = None, on_key_up: Optional[Callable[[str, int], bool]] = None) -> FocusableModifier

Mark the widget as focusable.

Parameters:

Name Type Description Default
enabled bool

Whether the widget is focusable.

True
on_focus_change Optional[FocusChangeCallback]

Callback invoked when focus state changes.

None
on_key Optional[Callable[[str, int], bool]]

Callback invoked as on_key(key, modifier_keys) when a key is pressed while focused. modifier_keys is a bitmask of the held modifier keys (MOD_SHIFT, MOD_CTRL, ...). Return True to stop propagation (bubbling).

None
on_key_up Optional[Callable[[str, int], bool]]

Callback invoked as on_key_up(key, modifier_keys) when a key is released while focused. Bubbles to ancestor focusables and stops on a truthy return, exactly like on_key.

None
Source code in src/nuiitivet/modifiers/focus.py
def focusable(
    enabled: bool = True,
    on_focus_change: Optional[FocusChangeCallback] = None,
    on_key: Optional[Callable[[str, int], bool]] = None,
    on_key_up: Optional[Callable[[str, int], bool]] = None,
) -> FocusableModifier:
    """
    Mark the widget as focusable.

    Args:
        enabled: Whether the widget is focusable.
        on_focus_change: Callback invoked when focus state changes.
        on_key: Callback invoked as ``on_key(key, modifier_keys)`` when a key is
                pressed while focused. ``modifier_keys`` is a bitmask of the held
                modifier keys (``MOD_SHIFT``, ``MOD_CTRL``, ...). Return True to
                stop propagation (bubbling).
        on_key_up: Callback invoked as ``on_key_up(key, modifier_keys)`` when a
                key is released while focused. Bubbles to ancestor focusables and
                stops on a truthy return, exactly like ``on_key``.
    """
    return FocusableModifier(enabled, on_focus_change, on_key, on_key_up)

on_mount

on_mount(callback: VoidCallback) -> OnMountModifier

Return a modifier that runs callback when the widget is mounted.

The callback runs right after the widget's :meth:Widget.on_mount hook and before its children are mounted. Exceptions are logged and contained.

Parameters:

Name Type Description Default
callback VoidCallback

A no-argument callable. If it is a coroutine function, it is started as a task on mount and cancelled on unmount.

required

Returns:

Name Type Description
An OnMountModifier

class:OnMountModifier to apply via widget.modifier(...).

Note

Mount is not "once per logical component". A ComposableWidget rebuild discards the built subtree and mounts freshly-created widget instances, so the callback runs again for the new instance. Use it for work tied to the widget instance's presence in the tree, not for one-time initialization of a component.

Source code in src/nuiitivet/modifiers/lifecycle.py
def on_mount(callback: VoidCallback) -> OnMountModifier:
    """Return a modifier that runs *callback* when the widget is mounted.

    The callback runs right after the widget's :meth:`Widget.on_mount` hook and
    before its children are mounted. Exceptions are logged and contained.

    Args:
        callback: A no-argument callable. If it is a coroutine function, it is
            started as a task on mount and cancelled on unmount.

    Returns:
        An :class:`OnMountModifier` to apply via ``widget.modifier(...)``.

    Note:
        Mount is *not* "once per logical component". A ``ComposableWidget``
        rebuild discards the built subtree and mounts freshly-created widget
        instances, so the callback runs again for the new instance. Use it for
        work tied to the widget instance's presence in the tree, not for
        one-time initialization of a component.
    """
    return OnMountModifier(callback=callback)

on_size_changed

on_size_changed(callback: SizeCallback) -> OnSizeChangedModifier

Return a modifier that calls callback with the widget's measured size.

The callback receives a :class:Size - the widget's own (width, height) after layout, excluding its position. Use it to feed a size into imperative state: a ViewModel, or a plain Observable a child widget binds to::

class ResponsiveScaffold(ComposableWidget):
    def __init__(self) -> None:
        super().__init__()
        self.expanded = Observable(False)

    def _on_size(self, size: Size) -> None:
        self.expanded.value = size.width >= 700

    def build(self) -> Widget:
        rail = NavigationRail(..., expanded=self.expanded)
        return Row([rail, card], ...).modifier(on_size_changed(self._on_size))

Parameters:

Name Type Description Default
callback SizeCallback

A callable taking a :class:Size, sync or async. An async callback is scheduled as a task. Exceptions are logged and contained.

required

Returns:

Name Type Description
An OnSizeChangedModifier

class:OnSizeChangedModifier to apply via widget.modifier(...).

Note

Fires once with the first measurement, so the callback alone is enough to seed the state it drives. After that it fires only when the measured size actually changed; a widget that is re-laid-out at the same size, or merely moved, is silent.

Note

Dispatched between frames, never during layout, so the callback may safely do anything - push a route, write an Observable, replace children - and its effect lands on the frame after the measurement.

That includes the first call, which therefore arrives after the first paint. Give an Observable the value you expect at the initial size and the first report writes the same value, which de-dupes: no visible transition. Seed it differently and the widget animates once on startup.

Warning

Avoid making the callback change the measured widget's own size: that feeds back into the next measurement and can oscillate. The structurally safe pattern is to measure a widget whose size the parent imposes (Sizing.weight(...) / "wt") and let the callback change only what is inside it.

Source code in src/nuiitivet/modifiers/size_changed.py
def on_size_changed(callback: SizeCallback) -> OnSizeChangedModifier:
    """Return a modifier that calls *callback* with the widget's measured size.

    The callback receives a :class:`Size` - the widget's own ``(width, height)``
    after layout, excluding its position. Use it to feed a size into imperative
    state: a ViewModel, or a plain ``Observable`` a child widget binds to::

        class ResponsiveScaffold(ComposableWidget):
            def __init__(self) -> None:
                super().__init__()
                self.expanded = Observable(False)

            def _on_size(self, size: Size) -> None:
                self.expanded.value = size.width >= 700

            def build(self) -> Widget:
                rail = NavigationRail(..., expanded=self.expanded)
                return Row([rail, card], ...).modifier(on_size_changed(self._on_size))

    Args:
        callback: A callable taking a :class:`Size`, sync or async. An async
            callback is scheduled as a task. Exceptions are logged and contained.

    Returns:
        An :class:`OnSizeChangedModifier` to apply via ``widget.modifier(...)``.

    Note:
        **Fires once with the first measurement**, so the callback alone is
        enough to seed the state it drives. After that it fires only when the
        measured size actually changed; a widget that is re-laid-out at the same
        size, or merely moved, is silent.

    Note:
        **Dispatched between frames, never during layout**, so the callback may
        safely do anything - push a route, write an Observable, replace
        children - and its effect lands on the frame after the measurement.

        That includes the first call, which therefore arrives *after* the first
        paint. Give an Observable the value you expect at the initial size and
        the first report writes the same value, which de-dupes: no visible
        transition. Seed it differently and the widget animates once on startup.

    Warning:
        Avoid making the callback change the measured widget's *own* size: that
        feeds back into the next measurement and can oscillate. The structurally
        safe pattern is to measure a widget whose size the parent imposes
        (``Sizing.weight(...)`` / ``"wt"``) and let the callback change only what
        is *inside* it.
    """
    return OnSizeChangedModifier(callback=callback)

on_unmount

on_unmount(callback: VoidCallback) -> OnUnmountModifier

Return a modifier that runs callback when the widget is unmounted.

The callback runs right after the widget's :meth:Widget.on_unmount hook and before its children are unmounted. Exceptions are logged and contained.

Parameters:

Name Type Description Default
callback VoidCallback

A no-argument callable. A coroutine function is scheduled as a task, which may outlive the widget — prefer a synchronous callback for cleanup that must complete.

required

Returns:

Name Type Description
An OnUnmountModifier

class:OnUnmountModifier to apply via widget.modifier(...).

Source code in src/nuiitivet/modifiers/lifecycle.py
def on_unmount(callback: VoidCallback) -> OnUnmountModifier:
    """Return a modifier that runs *callback* when the widget is unmounted.

    The callback runs right after the widget's :meth:`Widget.on_unmount` hook
    and before its children are unmounted. Exceptions are logged and contained.

    Args:
        callback: A no-argument callable. A coroutine function is scheduled as
            a task, which may outlive the widget — prefer a synchronous
            callback for cleanup that must complete.

    Returns:
        An :class:`OnUnmountModifier` to apply via ``widget.modifier(...)``.
    """
    return OnUnmountModifier(callback=callback)

opacity

opacity(value: OpacityLike) -> TransformModifier

Return a modifier that applies opacity to a widget during paint.

Parameters:

Name Type Description Default
value OpacityLike

Opacity value between 0.0 (transparent) and 1.0 (opaque), or an observable providing opacity.

required
Note

Opacity is paint-only. Layout and hit-testing remain unchanged.

Source code in src/nuiitivet/modifiers/transform.py
def opacity(value: OpacityLike) -> TransformModifier:
    """Return a modifier that applies opacity to a widget during paint.

    Args:
        value: Opacity value between 0.0 (transparent) and 1.0 (opaque),
            or an observable providing opacity.

    Note:
        Opacity is paint-only. Layout and hit-testing remain unchanged.
    """
    return TransformModifier(opacity=value)

rotate

rotate(angle: AngleLike, origin: OriginLike = 'center') -> TransformModifier

Return a modifier that rotates a widget during paint.

Parameters:

Name Type Description Default
angle AngleLike

Rotation angle in degrees, or an observable providing degrees.

required
origin OriginLike

Rotation origin. Accepts any of the nine-point alignment tokens in canonical hyphen form ("center", "top-left", "top-center", "top-right", "center-left", "center-right", "bottom-left", "bottom-center", "bottom-right"); the underscore form ("top_left") is accepted as an alias. Alternatively an (x, y) tuple in local coords. Defaults to "center".

'center'
Note

Rotation is paint-only. Layout and hit-testing remain untransformed.

Source code in src/nuiitivet/modifiers/transform.py
def rotate(angle: AngleLike, origin: OriginLike = "center") -> TransformModifier:
    """Return a modifier that rotates a widget during paint.

    Args:
        angle: Rotation angle in degrees, or an observable providing degrees.
        origin: Rotation origin. Accepts any of the nine-point alignment tokens
            in canonical hyphen form ("center", "top-left", "top-center",
            "top-right", "center-left", "center-right", "bottom-left",
            "bottom-center", "bottom-right"); the underscore form ("top_left")
            is accepted as an alias. Alternatively an (x, y) tuple in local
            coords. Defaults to "center".

    Note:
        Rotation is paint-only. Layout and hit-testing remain untransformed.
    """
    return TransformModifier(rotation=angle, transform_origin=origin)

scale

scale(factor: ScaleLike, origin: OriginLike = 'center') -> TransformModifier

Return a modifier that scales a widget during paint.

Parameters:

Name Type Description Default
factor ScaleLike

Scale factor (uniform) or (sx, sy) tuple, or an observable.

required
origin OriginLike

Scale origin. Accepts any of the nine-point alignment tokens in canonical hyphen form ("center", "top-left", "top-center", "top-right", "center-left", "center-right", "bottom-left", "bottom-center", "bottom-right"); the underscore form ("top_left") is accepted as an alias. Alternatively an (x, y) tuple in local coords. Defaults to "center".

'center'
Note

Scale is paint-only. Layout and hit-testing remain untransformed.

Source code in src/nuiitivet/modifiers/transform.py
def scale(factor: ScaleLike, origin: OriginLike = "center") -> TransformModifier:
    """Return a modifier that scales a widget during paint.

    Args:
        factor: Scale factor (uniform) or (sx, sy) tuple, or an observable.
        origin: Scale origin. Accepts any of the nine-point alignment tokens
            in canonical hyphen form ("center", "top-left", "top-center",
            "top-right", "center-left", "center-right", "bottom-left",
            "bottom-center", "bottom-right"); the underscore form ("top_left")
            is accepted as an alias. Alternatively an (x, y) tuple in local
            coords. Defaults to "center".

    Note:
        Scale is paint-only. Layout and hit-testing remain untransformed.
    """
    return TransformModifier(scale=factor, transform_origin=origin)

shadows

shadows(layers: ShadowLike) -> ShadowModifier

Draw a stack of shadow layers behind the widget.

Parameters:

Name Type Description Default
layers ShadowLike

A Shadow, a sequence of them ordered back to front, or None for no shadow. Material Design's elevation, for instance, is a key layer over a wider ambient one.

required

Returns:

Type Description
ShadowModifier

The modifier to apply.

Source code in src/nuiitivet/modifiers/shadow.py
def shadows(layers: ShadowLike) -> ShadowModifier:
    """Draw a stack of shadow layers behind the widget.

    Args:
        layers: A ``Shadow``, a sequence of them ordered back to front, or
            ``None`` for no shadow. Material Design's elevation, for
            instance, is a key layer over a wider ambient one.

    Returns:
        The modifier to apply.
    """
    return ShadowModifier(layers=normalize_shadows(layers))

translate

translate(offset: TranslateLike) -> TransformModifier

Return a modifier that translates a widget during paint.

Parameters:

Name Type Description Default
offset TranslateLike

Translation offset as (dx, dy) tuple, or an observable.

required
Note

Translation is paint-only. Layout and hit-testing remain untransformed.

Source code in src/nuiitivet/modifiers/transform.py
def translate(offset: TranslateLike) -> TransformModifier:
    """Return a modifier that translates a widget during paint.

    Args:
        offset: Translation offset as (dx, dy) tuple, or an observable.

    Note:
        Translation is paint-only. Layout and hit-testing remain untransformed.
    """
    return TransformModifier(translation=offset)