Skip to content

Material Components

High-level widgets implementing Material Design 3.

material

Material design system root.

This is the single public import root for Material apps. It re-exports every core symbol from :mod:nuiitivet and adds the Material widgets/styles, so a single import gives access to everything::

import nuiitivet.material as nv

nv.Column(...)   # core symbol
nv.Button(...)   # material symbol

Deep imports (nuiitivet.material.buttons, nuiitivet.material.styles.*, ...) are internal and unsupported.

ButtonSize module-attribute

ButtonSize = Literal['xs', 's', 'm', 'l', 'xl']

M3 button size preset.

FabSize module-attribute

FabSize = Literal['s', 'm', 'l']

Framework-unified FAB size preset.

The literal values follow the project-wide s/m/l convention rather than the MD3 spec wording (FAB / Medium FAB / Large FAB):

  • "s" corresponds to the baseline 56dp FAB (MD3 "FAB").
  • "m" corresponds to the 80dp Medium FAB.
  • "l" corresponds to the 96dp Large FAB.

The deprecated 40dp Small FAB is intentionally not represented.

ButtonGroupPosition module-attribute

ButtonGroupPosition = Literal['start', 'middle', 'end', 'only']

Position of a segment within a ButtonGroup.

LargeBadge

LargeBadge(text: str, *, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int], None] = None, style: Optional[LargeBadgeStyle] = None, key: Optional[str] = None)

Bases: Box

Large text badge widget.

Initialize LargeBadge.

The height is MD3-fixed (spec) and the width is content-driven, so neither is a constructor parameter; customize the height via style (SIZE_POLICY: MD3 fixes the axis -> style only).

Parameters:

Name Type Description Default
text str

Badge text to display. Must be non-empty.

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

Insets from the allocated rect to the pill.

None
style Optional[LargeBadgeStyle]

Optional style override.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/badge.py
def __init__(
    self,
    text: str,
    *,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int], None] = None,
    style: Optional[LargeBadgeStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize LargeBadge.

    The height is MD3-fixed (spec) and the width is content-driven, so
    neither is a constructor parameter; customize the height via ``style``
    (SIZE_POLICY: MD3 fixes the axis -> style only).

    Args:
        text: Badge text to display. Must be non-empty.
        padding: Insets from the allocated rect to the pill.
        style: Optional style override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    if not text:
        raise ValueError("text must be non-empty")

    self.text = text

    effective_style = style or LargeBadgeStyle()

    label = Text(
        text,
        style=TextStyle(color=effective_style.content_color),
        type_scale=TypeScaleToken.from_size(effective_style.font_size),
        alignment="center",
        max_lines=1,
        overflow="clip",
    )
    pill = Box(
        child=label,
        height=Sizing.fixed(effective_style.height),
        padding=effective_style.content_insets,
        background_color=effective_style.background_color,
        corner_radius=effective_style.corner_radius,
        alignment="center",
    )

    super().__init__(
        child=pill,
        padding=padding if padding is not None else 0,
        alignment="center",
        key=key,
    )

stick_modifier

stick_modifier(*, badge: Optional[Widget] = None) -> StickModifier

Create a spec-aligned stick modifier for attaching this large badge.

Parameters:

Name Type Description Default
badge Optional[Widget]

Optional badge widget to place. Defaults to this badge instance.

None

Returns:

Type Description
StickModifier

Stick modifier configured for MD3-like large badge placement.

Source code in src/nuiitivet/material/badge.py
def stick_modifier(self, *, badge: Optional[Widget] = None) -> StickModifier:
    """Create a spec-aligned stick modifier for attaching this large badge.

    Args:
        badge: Optional badge widget to place. Defaults to this badge instance.

    Returns:
        Stick modifier configured for MD3-like large badge placement.
    """
    target_badge = badge if badge is not None else self
    return stick(
        target_badge,
        target_anchor="top-right",
        content_anchor="bottom-left",
        offset=(-12.0, 14.0),
    )

SmallBadge

SmallBadge(*, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, style: Optional[SmallBadgeStyle] = None, key: Optional[str] = None)

Bases: Box

Small dot badge widget.

Initialize SmallBadge.

The dot dimensions are MD3-fixed (spec size tokens), so they are not constructor parameters; customize them via style instead (SIZE_POLICY: MD3 fixes the axis -> style only).

Parameters:

Name Type Description Default
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

External badge padding.

0
style Optional[SmallBadgeStyle]

Optional style override.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/badge.py
def __init__(
    self,
    *,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    style: Optional[SmallBadgeStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize SmallBadge.

    The dot dimensions are MD3-fixed (spec size tokens), so they are not
    constructor parameters; customize them via ``style`` instead
    (SIZE_POLICY: MD3 fixes the axis -> style only).

    Args:
        padding: External badge padding.
        style: Optional style override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    effective_style = style or SmallBadgeStyle()

    super().__init__(
        child=None,
        width=Sizing.fixed(effective_style.width),
        height=Sizing.fixed(effective_style.height),
        padding=padding,
        background_color=effective_style.background_color,
        corner_radius=effective_style.corner_radius,
        key=key,
    )

stick_modifier

stick_modifier(*, badge: Optional[Widget] = None) -> StickModifier

Create a spec-aligned stick modifier for attaching this small badge.

Parameters:

Name Type Description Default
badge Optional[Widget]

Optional badge widget to place. Defaults to this badge instance.

None

Returns:

Type Description
StickModifier

Stick modifier configured for MD3-like small badge placement.

Source code in src/nuiitivet/material/badge.py
def stick_modifier(self, *, badge: Optional[Widget] = None) -> StickModifier:
    """Create a spec-aligned stick modifier for attaching this small badge.

    Args:
        badge: Optional badge widget to place. Defaults to this badge instance.

    Returns:
        Stick modifier configured for MD3-like small badge placement.
    """
    target_badge = badge if badge is not None else self
    return stick(
        target_badge,
        target_anchor="top-right",
        content_anchor="bottom-left",
        offset=(-6.0, 6.0),
    )

App

App(window: Window, *, theme: Optional[Any] = None, exit_policy: ExitPolicy = LAST_WINDOW_CLOSED, tray: Optional[TrayIcon] = None)

Bases: App

Material Design application runner.

Takes its main window as the first argument and supplies the Material default theme. Through the public surface the window is a :class:~nuiitivet.material.window.MaterialWindow: nv.App(nv.Window(content=...)). Secondary windows are constructed with nv.Window(...) and shown with window.open().

Initialize a MaterialApp.

Parameters:

Name Type Description Default
window Window

The main window, typically a :class:~nuiitivet.material.window.MaterialWindow.

required
theme Optional[Any]

The MaterialThemeFactory to use. Defaults to Light theme.

None
exit_policy ExitPolicy

When :meth:run returns; see :class:~nuiitivet.runtime.app.ExitPolicy.

LAST_WINDOW_CLOSED
tray Optional[TrayIcon]

A :class:~nuiitivet.platform.tray.TrayIcon to show while the app runs; see :attr:TrayIcon.installed.

None
Source code in src/nuiitivet/material/app.py
def __init__(
    self,
    window: Window,
    *,
    theme: Optional[Any] = None,
    exit_policy: ExitPolicy = ExitPolicy.LAST_WINDOW_CLOSED,
    tray: Optional[TrayIcon] = None,
) -> None:
    """Initialize a MaterialApp.

    Args:
        window: The main window, typically a
            :class:`~nuiitivet.material.window.MaterialWindow`.
        theme: The MaterialThemeFactory to use. Defaults to Light theme.
        exit_policy: When :meth:`run` returns; see
            :class:`~nuiitivet.runtime.app.ExitPolicy`.
        tray: A :class:`~nuiitivet.platform.tray.TrayIcon` to show while
            the app runs; see :attr:`TrayIcon.installed`.
    """
    if theme is None:
        theme = MaterialThemeFactory.light("#6750A4")

    super().__init__(window, theme=theme, exit_policy=exit_policy, tray=tray)

HorizontalDivider

HorizontalDivider(*, width: SizingLike = None, padding: PaddingLike = 0, style: Optional[DividerStyle] = None, key: Optional[str] = None)

Bases: _DividerBase

Material Design 3 horizontal divider.

Draws a full-width line to separate content. Only width is exposed; the height (thickness) is derived from the style.

Initialize HorizontalDivider.

Parameters:

Name Type Description Default
width SizingLike

Width sizing override. Defaults to Sizing.weight().

None
padding PaddingLike

Insets from the allocated rect to the divider line.

0
style Optional[DividerStyle]

Optional :class:~nuiitivet.material.styles.divider_style.DividerStyle override. Falls back to the default DividerStyle when None.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/divider.py
def __init__(
    self,
    *,
    width: SizingLike = None,
    padding: PaddingLike = 0,
    style: Optional[DividerStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize HorizontalDivider.

    Args:
        width: Width sizing override. Defaults to ``Sizing.weight()``.
        padding: Insets from the allocated rect to the divider line.
        style: Optional :class:`~nuiitivet.material.styles.divider_style.DividerStyle`
            override. Falls back to the default ``DividerStyle`` when ``None``.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    _pad_l, pad_t, _pad_r, pad_b = parse_padding(padding)
    resolved_width: SizingLike = Sizing.weight() if width is None else width
    super().__init__(
        orientation="horizontal",
        width=resolved_width,
        height=_cross_axis_thickness(style, pad_t, pad_b),
        padding=padding,
        style=style,
        key=key,
    )

VerticalDivider

VerticalDivider(*, height: SizingLike = None, padding: PaddingLike = 0, style: Optional[DividerStyle] = None, key: Optional[str] = None)

Bases: _DividerBase

Material Design 3 vertical divider.

Draws a full-height line to separate content. Only height is exposed; the width (thickness) is derived from the style.

Initialize VerticalDivider.

Parameters:

Name Type Description Default
height SizingLike

Height sizing override. Defaults to Sizing.weight().

None
padding PaddingLike

Insets from the allocated rect to the divider line.

0
style Optional[DividerStyle]

Optional :class:~nuiitivet.material.styles.divider_style.DividerStyle override. Falls back to the default DividerStyle when None.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/divider.py
def __init__(
    self,
    *,
    height: SizingLike = None,
    padding: PaddingLike = 0,
    style: Optional[DividerStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize VerticalDivider.

    Args:
        height: Height sizing override. Defaults to ``Sizing.weight()``.
        padding: Insets from the allocated rect to the divider line.
        style: Optional :class:`~nuiitivet.material.styles.divider_style.DividerStyle`
            override. Falls back to the default ``DividerStyle`` when ``None``.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    pad_l, _pad_t, pad_r, _pad_b = parse_padding(padding)
    resolved_height: SizingLike = Sizing.weight() if height is None else height
    super().__init__(
        orientation="vertical",
        width=_cross_axis_thickness(style, pad_l, pad_r),
        height=resolved_height,
        padding=padding,
        style=style,
        key=key,
    )

Button

Button(label: str | ReadOnlyObservableProtocol[str] | None = None, icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None = None, *, on_click: Optional[VoidCallback] = None, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = None, padding: PaddingLike = None, style: Optional[ButtonStyle] = None, key: Optional[str] = None)

Bases: MaterialButtonBase

Unified Material Design 3 button.

The visual variant (filled, outlined, text, elevated, tonal) and the M3 size preset ("xs".."xl") are both expressed through the style argument, which accepts any :class:ButtonStyle instance. Use the :class:ButtonStyle factory methods to obtain variant presets: ButtonStyle.filled("s"), ButtonStyle.outlined("m") and so on.

When style is not provided, :meth:ButtonStyle.filled with size "s" is used as the default.

Initialize Button.

The height is MD3-fixed by the style's size variant, so it is not a constructor parameter; select it through style (e.g. ButtonStyle.filled("m")) — SIZE_POLICY: MD3 fixes the axis -> style only.

Parameters:

Name Type Description Default
label str | ReadOnlyObservableProtocol[str] | None

Text label for the button.

None
icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None

Icon glyph for the button (Symbol, string, or observable).

None
on_click Optional[VoidCallback]

Callback invoked when the button is clicked.

None
disabled bool | ObservableProtocol[bool]

Whether the button is disabled.

False
width SizingLike

Width specification. Defaults to auto.

None
padding PaddingLike

Insets from the allocated rect to the container.

None
style Optional[ButtonStyle]

Visual style preset. Defaults to ButtonStyle.filled("s").

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/buttons.py
def __init__(
    self,
    label: str | ReadOnlyObservableProtocol[str] | None = None,
    icon: "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str] | None = None,
    *,
    on_click: Optional[VoidCallback] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = None,
    padding: PaddingLike = None,
    style: Optional[ButtonStyle] = None,
    key: Optional[str] = None,
):
    """Initialize Button.

    The height is MD3-fixed by the style's size variant, so it is not a
    constructor parameter; select it through ``style`` (e.g.
    ``ButtonStyle.filled("m")``) — SIZE_POLICY: MD3 fixes the axis -> style
    only.

    Args:
        label: Text label for the button.
        icon: Icon glyph for the button (Symbol, string, or observable).
        on_click: Callback invoked when the button is clicked.
        disabled: Whether the button is disabled.
        width: Width specification. Defaults to auto.
        padding: Insets from the allocated rect to the container.
        style: Visual style preset. Defaults to ``ButtonStyle.filled("s")``.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    effective_style = style if style is not None else ButtonStyle.filled("s")
    #: The style the caller passed, or ``None`` to follow the host or the preset.
    self._user_style: Optional[ButtonStyle] = style
    self._user_padding = padding
    self._user_height = None

    text_color = effective_style.foreground if effective_style else ColorRole.ON_PRIMARY

    child_widget = build_button_child(
        label=label,
        icon=icon,
        foreground=text_color,
        button_height=None,
        style=effective_style,
    )

    params = resolve_button_style_params(effective_style, padding, None, disabled)

    super().__init__(
        child=child_widget,
        on_click=on_click,
        width=width,
        disabled=disabled,
        key=key,
        **params,
    )

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:ButtonStyle's presets.

Source code in src/nuiitivet/material/buttons.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`ButtonStyle`'s presets."""
    return {"style": ButtonStyle}

ExtendedFab

ExtendedFab(label: str | ReadOnlyObservableProtocol[str], *, icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None = None, on_click: Optional[VoidCallback] = None, expanded: bool | ObservableProtocol[bool] = True, disabled: bool | ObservableProtocol[bool] = False, padding: PaddingLike = 0, style: Optional[FabStyle] = None, key: Optional[str] = None)

Bases: _FabBase

Material Design 3 Extended FAB with a collapse/expand state.

A pill-shaped FAB carrying a required label and an optional leading icon. The expanded observable morphs the button between the extended pill (icon + label) and a collapsed circular FAB (icon only). The width is content-driven when expanded and animates down to the circular container footprint when collapsed.

When icon is omitted the collapse is a no-op: the button stays a label pill because there is nothing to show in the circular footprint.

Initialize ExtendedFab.

Parameters:

Name Type Description Default
label str | ReadOnlyObservableProtocol[str]

Required text label (string or observable).

required
icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None

Optional leading icon. When present, the collapsed state shows it as a circular FAB; when absent, collapse is a no-op.

None
on_click Optional[VoidCallback]

Callback invoked when the button is clicked.

None
expanded bool | ObservableProtocol[bool]

Initial expanded state or external observable. True (default) shows the icon + label pill; False collapses to the circular FAB footprint.

True
disabled bool | ObservableProtocol[bool]

Whether the button is disabled.

False
padding PaddingLike

Insets from the allocated rect to the container.

0
style Optional[FabStyle]

FAB style preset selecting the colour mapping and size. Defaults to the theme's FAB style, which itself falls back to :meth:FabStyle.primary (size "s", 56dp).

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/buttons.py
def __init__(
    self,
    label: str | ReadOnlyObservableProtocol[str],
    *,
    icon: "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str] | None = None,
    on_click: Optional[VoidCallback] = None,
    expanded: bool | ObservableProtocol[bool] = True,
    disabled: bool | ObservableProtocol[bool] = False,
    padding: PaddingLike = 0,
    style: Optional[FabStyle] = None,
    key: Optional[str] = None,
):
    """Initialize ExtendedFab.

    Args:
        label: Required text label (string or observable).
        icon: Optional leading icon.  When present, the collapsed state
            shows it as a circular FAB; when absent, collapse is a no-op.
        on_click: Callback invoked when the button is clicked.
        expanded: Initial expanded state or external observable.  ``True``
            (default) shows the icon + label pill; ``False`` collapses to
            the circular FAB footprint.
        disabled: Whether the button is disabled.
        padding: Insets from the allocated rect to the container.
        style: FAB style preset selecting the colour mapping and size.
            Defaults to the theme's FAB style, which itself falls back to
            :meth:`FabStyle.primary` (size ``"s"``, 56dp).
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    # See ``Fab.__init__``: the preset stands in until the first measure can
    # reach the theme.
    base_style = style if style is not None else FabStyle.default()
    effective_style = self._adapt_style(base_style)
    self._container_height = effective_style.container_height
    ext = EXTENDED_FAB_SIZE_TOKENS[_fab_size_from_height(base_style.container_height)]

    self._user_style = style
    self._user_padding = padding
    pl, pt, pr, pb = parse_padding(padding if padding is not None else 0)
    # The pill height is MD3-fixed, so the fixed sizing carries the padding.
    self._user_height = self._container_height + pt + pb
    self._has_icon = icon is not None

    self._expanded_external: ObservableProtocol[bool] | None = None
    if hasattr(expanded, "subscribe") and hasattr(expanded, "value"):
        self._expanded_external = cast("ObservableProtocol[bool]", expanded)
        self._expanded_internal = bool(self._expanded_external.value)
    else:
        self._expanded_internal = bool(expanded)

    text_color = effective_style.foreground if effective_style else ColorRole.ON_PRIMARY_CONTAINER

    child_widget = build_button_child(
        label,
        icon,
        foreground=text_color,
        button_height=self._container_height,
        icon_position="leading",
        spacing=ext["icon_label_space"],
        style=effective_style,
    )

    params = resolve_button_style_params(effective_style, padding, self._user_height, disabled)

    super().__init__(
        child=child_widget,
        on_click=on_click,
        width=None,
        disabled=disabled,
        key=key,
        **params,
    )
    # Clip the label as the container shrinks toward the circular footprint.
    self.clip_content = True
    self._sync_state_tokens(effective_style)
    self._setup_press_scale()

    from nuiitivet.widgets.text import TextBase

    self._label_widget: Optional[Widget] = next(
        (w for w in self._foreground_targets if isinstance(w, TextBase)), None
    )
    self._label_base_rgba: Optional[Tuple[int, int, int, int]] = None

    initial_t = 1.0 if self._effective_expanded() else 0.0
    self._morph_anim: Animatable[float] = Animatable(initial_t, motion=EXPRESSIVE_DEFAULT_SPATIAL)
    self.bind(self._morph_anim.subscribe(lambda _: self._on_morph_tick()))
    self._apply_morph_label_alpha()

expanded property writable

expanded: bool

Return whether the button is currently expanded (pill) state.

Fab

Fab(icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str], *, on_click: Optional[VoidCallback] = None, disabled: bool | ObservableProtocol[bool] = False, padding: PaddingLike = None, style: Optional[FabStyle] = None, key: Optional[str] = None)

Bases: _FabBase

Material Design 3 Floating Action Button (FAB).

Initialize Fab.

Parameters:

Name Type Description Default
icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str]

Icon for the button.

required
on_click Optional[VoidCallback]

Callback to be invoked when the button is clicked.

None
disabled bool | ObservableProtocol[bool]

Whether the button is disabled.

False
padding PaddingLike

Insets from the allocated rect to the container.

None
style Optional[FabStyle]

FAB style preset. Defaults to the theme's FAB style, which itself falls back to :meth:FabStyle.primary (size "s", 56dp). Use FabStyle.primary("m") / FabStyle.primary("l") for the 80dp / 96dp variants, or FabStyle.secondary / FabStyle.tertiary for alternative tonal colour sets.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/buttons.py
def __init__(
    self,
    icon: "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str],
    *,
    on_click: Optional[VoidCallback] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    padding: PaddingLike = None,
    style: Optional[FabStyle] = None,
    key: Optional[str] = None,
):
    """Initialize Fab.

    Args:
        icon: Icon for the button.
        on_click: Callback to be invoked when the button is clicked.
        disabled: Whether the button is disabled.
        padding: Insets from the allocated rect to the container.
        style: FAB style preset.  Defaults to the theme's FAB style, which
            itself falls back to :meth:`FabStyle.primary` (size ``"s"``,
            56dp).  Use ``FabStyle.primary("m")`` / ``FabStyle.primary("l")``
            for the 80dp / 96dp variants, or ``FabStyle.secondary`` /
            ``FabStyle.tertiary`` for alternative tonal colour sets.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    # Held in a local for everything below ``super().__init__()``: reads that
    # run before the widget is attached must not go through ``self.style``,
    # which cannot reach the theme yet. The preset is what
    # ``FabStyle.from_theme`` falls back to, so an unthemed app sees no
    # change; a themed one adopts its style on the first measure.
    effective_style: FabStyle = style if style is not None else FabStyle.default()
    self._user_style = style
    self._user_padding = padding
    size = effective_style.container_height
    # The container is MD3-fixed, so the fixed sizing carries the padding
    # band as well: allocated = container + padding.
    pl, pt, pr, pb = parse_padding(padding if padding is not None else 0)
    self._user_height = size + pt + pb

    text_color = effective_style.foreground if effective_style else ColorRole.ON_PRIMARY_CONTAINER

    child_widget = build_button_child(
        None,
        icon,
        foreground=text_color,
        button_height=size,
        style=effective_style,
    )

    params = resolve_button_style_params(effective_style, padding, self._user_height, disabled)

    super().__init__(
        child=child_widget,
        on_click=on_click,
        width=size + pl + pr,
        disabled=disabled,
        key=key,
        **params,
    )
    self._sync_state_tokens(effective_style)
    self._setup_press_scale()

IconButton

IconButton(icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str], *, on_click: Optional[VoidCallback] = None, disabled: bool | ObservableProtocol[bool] = False, padding: PaddingLike = 0, style: Optional[ButtonStyle] = None, key: Optional[str] = None)

Bases: MaterialButtonBase

Material icon-only action button driven by style presets.

Initialize IconButton.

Parameters:

Name Type Description Default
icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str]

Icon glyph source.

required
on_click Optional[VoidCallback]

Callback invoked when the button is clicked.

None
disabled bool | ObservableProtocol[bool]

Whether the button is disabled.

False
padding PaddingLike

Insets from the allocated rect to the container.

0
style Optional[ButtonStyle]

Icon button style preset or custom style. Defaults to :meth:IconButtonStyle.standard (size "s", 40dp). Use size-aware factories such as IconButtonStyle.filled("m") to control container/icon sizing.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/buttons.py
def __init__(
    self,
    icon: "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str],
    *,
    on_click: Optional[VoidCallback] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    padding: PaddingLike = 0,
    style: Optional[ButtonStyle] = None,
    key: Optional[str] = None,
):
    """Initialize IconButton.

    Args:
        icon: Icon glyph source.
        on_click: Callback invoked when the button is clicked.
        disabled: Whether the button is disabled.
        padding: Insets from the allocated rect to the container.
        style: Icon button style preset or custom style.  Defaults to
            :meth:`IconButtonStyle.standard` (size ``"s"``, 40dp).  Use
            size-aware factories such as ``IconButtonStyle.filled("m")``
            to control container/icon sizing.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    effective_style = style if style is not None else IconButtonStyle.default()

    #: The style the caller passed, or ``None`` to follow the host or the preset.
    self._user_style: Optional[ButtonStyle] = style
    self._user_padding = padding
    # The container is MD3-fixed, so the fixed sizing carries the padding
    # band as well: allocated = container + padding.
    pl, pt, pr, pb = parse_padding(padding if padding is not None else 0)
    container = effective_style.container_height
    self._user_height = container + pt + pb

    child_widget = build_button_child(
        label=None,
        icon=icon,
        foreground=effective_style.foreground if effective_style else ColorRole.ON_SURFACE,
        button_height=effective_style.container_height,
        style=effective_style,
    )

    params = resolve_button_style_params(effective_style, padding, self._user_height, disabled)
    super().__init__(
        child=child_widget,
        on_click=on_click,
        width=container + pl + pr,
        disabled=disabled,
        key=key,
        **params,
    )

style property

style: ButtonStyle

Return the style in effect: the caller's, else the host's, else the standard preset.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:IconButtonStyle's presets.

Source code in src/nuiitivet/material/buttons.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`IconButtonStyle`'s presets."""
    return {"style": IconButtonStyle}

IconToggleButton

IconToggleButton(icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str], *, selected: bool | ObservableProtocol[bool] = False, on_change: Optional[BoolCallback] = None, disabled: bool | ObservableProtocol[bool] = False, padding: PaddingLike = 0, style: Optional[IconToggleButtonStyle] = None, key: Optional[str] = None)

Bases: ToggleButtonBase

Material icon-only toggle button driven by state-paired styles.

Initialize IconToggleButton.

Parameters:

Name Type Description Default
icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str]

Icon glyph source.

required
selected bool | ObservableProtocol[bool]

Selected state value or observable.

False
on_change Optional[BoolCallback]

Callback invoked with the new selected state.

None
disabled bool | ObservableProtocol[bool]

Whether the button is disabled.

False
padding PaddingLike

Insets from the allocated rect to the container.

0
style Optional[IconToggleButtonStyle]

Toggle style pair for selected and unselected states. Defaults to the theme's icon toggle button style, which itself falls back to :meth:IconToggleButtonStyle.standard (size "s"). Use size-aware factories such as IconToggleButtonStyle.filled("m") to control sizing.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/buttons.py
def __init__(
    self,
    icon: "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str],
    *,
    selected: bool | ObservableProtocol[bool] = False,
    on_change: Optional[BoolCallback] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    padding: PaddingLike = 0,
    style: Optional[IconToggleButtonStyle] = None,
    key: Optional[str] = None,
):
    """Initialize IconToggleButton.

    Args:
        icon: Icon glyph source.
        selected: Selected state value or observable.
        on_change: Callback invoked with the new selected state.
        disabled: Whether the button is disabled.
        padding: Insets from the allocated rect to the container.
        style: Toggle style pair for selected and unselected states.
            Defaults to the theme's icon toggle button style, which itself
            falls back to :meth:`IconToggleButtonStyle.standard` (size
            ``"s"``).  Use size-aware factories such as
            ``IconToggleButtonStyle.filled("m")`` to control sizing.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    #: The style the caller passed, or ``None`` to follow the theme.
    self._user_toggle_style: Optional[IconToggleButtonStyle] = style
    preset = style if style is not None else IconToggleButtonStyle.default()
    # Both selected/unselected share container_height per factory contract.
    # The container is sized once, from the style available at construction:
    # a theme arriving later restyles the button but does not resize it.
    self._icon_size = preset.unselected.container_height
    # The container is MD3-fixed, so the fixed sizing carries the padding
    # band as well: allocated = container + padding.
    pl, pt, pr, pb = parse_padding(padding if padding is not None else 0)

    super().__init__(
        label=None,
        icon=icon,
        selected=selected,
        on_change=on_change,
        disabled=disabled,
        width=self._icon_size + pl + pr,
        height=self._icon_size + pt + pb,
        padding=padding,
        key=key,
    )

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:IconToggleButtonStyle's presets.

Source code in src/nuiitivet/material/buttons.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`IconToggleButtonStyle`'s presets."""
    return {"style": IconToggleButtonStyle}

ToggleButton

ToggleButton(label: str | ReadOnlyObservableProtocol[str] | None = None, icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None = None, *, selected: bool | ObservableProtocol[bool] = False, on_change: Optional[BoolCallback] = None, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = None, padding: PaddingLike = None, style: Optional[ToggleButtonStyle] = None, key: Optional[str] = None)

Bases: ToggleButtonBase

Unified Material Design 3 toggle button.

Visual variant and size are encoded in a :class:ToggleButtonStyle which carries both unselected- and selected-state colours. Use :meth:ToggleButtonStyle.filled, .outlined, .elevated or .tonal to obtain presets. When style is None, the style defaults to :meth:ToggleButtonStyle.filled at size "s".

Initialize ToggleButton.

The height is MD3-fixed by the style's size variant, so it is not a constructor parameter; select it through style (e.g. ToggleButtonStyle.filled("m")) — SIZE_POLICY: MD3 fixes the axis -> style only.

Parameters:

Name Type Description Default
label str | ReadOnlyObservableProtocol[str] | None

Text label for the button.

None
icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None

Icon glyph for the button.

None
selected bool | ObservableProtocol[bool]

Initial selected state or external observable.

False
on_change Optional[BoolCallback]

Callback invoked with the new selected value.

None
disabled bool | ObservableProtocol[bool]

Whether the button is disabled.

False
width SizingLike

Width specification.

None
padding PaddingLike

Insets from the allocated rect to the container.

None
style Optional[ToggleButtonStyle]

Toggle style preset. Defaults to the theme's toggle button style, which itself falls back to ToggleButtonStyle.filled("s").

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/buttons.py
def __init__(
    self,
    label: str | ReadOnlyObservableProtocol[str] | None = None,
    icon: "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str] | None = None,
    *,
    selected: bool | ObservableProtocol[bool] = False,
    on_change: Optional[BoolCallback] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = None,
    padding: PaddingLike = None,
    style: Optional[ToggleButtonStyle] = None,
    key: Optional[str] = None,
):
    """Initialize ToggleButton.

    The height is MD3-fixed by the style's size variant, so it is not a
    constructor parameter; select it through ``style`` (e.g.
    ``ToggleButtonStyle.filled("m")``) — SIZE_POLICY: MD3 fixes the axis ->
    style only.

    Args:
        label: Text label for the button.
        icon: Icon glyph for the button.
        selected: Initial selected state or external observable.
        on_change: Callback invoked with the new selected value.
        disabled: Whether the button is disabled.
        width: Width specification.
        padding: Insets from the allocated rect to the container.
        style: Toggle style preset. Defaults to the theme's toggle button
            style, which itself falls back to ``ToggleButtonStyle.filled("s")``.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    #: The style the caller passed, or ``None`` to follow the theme.
    self._user_toggle_style: Optional[ToggleButtonStyle] = style

    super().__init__(
        label=label,
        icon=icon,
        selected=selected,
        on_change=on_change,
        disabled=disabled,
        width=width,
        padding=padding,
        key=key,
    )

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:ToggleButtonStyle's presets.

Source code in src/nuiitivet/material/buttons.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`ToggleButtonStyle`'s presets."""
    return {"style": ToggleButtonStyle}

ButtonStyle dataclass

ButtonStyle(background: Optional[ColorSpec] = None, foreground: Optional[ColorSpec] = None, border_color: Optional[ColorSpec] = None, border_width: float = 0.0, corner_radius: int = 20, pressed_corner_radius: Optional[int] = None, container_height: int = 40, content_insets: PaddingLike = (16, 0, 16, 0), spacing: int = 8, min_width: Optional[int] = 64, min_height: Optional[int] = None, label_font_size: int = 14, icon_size: int = 20, elevation: int = 0, overlay_color: Optional[ColorSpec] = None, overlay_alpha: float = 0.0)

Immutable style for the :class:Button widget (M3-compliant).

Use the filled / outlined / text / elevated / tonal factory classmethods (each accepting a :data:ButtonSize) rather than constructing directly where possible.

copy_with

copy_with(**changes) -> _ButtonStyleT

Create a new style instance with specified fields changed.

Returns the caller's own type, so a :class:FabStyle copy is still a :class:FabStyle.

Source code in src/nuiitivet/material/styles/button_style.py
def copy_with(self: _ButtonStyleT, **changes) -> _ButtonStyleT:
    """Create a new style instance with specified fields changed.

    Returns the caller's own type, so a :class:`FabStyle` copy is still a
    :class:`FabStyle`.
    """
    return replace(self, **changes)

resolve_colors

resolve_colors(theme: Theme | None = None) -> dict

Resolve :class:ColorRole entries to concrete RGBA values.

Source code in src/nuiitivet/material/styles/button_style.py
def resolve_colors(self, theme: "Theme | None" = None) -> dict:
    """Resolve :class:`ColorRole` entries to concrete RGBA values."""
    from ...theme.resolver import resolve_color_to_rgba

    return {
        "background": resolve_color_to_rgba(self.background, theme=theme) if self.background else None,
        "foreground": resolve_color_to_rgba(self.foreground, theme=theme) if self.foreground else None,
        "border_color": resolve_color_to_rgba(self.border_color, theme=theme) if self.border_color else None,
        "overlay_color": resolve_color_to_rgba(self.overlay_color, theme=theme) if self.overlay_color else None,
    }

resolve

resolve(theme: Theme | None = None) -> dict

Compatibility resolver returning a dict shaped like the legacy style.

Source code in src/nuiitivet/material/styles/button_style.py
def resolve(self, theme: "Theme | None" = None) -> dict:
    """Compatibility resolver returning a dict shaped like the legacy style."""
    colors = self.resolve_colors(theme=theme)
    resolved = {
        "background": colors.get("background"),
        "foreground": colors.get("foreground"),
        "border_color": colors.get("border_color"),
        "corner_radius": self.corner_radius,
        "content_insets": self.content_insets,
        "spacing": getattr(self, "spacing", 8),
        "min_size": (self.min_width, self.min_height),
        "text_style": None,
    }

    if self.overlay_color is not None:
        try:
            resolved["overlay"] = (self.overlay_color, float(self.overlay_alpha or 0.0))
        except Exception:
            resolved["overlay"] = None
    else:
        resolved["overlay"] = None

    return resolved

default classmethod

default() -> ButtonStyle

Return the framework default, ignoring any theme.

This is what a button renders with before it is mounted, and what a button without a theme falls back to.

Returns:

Type Description
ButtonStyle

The filled style at size "s".

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def default(cls) -> "ButtonStyle":
    """Return the framework default, ignoring any theme.

    This is what a button renders with before it is mounted, and what a
    button without a theme falls back to.

    Returns:
        The filled style at size ``"s"``.
    """
    return cls.filled("s")

filled classmethod

filled(size: ButtonSize = 's') -> ButtonStyle

Return the filled-variant style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def filled(cls, size: ButtonSize = "s") -> "ButtonStyle":
    """Return the filled-variant style at the given M3 size."""
    t = BUTTON_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.PRIMARY,
        foreground=ColorRole.ON_PRIMARY,
        border_width=0.0,
        corner_radius=t["corner_radius"],
        pressed_corner_radius=t["pressed_corner_radius"],
        container_height=t["container_height"],
        content_insets=_size_content_insets(size),
        spacing=t["icon_label_space"],
        min_width=_size_min_width(size),
        label_font_size=t["label_font_size"],
        icon_size=t["icon_size"],
        elevation=0,
        overlay_color=ColorRole.ON_PRIMARY,
        overlay_alpha=0.08,
    )

outlined classmethod

outlined(size: ButtonSize = 's') -> ButtonStyle

Return the outlined-variant style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def outlined(cls, size: ButtonSize = "s") -> "ButtonStyle":
    """Return the outlined-variant style at the given M3 size."""
    t = BUTTON_SIZE_TOKENS[size]
    return cls(
        background=None,
        foreground=ColorRole.ON_SURFACE_VARIANT,
        border_color=ColorRole.OUTLINE_VARIANT,
        border_width=t["outline_width"],
        corner_radius=t["corner_radius"],
        pressed_corner_radius=t["pressed_corner_radius"],
        container_height=t["container_height"],
        content_insets=_size_content_insets(size),
        spacing=t["icon_label_space"],
        min_width=_size_min_width(size),
        label_font_size=t["label_font_size"],
        icon_size=t["icon_size"],
        elevation=0,
        overlay_color=ColorRole.ON_SURFACE_VARIANT,
        overlay_alpha=0.08,
    )

text classmethod

text(size: ButtonSize = 's') -> ButtonStyle

Return the text-variant style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def text(cls, size: ButtonSize = "s") -> "ButtonStyle":
    """Return the text-variant style at the given M3 size."""
    t = BUTTON_SIZE_TOKENS[size]
    return cls(
        background=None,
        foreground=ColorRole.PRIMARY,
        border_width=0.0,
        corner_radius=t["corner_radius"],
        pressed_corner_radius=t["pressed_corner_radius"],
        container_height=t["container_height"],
        content_insets=_size_content_insets(size),
        spacing=t["icon_label_space"],
        min_width=None,
        label_font_size=t["label_font_size"],
        icon_size=t["icon_size"],
        elevation=0,
        overlay_color=ColorRole.PRIMARY,
        overlay_alpha=0.08,
    )

elevated classmethod

elevated(size: ButtonSize = 's') -> ButtonStyle

Return the elevated-variant style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def elevated(cls, size: ButtonSize = "s") -> "ButtonStyle":
    """Return the elevated-variant style at the given M3 size."""
    t = BUTTON_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.SURFACE_CONTAINER_LOW,
        foreground=ColorRole.PRIMARY,
        border_width=0.0,
        corner_radius=t["corner_radius"],
        pressed_corner_radius=t["pressed_corner_radius"],
        container_height=t["container_height"],
        content_insets=_size_content_insets(size),
        spacing=t["icon_label_space"],
        min_width=_size_min_width(size),
        label_font_size=t["label_font_size"],
        icon_size=t["icon_size"],
        elevation=1,
        overlay_color=ColorRole.PRIMARY,
        overlay_alpha=0.08,
    )

tonal classmethod

tonal(size: ButtonSize = 's') -> ButtonStyle

Return the tonal-variant style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def tonal(cls, size: ButtonSize = "s") -> "ButtonStyle":
    """Return the tonal-variant style at the given M3 size."""
    t = BUTTON_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.SECONDARY_CONTAINER,
        foreground=ColorRole.ON_SECONDARY_CONTAINER,
        border_width=0.0,
        corner_radius=t["corner_radius"],
        pressed_corner_radius=t["pressed_corner_radius"],
        container_height=t["container_height"],
        content_insets=_size_content_insets(size),
        spacing=t["icon_label_space"],
        min_width=_size_min_width(size),
        label_font_size=t["label_font_size"],
        icon_size=t["icon_size"],
        elevation=0,
        overlay_color=ColorRole.ON_SECONDARY_CONTAINER,
        overlay_alpha=0.08,
    )

IconButtonStyle

Preset factories for icon-only button styles.

Each factory accepts a :data:ButtonSize argument that drives container size, icon size, corner radius, and outline width from :data:ICON_BUTTON_SIZE_TOKENS. Defaults to "s" (40dp).

default classmethod

default() -> ButtonStyle

Return the framework default, ignoring any theme.

This is what an icon button renders with before it is mounted, and what an icon button without a theme falls back to.

Returns:

Type Description
ButtonStyle

The standard icon-button style at size "s".

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def default(cls) -> ButtonStyle:
    """Return the framework default, ignoring any theme.

    This is what an icon button renders with before it is mounted, and
    what an icon button without a theme falls back to.

    Returns:
        The standard icon-button style at size ``"s"``.
    """
    return cls.standard("s")

standard classmethod

standard(size: ButtonSize = 's') -> ButtonStyle

Return the standard icon-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def standard(cls, size: ButtonSize = "s") -> ButtonStyle:
    """Return the standard icon-button style at the given M3 size."""
    return ButtonStyle(
        background=None,
        foreground=ColorRole.ON_SURFACE_VARIANT,
        border_width=0.0,
        elevation=0,
        overlay_color=ColorRole.ON_SURFACE,
        overlay_alpha=0.12,
        **cls._base(size),
    )

filled classmethod

filled(size: ButtonSize = 's') -> ButtonStyle

Return the filled icon-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def filled(cls, size: ButtonSize = "s") -> ButtonStyle:
    """Return the filled icon-button style at the given M3 size."""
    return ButtonStyle(
        background=ColorRole.PRIMARY,
        foreground=ColorRole.ON_PRIMARY,
        border_width=0.0,
        elevation=0,
        overlay_color=ColorRole.ON_PRIMARY,
        overlay_alpha=0.12,
        **cls._base(size),
    )

outlined classmethod

outlined(size: ButtonSize = 's') -> ButtonStyle

Return the outlined icon-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def outlined(cls, size: ButtonSize = "s") -> ButtonStyle:
    """Return the outlined icon-button style at the given M3 size."""
    t = ICON_BUTTON_SIZE_TOKENS[size]
    return ButtonStyle(
        background=None,
        foreground=ColorRole.ON_SURFACE_VARIANT,
        border_color=ColorRole.OUTLINE,
        border_width=t["outline_width"],
        elevation=0,
        overlay_color=ColorRole.ON_SURFACE,
        overlay_alpha=0.12,
        **cls._base(size),
    )

tonal classmethod

tonal(size: ButtonSize = 's') -> ButtonStyle

Return the tonal icon-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def tonal(cls, size: ButtonSize = "s") -> ButtonStyle:
    """Return the tonal icon-button style at the given M3 size."""
    return ButtonStyle(
        background=ColorRole.SECONDARY_CONTAINER,
        foreground=ColorRole.ON_SECONDARY_CONTAINER,
        border_width=0.0,
        elevation=0,
        overlay_color=ColorRole.ON_SECONDARY_CONTAINER,
        overlay_alpha=0.12,
        **cls._base(size),
    )

vibrant classmethod

vibrant(size: ButtonSize = 's') -> ButtonStyle

Return the vibrant icon-button style at the given M3 size.

Intended for use on vibrant containers such as a vibrant toolbar.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def vibrant(cls, size: ButtonSize = "s") -> ButtonStyle:
    """Return the vibrant icon-button style at the given M3 size.

    Intended for use on vibrant containers such as a vibrant toolbar.
    """
    return ButtonStyle(
        background=None,
        foreground=ColorRole.ON_PRIMARY_CONTAINER,
        border_width=0.0,
        elevation=0,
        overlay_color=ColorRole.ON_PRIMARY_CONTAINER,
        overlay_alpha=0.12,
        **cls._base(size),
    )

filled_vibrant classmethod

filled_vibrant(size: ButtonSize = 's') -> ButtonStyle

Return the filled vibrant icon-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def filled_vibrant(cls, size: ButtonSize = "s") -> ButtonStyle:
    """Return the filled vibrant icon-button style at the given M3 size."""
    return ButtonStyle(
        background=ColorRole.PRIMARY,
        foreground=ColorRole.ON_PRIMARY,
        border_width=0.0,
        elevation=0,
        overlay_color=ColorRole.ON_PRIMARY,
        overlay_alpha=0.12,
        **cls._base(size),
    )

outlined_vibrant classmethod

outlined_vibrant(size: ButtonSize = 's') -> ButtonStyle

Return the outlined vibrant icon-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def outlined_vibrant(cls, size: ButtonSize = "s") -> ButtonStyle:
    """Return the outlined vibrant icon-button style at the given M3 size."""
    t = ICON_BUTTON_SIZE_TOKENS[size]
    return ButtonStyle(
        background=None,
        foreground=ColorRole.ON_PRIMARY_CONTAINER,
        border_color=ColorRole.ON_PRIMARY_CONTAINER,
        border_width=t["outline_width"],
        elevation=0,
        overlay_color=ColorRole.ON_PRIMARY_CONTAINER,
        overlay_alpha=0.12,
        **cls._base(size),
    )

tonal_vibrant classmethod

tonal_vibrant(size: ButtonSize = 's') -> ButtonStyle

Return the tonal vibrant icon-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def tonal_vibrant(cls, size: ButtonSize = "s") -> ButtonStyle:
    """Return the tonal vibrant icon-button style at the given M3 size."""
    return ButtonStyle(
        background=ColorRole.SURFACE_CONTAINER_HIGHEST,
        foreground=ColorRole.ON_SURFACE,
        border_width=0.0,
        elevation=0,
        overlay_color=ColorRole.ON_SURFACE,
        overlay_alpha=0.12,
        **cls._base(size),
    )

IconToggleButtonStyle dataclass

IconToggleButtonStyle(selected: ButtonStyle, unselected: ButtonStyle)

State-paired style for icon toggle button widgets.

The unselected style keeps the round container, the selected style the square one; the widget morphs between them.

Presets: :meth:standard, :meth:filled, :meth:outlined, :meth:tonal.

standard classmethod

standard(size: ButtonSize = 's') -> IconToggleButtonStyle

Return styles for the standard icon-toggle button variant.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def standard(cls, size: ButtonSize = "s") -> "IconToggleButtonStyle":
    """Return styles for the standard icon-toggle button variant."""
    base = IconButtonStyle._base(size)
    return cls(
        selected=_icon_selected_shape(
            ButtonStyle(
                background=ColorRole.SECONDARY_CONTAINER,
                foreground=ColorRole.ON_SECONDARY_CONTAINER,
                border_width=0.0,
                elevation=0,
                overlay_color=ColorRole.ON_SECONDARY_CONTAINER,
                overlay_alpha=0.12,
                **base,
            ),
            size,
        ),
        unselected=IconButtonStyle.standard(size),
    )

filled classmethod

filled(size: ButtonSize = 's') -> IconToggleButtonStyle

Return styles for the filled icon-toggle button variant.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def filled(cls, size: ButtonSize = "s") -> "IconToggleButtonStyle":
    """Return styles for the filled icon-toggle button variant."""
    base = IconButtonStyle._base(size)
    return cls(
        selected=_icon_selected_shape(IconButtonStyle.filled(size), size),
        unselected=ButtonStyle(
            background=ColorRole.SURFACE_CONTAINER_HIGHEST,
            foreground=ColorRole.ON_SURFACE_VARIANT,
            border_width=0.0,
            elevation=0,
            overlay_color=ColorRole.ON_SURFACE,
            overlay_alpha=0.12,
            **base,
        ),
    )

outlined classmethod

outlined(size: ButtonSize = 's') -> IconToggleButtonStyle

Return styles for the outlined icon-toggle button variant.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def outlined(cls, size: ButtonSize = "s") -> "IconToggleButtonStyle":
    """Return styles for the outlined icon-toggle button variant."""
    t = ICON_BUTTON_SIZE_TOKENS[size]
    base = IconButtonStyle._base(size)
    return cls(
        selected=_icon_selected_shape(
            ButtonStyle(
                background=ColorRole.INVERSE_SURFACE,
                foreground=ColorRole.INVERSE_ON_SURFACE,
                border_color=ColorRole.INVERSE_SURFACE,
                border_width=t["outline_width"],
                elevation=0,
                overlay_color=ColorRole.INVERSE_ON_SURFACE,
                overlay_alpha=0.12,
                **base,
            ),
            size,
        ),
        unselected=IconButtonStyle.outlined(size),
    )

tonal classmethod

tonal(size: ButtonSize = 's') -> IconToggleButtonStyle

Return styles for the tonal icon-toggle button variant.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def tonal(cls, size: ButtonSize = "s") -> "IconToggleButtonStyle":
    """Return styles for the tonal icon-toggle button variant."""
    base = IconButtonStyle._base(size)
    return cls(
        selected=_icon_selected_shape(
            ButtonStyle(
                background=ColorRole.TERTIARY_CONTAINER,
                foreground=ColorRole.ON_TERTIARY_CONTAINER,
                border_width=0.0,
                elevation=0,
                overlay_color=ColorRole.ON_TERTIARY_CONTAINER,
                overlay_alpha=0.12,
                **base,
            ),
            size,
        ),
        unselected=ButtonStyle(
            background=ColorRole.SECONDARY_CONTAINER,
            foreground=ColorRole.ON_SECONDARY_CONTAINER,
            border_width=0.0,
            elevation=0,
            overlay_color=ColorRole.ON_SECONDARY_CONTAINER,
            overlay_alpha=0.12,
            **base,
        ),
    )

default classmethod

default() -> IconToggleButtonStyle

Return the framework default, ignoring any theme.

This is what an icon toggle button renders with before it is mounted, and what :meth:from_theme falls back to when no Material theme is installed.

Returns:

Type Description
IconToggleButtonStyle

The standard icon-toggle style at size "s".

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def default(cls) -> "IconToggleButtonStyle":
    """Return the framework default, ignoring any theme.

    This is what an icon toggle button renders with before it is mounted,
    and what :meth:`from_theme` falls back to when no Material theme is
    installed.

    Returns:
        The standard icon-toggle style at size ``"s"``.
    """
    return cls.standard()

from_theme classmethod

from_theme(theme: Theme) -> IconToggleButtonStyle

Resolve the icon-toggle style from theme.

Parameters:

Name Type Description Default
theme Theme

Theme instance.

required

Returns:

Type Description
IconToggleButtonStyle

Resolved icon-toggle style.

Source code in src/nuiitivet/material/styles/button_style.py
@classmethod
def from_theme(cls, theme: "Theme") -> "IconToggleButtonStyle":
    """Resolve the icon-toggle style from ``theme``.

    Args:
        theme: Theme instance.

    Returns:
        Resolved icon-toggle style.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    theme_data = theme.extension(MaterialThemeData)
    if theme_data is not None:
        return theme_data.icon_toggle_button_style
    return cls.default()

FabStyle dataclass

FabStyle(background: Optional[ColorSpec] = None, foreground: Optional[ColorSpec] = None, border_color: Optional[ColorSpec] = None, border_width: float = 0.0, corner_radius: int = 20, pressed_corner_radius: Optional[int] = None, container_height: int = 40, content_insets: PaddingLike = (16, 0, 16, 0), spacing: int = 8, min_width: Optional[int] = 64, min_height: Optional[int] = None, label_font_size: int = 14, icon_size: int = 20, elevation: int = 0, overlay_color: Optional[ColorSpec] = None, overlay_alpha: float = 0.0, focus_opacity: float = 0.1, hover_opacity: float = 0.08, pressed_opacity: float = 0.1, focused_elevation: int = 3, hovered_elevation: int = 4, pressed_elevation: int = 3)

Bases: ButtonStyle

Style of the :class:Fab widget, with the field set of :class:ButtonStyle.

Presets, each taking a size: :meth:primary, :meth:secondary, :meth:tertiary (tonal containers), and :meth:primary_solid, :meth:secondary_solid, :meth:tertiary_solid.

primary classmethod

primary(size: FabSize = 's') -> FabStyle

Return the tonal-primary FAB style at the given size.

Source code in src/nuiitivet/material/styles/fab_style.py
@classmethod
def primary(cls, size: FabSize = "s") -> "FabStyle":
    """Return the tonal-primary FAB style at the given size."""
    return cls(
        background=ColorRole.PRIMARY_CONTAINER,
        foreground=ColorRole.ON_PRIMARY_CONTAINER,
        overlay_color=ColorRole.ON_PRIMARY_CONTAINER,
        **cls._base(size),
    )

secondary classmethod

secondary(size: FabSize = 's') -> FabStyle

Return the tonal-secondary FAB style at the given size.

Source code in src/nuiitivet/material/styles/fab_style.py
@classmethod
def secondary(cls, size: FabSize = "s") -> "FabStyle":
    """Return the tonal-secondary FAB style at the given size."""
    return cls(
        background=ColorRole.SECONDARY_CONTAINER,
        foreground=ColorRole.ON_SECONDARY_CONTAINER,
        overlay_color=ColorRole.ON_SECONDARY_CONTAINER,
        **cls._base(size),
    )

tertiary classmethod

tertiary(size: FabSize = 's') -> FabStyle

Return the tonal-tertiary FAB style at the given size.

Source code in src/nuiitivet/material/styles/fab_style.py
@classmethod
def tertiary(cls, size: FabSize = "s") -> "FabStyle":
    """Return the tonal-tertiary FAB style at the given size."""
    return cls(
        background=ColorRole.TERTIARY_CONTAINER,
        foreground=ColorRole.ON_TERTIARY_CONTAINER,
        overlay_color=ColorRole.ON_TERTIARY_CONTAINER,
        **cls._base(size),
    )

primary_solid classmethod

primary_solid(size: FabSize = 's') -> FabStyle

Return the solid-primary FAB style at the given size.

Source code in src/nuiitivet/material/styles/fab_style.py
@classmethod
def primary_solid(cls, size: FabSize = "s") -> "FabStyle":
    """Return the solid-primary FAB style at the given size."""
    return cls(
        background=ColorRole.PRIMARY,
        foreground=ColorRole.ON_PRIMARY,
        overlay_color=ColorRole.ON_PRIMARY,
        **cls._base(size),
    )

secondary_solid classmethod

secondary_solid(size: FabSize = 's') -> FabStyle

Return the solid-secondary FAB style at the given size.

Source code in src/nuiitivet/material/styles/fab_style.py
@classmethod
def secondary_solid(cls, size: FabSize = "s") -> "FabStyle":
    """Return the solid-secondary FAB style at the given size."""
    return cls(
        background=ColorRole.SECONDARY,
        foreground=ColorRole.ON_SECONDARY,
        overlay_color=ColorRole.ON_SECONDARY,
        **cls._base(size),
    )

tertiary_solid classmethod

tertiary_solid(size: FabSize = 's') -> FabStyle

Return the solid-tertiary FAB style at the given size.

Source code in src/nuiitivet/material/styles/fab_style.py
@classmethod
def tertiary_solid(cls, size: FabSize = "s") -> "FabStyle":
    """Return the solid-tertiary FAB style at the given size."""
    return cls(
        background=ColorRole.TERTIARY,
        foreground=ColorRole.ON_TERTIARY,
        overlay_color=ColorRole.ON_TERTIARY,
        **cls._base(size),
    )

default classmethod

default() -> FabStyle

Return the framework default, ignoring any theme.

This is what a FAB renders with before it is mounted, and what :meth:from_theme falls back to when no Material theme is installed.

Returns:

Type Description
FabStyle

The tonal-primary FAB style at size "s".

Source code in src/nuiitivet/material/styles/fab_style.py
@classmethod
def default(cls) -> "FabStyle":
    """Return the framework default, ignoring any theme.

    This is what a FAB renders with before it is mounted, and what
    :meth:`from_theme` falls back to when no Material theme is installed.

    Returns:
        The tonal-primary FAB style at size ``"s"``.
    """
    return cls.primary()

from_theme classmethod

from_theme(theme: Theme) -> FabStyle

Resolve the FAB style from theme.

Parameters:

Name Type Description Default
theme Theme

Theme instance.

required

Returns:

Type Description
FabStyle

Resolved FAB style.

Source code in src/nuiitivet/material/styles/fab_style.py
@classmethod
def from_theme(cls, theme: "Theme") -> "FabStyle":
    """Resolve the FAB style from ``theme``.

    Args:
        theme: Theme instance.

    Returns:
        Resolved FAB style.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    theme_data = theme.extension(MaterialThemeData)
    if theme_data is not None:
        return theme_data.fab_style
    return cls.default()

ToggleButtonStyle dataclass

ToggleButtonStyle(container_height: int = 40, corner_radius: int = 20, selected_corner_radius: int = 12, pressed_corner_radius: Optional[int] = 8, content_insets: PaddingLike = (16, 0, 16, 0), spacing: int = 8, min_width: Optional[int] = 64, min_height: Optional[int] = None, label_font_size: int = 14, icon_size: int = 20, border_width: float = 0.0, elevation: int = 0, unselected_background: Optional[ColorSpec] = None, unselected_foreground: Optional[ColorSpec] = None, unselected_border_color: Optional[ColorSpec] = None, unselected_overlay_color: Optional[ColorSpec] = None, unselected_overlay_alpha: float = 0.08, selected_background: Optional[ColorSpec] = None, selected_foreground: Optional[ColorSpec] = None, selected_border_color: Optional[ColorSpec] = None, selected_overlay_color: Optional[ColorSpec] = None, selected_overlay_alpha: float = 0.08)

Immutable style for :class:ToggleButton (M3-compliant).

Stores a single flat set of size tokens, the corner radius of each shape the container takes, and two paired colour groups: unselected_* and selected_*. The :meth:for_selected helper projects the style into a :class:ButtonStyle for the active state, so the widget internals can reuse the normal Button machinery.

Presets: :meth:filled, :meth:outlined, :meth:elevated, :meth:tonal.

copy_with

copy_with(**changes) -> 'ToggleButtonStyle'

Create a new style instance with specified fields changed.

Source code in src/nuiitivet/material/styles/toggle_button_style.py
def copy_with(self, **changes) -> "ToggleButtonStyle":
    """Create a new style instance with specified fields changed."""
    return replace(self, **changes)

for_selected

for_selected(selected: bool) -> ButtonStyle

Project this style into a :class:ButtonStyle for the given state.

Parameters:

Name Type Description Default
selected bool

When True, project the selected-state colours; otherwise use unselected-state colours.

required
Source code in src/nuiitivet/material/styles/toggle_button_style.py
def for_selected(self, selected: bool) -> ButtonStyle:
    """Project this style into a :class:`ButtonStyle` for the given state.

    Args:
        selected: When ``True``, project the selected-state colours;
            otherwise use unselected-state colours.
    """
    if selected:
        return ButtonStyle(
            background=self.selected_background,
            foreground=self.selected_foreground,
            border_color=self.selected_border_color,
            border_width=self.border_width,
            corner_radius=self.selected_corner_radius,
            pressed_corner_radius=self.pressed_corner_radius,
            container_height=self.container_height,
            content_insets=self.content_insets,
            spacing=self.spacing,
            min_width=self.min_width,
            min_height=self.min_height,
            label_font_size=self.label_font_size,
            icon_size=self.icon_size,
            elevation=self.elevation,
            overlay_color=self.selected_overlay_color,
            overlay_alpha=self.selected_overlay_alpha,
        )
    return ButtonStyle(
        background=self.unselected_background,
        foreground=self.unselected_foreground,
        border_color=self.unselected_border_color,
        border_width=self.border_width,
        corner_radius=self.corner_radius,
        pressed_corner_radius=self.pressed_corner_radius,
        container_height=self.container_height,
        content_insets=self.content_insets,
        spacing=self.spacing,
        min_width=self.min_width,
        min_height=self.min_height,
        label_font_size=self.label_font_size,
        icon_size=self.icon_size,
        elevation=self.elevation,
        overlay_color=self.unselected_overlay_color,
        overlay_alpha=self.unselected_overlay_alpha,
    )

filled classmethod

filled(size: ButtonSize = 's') -> 'ToggleButtonStyle'

Return the filled toggle-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/toggle_button_style.py
@classmethod
def filled(cls, size: ButtonSize = "s") -> "ToggleButtonStyle":
    """Return the filled toggle-button style at the given M3 size."""
    t = BUTTON_SIZE_TOKENS[size]
    return cls(
        container_height=t["container_height"],
        corner_radius=t["corner_radius"],
        selected_corner_radius=t["square_corner_radius"],
        pressed_corner_radius=t["pressed_corner_radius"],
        content_insets=_size_content_insets(size),
        spacing=t["icon_label_space"],
        min_width=_size_min_width(size),
        label_font_size=t["label_font_size"],
        icon_size=t["icon_size"],
        border_width=0.0,
        elevation=0,
        unselected_background=ColorRole.SURFACE_CONTAINER_HIGHEST,
        unselected_foreground=ColorRole.ON_SURFACE_VARIANT,
        unselected_border_color=None,
        unselected_overlay_color=ColorRole.ON_SURFACE_VARIANT,
        unselected_overlay_alpha=0.08,
        selected_background=ColorRole.PRIMARY,
        selected_foreground=ColorRole.ON_PRIMARY,
        selected_border_color=None,
        selected_overlay_color=ColorRole.ON_PRIMARY,
        selected_overlay_alpha=0.08,
    )

outlined classmethod

outlined(size: ButtonSize = 's') -> 'ToggleButtonStyle'

Return the outlined toggle-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/toggle_button_style.py
@classmethod
def outlined(cls, size: ButtonSize = "s") -> "ToggleButtonStyle":
    """Return the outlined toggle-button style at the given M3 size."""
    t = BUTTON_SIZE_TOKENS[size]
    return cls(
        container_height=t["container_height"],
        corner_radius=t["corner_radius"],
        selected_corner_radius=t["square_corner_radius"],
        pressed_corner_radius=t["pressed_corner_radius"],
        content_insets=_size_content_insets(size),
        spacing=t["icon_label_space"],
        min_width=_size_min_width(size),
        label_font_size=t["label_font_size"],
        icon_size=t["icon_size"],
        border_width=t["outline_width"],
        elevation=0,
        unselected_background=None,
        unselected_foreground=ColorRole.ON_SURFACE_VARIANT,
        unselected_border_color=ColorRole.OUTLINE_VARIANT,
        unselected_overlay_color=ColorRole.ON_SURFACE_VARIANT,
        unselected_overlay_alpha=0.08,
        selected_background=ColorRole.INVERSE_SURFACE,
        selected_foreground=ColorRole.INVERSE_ON_SURFACE,
        selected_border_color=ColorRole.INVERSE_SURFACE,
        selected_overlay_color=ColorRole.INVERSE_ON_SURFACE,
        selected_overlay_alpha=0.08,
    )

elevated classmethod

elevated(size: ButtonSize = 's') -> 'ToggleButtonStyle'

Return the elevated toggle-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/toggle_button_style.py
@classmethod
def elevated(cls, size: ButtonSize = "s") -> "ToggleButtonStyle":
    """Return the elevated toggle-button style at the given M3 size."""
    t = BUTTON_SIZE_TOKENS[size]
    return cls(
        container_height=t["container_height"],
        corner_radius=t["corner_radius"],
        selected_corner_radius=t["square_corner_radius"],
        pressed_corner_radius=t["pressed_corner_radius"],
        content_insets=_size_content_insets(size),
        spacing=t["icon_label_space"],
        min_width=_size_min_width(size),
        label_font_size=t["label_font_size"],
        icon_size=t["icon_size"],
        border_width=0.0,
        elevation=1,
        unselected_background=ColorRole.SURFACE_CONTAINER_LOW,
        unselected_foreground=ColorRole.PRIMARY,
        unselected_border_color=None,
        unselected_overlay_color=ColorRole.PRIMARY,
        unselected_overlay_alpha=0.08,
        selected_background=ColorRole.PRIMARY,
        selected_foreground=ColorRole.ON_PRIMARY,
        selected_border_color=None,
        selected_overlay_color=ColorRole.ON_PRIMARY,
        selected_overlay_alpha=0.08,
    )

tonal classmethod

tonal(size: ButtonSize = 's') -> 'ToggleButtonStyle'

Return the tonal toggle-button style at the given M3 size.

Source code in src/nuiitivet/material/styles/toggle_button_style.py
@classmethod
def tonal(cls, size: ButtonSize = "s") -> "ToggleButtonStyle":
    """Return the tonal toggle-button style at the given M3 size."""
    t = BUTTON_SIZE_TOKENS[size]
    return cls(
        container_height=t["container_height"],
        corner_radius=t["corner_radius"],
        selected_corner_radius=t["square_corner_radius"],
        pressed_corner_radius=t["pressed_corner_radius"],
        content_insets=_size_content_insets(size),
        spacing=t["icon_label_space"],
        min_width=_size_min_width(size),
        label_font_size=t["label_font_size"],
        icon_size=t["icon_size"],
        border_width=0.0,
        elevation=0,
        unselected_background=ColorRole.SECONDARY_CONTAINER,
        unselected_foreground=ColorRole.ON_SECONDARY_CONTAINER,
        unselected_border_color=None,
        unselected_overlay_color=ColorRole.ON_SECONDARY_CONTAINER,
        unselected_overlay_alpha=0.08,
        selected_background=ColorRole.SECONDARY,
        selected_foreground=ColorRole.ON_SECONDARY,
        selected_border_color=None,
        selected_overlay_color=ColorRole.ON_SECONDARY,
        selected_overlay_alpha=0.08,
    )

default classmethod

default() -> 'ToggleButtonStyle'

Return the framework default, ignoring any theme.

This is what a toggle button renders with before it is mounted, and what :meth:from_theme falls back to when no Material theme is installed.

Returns:

Type Description
'ToggleButtonStyle'

The filled toggle-button style at size "s".

Source code in src/nuiitivet/material/styles/toggle_button_style.py
@classmethod
def default(cls) -> "ToggleButtonStyle":
    """Return the framework default, ignoring any theme.

    This is what a toggle button renders with before it is mounted, and
    what :meth:`from_theme` falls back to when no Material theme is
    installed.

    Returns:
        The filled toggle-button style at size ``"s"``.
    """
    return cls.filled("s")

from_theme classmethod

from_theme(theme: 'Theme') -> 'ToggleButtonStyle'

Resolve the toggle-button style from theme.

Parameters:

Name Type Description Default
theme 'Theme'

Theme instance.

required

Returns:

Type Description
'ToggleButtonStyle'

Resolved toggle-button style.

Source code in src/nuiitivet/material/styles/toggle_button_style.py
@classmethod
def from_theme(cls, theme: "Theme") -> "ToggleButtonStyle":
    """Resolve the toggle-button style from ``theme``.

    Args:
        theme: Theme instance.

    Returns:
        Resolved toggle-button style.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    theme_data = theme.extension(MaterialThemeData)
    if theme_data is not None:
        return theme_data.toggle_button_style
    return cls.default()

Card

Card(child: ChildSpec, *, width: SizingLike = None, height: SizingLike = None, padding: PaddingLike = 0, alignment: AlignmentLike = 'start', style: Optional[CardStyle] = None, key: Optional[str] = None)

Bases: ComposableWidget, Box

Unified Material Design 3 card.

The visual variant (filled, outlined, elevated) is expressed entirely through the style argument, which accepts any :class:CardStyle instance. Use the :class:CardStyle factory methods to obtain variant presets: CardStyle.filled(), CardStyle.outlined(), CardStyle.elevated().

When style is not provided, the theme's filled card style is used as the default.

Initialize Card.

Parameters:

Name Type Description Default
child ChildSpec

The child widget or factory.

required
width SizingLike

Width specification.

None
height SizingLike

Height specification.

None
padding PaddingLike

Padding around the content.

0
alignment AlignmentLike

Alignment of the content.

'start'
style Optional[CardStyle]

Visual style preset. Defaults to the theme's filled card style. Use :meth:CardStyle.filled, :meth:CardStyle.outlined, or :meth:CardStyle.elevated for the standard M3 variants.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/card.py
def __init__(
    self,
    child: ChildSpec,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: PaddingLike = 0,
    alignment: AlignmentLike = "start",
    style: Optional[CardStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize Card.

    Args:
        child: The child widget or factory.
        width: Width specification.
        height: Height specification.
        padding: Padding around the content.
        alignment: Alignment of the content.
        style: Visual style preset. Defaults to the theme's filled card
            style. Use :meth:`CardStyle.filled`, :meth:`CardStyle.outlined`,
            or :meth:`CardStyle.elevated` for the standard M3 variants.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._child_spec: ChildSpec = child
    self._user_style: Optional[CardStyle] = style

    # The theme is unreachable here: the widget has no parent link until it
    # is attached, so ``Theme.of`` would return the light default and freeze
    # it forever. Start from the framework default -- the same value
    # ``CardStyle.from_theme`` falls back to when no Material theme is
    # installed -- and re-apply the theme's style once mounted.
    initial_style = style if style is not None else CardStyle.default()
    self._effective_style: CardStyle = initial_style
    _shadows = elevation_shadows(initial_style.elevation)

    # Pass raw colors to Box; it will resolve them lazily via BackgroundRenderer
    super().__init__(
        child=None,
        width=width,
        height=height,
        padding=padding,
        background_color=initial_style.background,
        border_width=initial_style.border_width,
        border_color=initial_style.border_color,
        corner_radius=initial_style.border_radius,
        shadows=_shadows,
        alignment=alignment,
        key=key,
    )

    self._content_scope_id: Optional[str] = None

    if isinstance(child, Widget):
        super().add_child(child)

style property

style: CardStyle

Return the style currently in effect.

This is the explicit style when one was given, otherwise the theme's filled card style — pushed in by :meth:on_mount and kept current by the theme subscription. It is not pulled from Theme.of, which cannot answer before the card is attached.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:CardStyle's presets.

Source code in src/nuiitivet/material/card.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`CardStyle`'s presets."""
    return {"style": CardStyle}

CardStyle dataclass

CardStyle(background: Optional[ColorSpec] = None, border_color: Optional[ColorSpec] = None, border_width: float = 0.0, border_radius: Union[float, tuple[float, float, float, float]] = 12.0, elevation: int = 0)

Immutable style for Card widgets (M3-compliant).

Presets: :meth:elevated, :meth:filled, :meth:outlined.

copy_with

copy_with(**changes) -> CardStyle

Create a new style instance with specified fields changed.

Source code in src/nuiitivet/material/styles/card_style.py
def copy_with(self, **changes) -> "CardStyle":
    """Create a new style instance with specified fields changed."""
    return replace(self, **changes)

resolve_colors

resolve_colors(theme: Theme | None = None) -> dict

Resolve ColorRole to concrete color values.

Source code in src/nuiitivet/material/styles/card_style.py
def resolve_colors(self, theme: "Theme | None" = None) -> dict:
    """Resolve ColorRole to concrete color values."""
    from ...theme.resolver import resolve_color_to_rgba

    return {
        "background": resolve_color_to_rgba(self.background, theme=theme) if self.background else None,
        "border_color": resolve_color_to_rgba(self.border_color, theme=theme) if self.border_color else None,
    }

default classmethod

default() -> CardStyle

Return the framework default, ignoring any theme.

This is what a card renders with before it is mounted, and what :meth:from_theme falls back to when no Material theme is installed.

Returns:

Type Description
CardStyle

The filled card style.

Source code in src/nuiitivet/material/styles/card_style.py
@classmethod
def default(cls) -> "CardStyle":
    """Return the framework default, ignoring any theme.

    This is what a card renders with before it is mounted, and what
    :meth:`from_theme` falls back to when no Material theme is installed.

    Returns:
        The filled card style.
    """
    return cls.filled()

elevated classmethod

elevated() -> CardStyle

Create a default style for an elevated card.

Source code in src/nuiitivet/material/styles/card_style.py
@classmethod
def elevated(cls) -> "CardStyle":
    """Create a default style for an elevated card."""
    return cls(
        background=ColorRole.SURFACE_CONTAINER_LOW,
        elevation=1,  # MD3 level 1 = 1 dp
        border_radius=12.0,
    )

filled classmethod

filled() -> CardStyle

Create a default style for a filled card.

Source code in src/nuiitivet/material/styles/card_style.py
@classmethod
def filled(cls) -> "CardStyle":
    """Create a default style for a filled card."""
    return cls(
        background=ColorRole.SURFACE_CONTAINER_HIGHEST,
        elevation=0,
        border_radius=12.0,
    )

outlined classmethod

outlined() -> CardStyle

Create a default style for an outlined card.

Source code in src/nuiitivet/material/styles/card_style.py
@classmethod
def outlined(cls) -> "CardStyle":
    """Create a default style for an outlined card."""
    return cls(
        background=ColorRole.SURFACE,
        elevation=0,
        border_width=1.0,
        border_color=ColorRole.OUTLINE,
        border_radius=12.0,
    )

from_theme classmethod

from_theme(theme: Theme) -> CardStyle

Resolve the default :class:CardStyle for the given theme.

Returns the theme's filled card style if a Material theme extension is present, otherwise a fresh :meth:filled preset.

Source code in src/nuiitivet/material/styles/card_style.py
@classmethod
def from_theme(cls, theme: "Theme") -> "CardStyle":
    """Resolve the default :class:`CardStyle` for the given theme.

    Returns the theme's filled card style if a Material theme extension
    is present, otherwise a fresh :meth:`filled` preset.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    theme_data = theme.extension(MaterialThemeData)
    if theme_data:
        return theme_data.filled_card_style
    return cls.default()

AssistChip

AssistChip(label: str | ReadOnlyObservableProtocol[str], *, leading_icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None = None, on_click: Optional[Callable[[], None]] = None, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = None, padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None, style: Optional['ChipStyle'] = None, key: Optional[str] = None)

Bases: MaterialChipBase

Material Design 3 Assist Chip widget.

Initialize AssistChip.

Parameters:

Name Type Description Default
label str | ReadOnlyObservableProtocol[str]

Chip label.

required
leading_icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None

Optional leading icon.

None
on_click Optional[Callable[[], None]]

Click callback.

None
disabled bool | ObservableProtocol[bool]

Disabled flag.

False
width SizingLike

Width sizing.

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

Insets from the allocated rect to the container.

None
style Optional['ChipStyle']

Optional chip style.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/chip.py
def __init__(
    self,
    label: str | ReadOnlyObservableProtocol[str],
    *,
    leading_icon: (
        "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str] | None
    ) = None,
    on_click: Optional[Callable[[], None]] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = None,
    padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None,
    style: Optional["ChipStyle"] = None,
    key: Optional[str] = None,
):
    """Initialize AssistChip.

    Args:
        label: Chip label.
        leading_icon: Optional leading icon.
        on_click: Click callback.
        disabled: Disabled flag.
        width: Width sizing.
        padding: Insets from the allocated rect to the container.
        style: Optional chip style.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._label = label
    self._leading_icon = leading_icon

    super().__init__(
        on_click=on_click,
        disabled=disabled,
        width=width,
        padding=padding,
        style=style,
        key=key,
    )

FilterChip

FilterChip(label: str | ReadOnlyObservableProtocol[str], *, selected: bool | ObservableProtocol[bool] = False, on_selected_change: Optional[Callable[[bool], None]] = None, leading_icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None = None, on_click: Optional[Callable[[], None]] = None, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = None, padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None, style: Optional['ChipStyle'] = None, key: Optional[str] = None)

Bases: MaterialChipBase

Material Design 3 Filter Chip widget.

Initialize FilterChip.

Parameters:

Name Type Description Default
label str | ReadOnlyObservableProtocol[str]

Chip label.

required
selected bool | ObservableProtocol[bool]

Selected state source.

False
on_selected_change Optional[Callable[[bool], None]]

Callback when selected state changes.

None
leading_icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None

Optional leading icon in unselected state.

None
on_click Optional[Callable[[], None]]

Additional click callback.

None
disabled bool | ObservableProtocol[bool]

Disabled flag.

False
width SizingLike

Width sizing.

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

Insets from the allocated rect to the container.

None
style Optional['ChipStyle']

Optional chip style.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/chip.py
def __init__(
    self,
    label: str | ReadOnlyObservableProtocol[str],
    *,
    selected: bool | ObservableProtocol[bool] = False,
    on_selected_change: Optional[Callable[[bool], None]] = None,
    leading_icon: (
        "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str] | None
    ) = None,
    on_click: Optional[Callable[[], None]] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = None,
    padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None,
    style: Optional["ChipStyle"] = None,
    key: Optional[str] = None,
):
    """Initialize FilterChip.

    Args:
        label: Chip label.
        selected: Selected state source.
        on_selected_change: Callback when selected state changes.
        leading_icon: Optional leading icon in unselected state.
        on_click: Additional click callback.
        disabled: Disabled flag.
        width: Width sizing.
        padding: Insets from the allocated rect to the container.
        style: Optional chip style.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._selected_external: ObservableProtocol[bool] | None = None
    self._selected = bool(selected)
    self._label = label
    if hasattr(selected, "subscribe") and hasattr(selected, "value"):
        self._selected_external = cast("ObservableProtocol[bool]", selected)
        self._selected = bool(self._selected_external.value)

    self._base_on_click = on_click
    self._on_selected_change = on_selected_change
    self._leading_icon = leading_icon

    super().__init__(
        on_click=self._handle_click,
        disabled=disabled,
        width=width,
        padding=padding,
        style=style,
        key=key,
    )

selected property

selected: bool

Return current selected state.

InputChip

InputChip(label: str | ReadOnlyObservableProtocol[str], *, trailing_icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str], leading_icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None = None, on_trailing_icon_click: Optional[Callable[[], None]] = None, on_click: Optional[Callable[[], None]] = None, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = None, padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None, style: Optional['ChipStyle'] = None, key: Optional[str] = None)

Bases: MaterialChipBase

Material Design 3 Input Chip widget.

Initialize InputChip.

Parameters:

Name Type Description Default
label str | ReadOnlyObservableProtocol[str]

Chip label.

required
trailing_icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str]

Required trailing icon.

required
leading_icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None

Optional leading icon.

None
on_trailing_icon_click Optional[Callable[[], None]]

Callback invoked when trailing icon is pressed.

None
on_click Optional[Callable[[], None]]

Click callback.

None
disabled bool | ObservableProtocol[bool]

Disabled flag.

False
width SizingLike

Width sizing.

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

Insets from the allocated rect to the container.

None
style Optional['ChipStyle']

Optional chip style.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/chip.py
def __init__(
    self,
    label: str | ReadOnlyObservableProtocol[str],
    *,
    trailing_icon: "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str],
    leading_icon: (
        "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str] | None
    ) = None,
    on_trailing_icon_click: Optional[Callable[[], None]] = None,
    on_click: Optional[Callable[[], None]] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = None,
    padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None,
    style: Optional["ChipStyle"] = None,
    key: Optional[str] = None,
):
    """Initialize InputChip.

    Args:
        label: Chip label.
        trailing_icon: Required trailing icon.
        leading_icon: Optional leading icon.
        on_trailing_icon_click: Callback invoked when trailing icon is pressed.
        on_click: Click callback.
        disabled: Disabled flag.
        width: Width sizing.
        padding: Insets from the allocated rect to the container.
        style: Optional chip style.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._label = label
    self._leading_icon = leading_icon
    self._trailing_icon = trailing_icon
    self._on_trailing_icon_click = on_trailing_icon_click
    self._trailing_icon_widget: Optional[Icon] = None
    self._trailing_icon_tap_target: Optional[Widget] = None

    super().__init__(
        on_click=on_click,
        disabled=disabled,
        width=width,
        padding=padding,
        style=style,
        key=key,
    )

SuggestionChip

SuggestionChip(label: str | ReadOnlyObservableProtocol[str], *, leading_icon: 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None = None, on_click: Optional[Callable[[], None]] = None, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = None, padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None, style: Optional['ChipStyle'] = None, key: Optional[str] = None)

Bases: MaterialChipBase

Material Design 3 Suggestion Chip widget.

Initialize SuggestionChip.

Parameters:

Name Type Description Default
label str | ReadOnlyObservableProtocol[str]

Chip label.

required
leading_icon 'Symbol' | str | ReadOnlyObservableProtocol['Symbol'] | ReadOnlyObservableProtocol[str] | None

Optional leading icon.

None
on_click Optional[Callable[[], None]]

Click callback.

None
disabled bool | ObservableProtocol[bool]

Disabled flag.

False
width SizingLike

Width sizing.

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

Insets from the allocated rect to the container.

None
style Optional['ChipStyle']

Optional chip style.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/chip.py
def __init__(
    self,
    label: str | ReadOnlyObservableProtocol[str],
    *,
    leading_icon: (
        "Symbol" | str | ReadOnlyObservableProtocol["Symbol"] | ReadOnlyObservableProtocol[str] | None
    ) = None,
    on_click: Optional[Callable[[], None]] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = None,
    padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None,
    style: Optional["ChipStyle"] = None,
    key: Optional[str] = None,
):
    """Initialize SuggestionChip.

    Args:
        label: Chip label.
        leading_icon: Optional leading icon.
        on_click: Click callback.
        disabled: Disabled flag.
        width: Width sizing.
        padding: Insets from the allocated rect to the container.
        style: Optional chip style.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._label = label
    self._leading_icon = leading_icon

    super().__init__(
        on_click=on_click,
        disabled=disabled,
        width=width,
        padding=padding,
        style=style,
        key=key,
    )

BasicDialog

BasicDialog(title: Optional[Union[str, ReadOnlyObservableProtocol[str]]] = None, message: Optional[Union[str, ReadOnlyObservableProtocol[str]]] = None, *, icon: Optional[Union[str, 'Symbol', ReadOnlyObservableProtocol[str], ReadOnlyObservableProtocol['Symbol']]] = None, actions: Optional[List[Widget]] = None, style: Optional[DialogStyle] = None, width: float = 280.0, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget

Material dialog widget (Basic Dialog).

Displays a modal dialog with optional icon, title, content, and action buttons. Follows Material Design 3 dialog guidelines.

Initialize BasicDialog.

Parameters:

Name Type Description Default
title Optional[Union[str, ReadOnlyObservableProtocol[str]]]

Title text, or an observable of it.

None
message Optional[Union[str, ReadOnlyObservableProtocol[str]]]

Message text, or an observable of it.

None
icon Optional[Union[str, 'Symbol', ReadOnlyObservableProtocol[str], ReadOnlyObservableProtocol['Symbol']]]

Icon shown above the title: a Symbol, an icon name, or an observable of either.

None
actions Optional[List[Widget]]

Action widgets, typically Button with ButtonStyle.text().

None
style Optional[DialogStyle]

Dialog style override; the theme's default when omitted.

None
width float

Container width in dp (MD3 range: 280-560). Defaults to 280.

280.0
padding PaddingLike

Insets from the allocated rect to the dialog surface.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/dialogs.py
def __init__(
    self,
    title: Optional[Union[str, ReadOnlyObservableProtocol[str]]] = None,
    message: Optional[Union[str, ReadOnlyObservableProtocol[str]]] = None,
    *,
    icon: Optional[
        Union[
            str,
            "Symbol",
            ReadOnlyObservableProtocol[str],
            ReadOnlyObservableProtocol["Symbol"],
        ]
    ] = None,
    actions: Optional[List[Widget]] = None,
    style: Optional[DialogStyle] = None,
    width: float = 280.0,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
):
    """Initialize BasicDialog.

    Args:
        title: Title text, or an observable of it.
        message: Message text, or an observable of it.
        icon: Icon shown above the title: a ``Symbol``, an icon name, or
            an observable of either.
        actions: Action widgets, typically ``Button`` with
            ``ButtonStyle.text()``.
        style: Dialog style override; the theme's default when omitted.
        width: Container width in dp (MD3 range: 280-560). Defaults to 280.
        padding: Insets from the allocated rect to the dialog surface.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)
    self.title = title
    self.message = message
    self.icon = icon
    self.actions = actions or []
    self._user_style = style
    self.width = float(width)

style property

style: DialogStyle

Get the resolved dialog style.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:DialogStyle's presets.

Source code in src/nuiitivet/material/dialogs.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`DialogStyle`'s presets."""
    return {"style": DialogStyle}

LoadingIndicator

LoadingIndicator(*, size: int = 48, padding: Optional[Tuple[int, int, int, int] | Tuple[int, int] | int] = 0, style: Optional[LoadingIndicatorStyle] = None, key: Optional[str] = None)

Bases: Widget

M3 Expressive loading indicator.

This widget is intended for short, indeterminate waits.

Initialize the LoadingIndicator.

Parameters:

Name Type Description Default
size int

Outer size of the indicator; sets both width and height.

48
padding Optional[Tuple[int, int, int, int] | Tuple[int, int] | int]

Insets from the allocated rect to the indicator.

0
style Optional[LoadingIndicatorStyle]

Style configuration for appearance and animation.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/loading_indicator.py
def __init__(
    self,
    *,
    size: int = 48,
    padding: Optional[Tuple[int, int, int, int] | Tuple[int, int] | int] = 0,
    style: Optional[LoadingIndicatorStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize the LoadingIndicator.

    Args:
        size: Outer size of the indicator; sets both width and height.
        padding: Insets from the allocated rect to the indicator.
        style: Style configuration for appearance and animation.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    # The indicator is MD3-fixed, so the fixed sizing carries the padding
    # band as well: allocated = indicator + padding.
    pl, pt, pr, pb = parse_padding(padding if padding is not None else 0)
    super().__init__(width=int(size) + pl + pr, height=int(size) + pt + pb, padding=padding, key=key)
    self._size = int(size)
    self._user_style = style

    self._phase_anim: Animatable[float] | None = None
    self._anim_sub: Any = None
    self._loop_timer: Any = None
    self._path: Any = None

style property

style: LoadingIndicatorStyle

Get effective style (user style or theme default).

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:LoadingIndicatorStyle's presets.

Source code in src/nuiitivet/material/loading_indicator.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`LoadingIndicatorStyle`'s presets."""
    return {"style": LoadingIndicatorStyle}

CircularProgressIndicator

CircularProgressIndicator(value: float | ObservableProtocol[float] = 0.0, *, disabled: bool | ObservableProtocol[bool] = False, size: int | None = None, padding: PaddingArg = 0, style: CircularProgressIndicatorStyle | None = None, key: str | None = None)

Bases: _DeterminateProgressBase

Material Design 3 determinate circular progress indicator.

Initialize CircularProgressIndicator.

Parameters:

Name Type Description Default
value float | ObservableProtocol[float]

Progress value in range [0.0, 1.0]. Values are clamped.

0.0
disabled bool | ObservableProtocol[bool]

Disabled state.

False
size int | None

Outer indicator size in dp. Uses style default when omitted.

None
padding PaddingArg

Insets from the allocated rect to the indicator.

0
style CircularProgressIndicatorStyle | None

Optional style override.

None
key str | None

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

None
Source code in src/nuiitivet/material/progress_indicators.py
def __init__(
    self,
    value: float | ObservableProtocol[float] = 0.0,
    *,
    disabled: bool | ObservableProtocol[bool] = False,
    size: int | None = None,
    padding: PaddingArg = 0,
    style: CircularProgressIndicatorStyle | None = None,
    key: str | None = None,
) -> None:
    """Initialize CircularProgressIndicator.

    Args:
        value: Progress value in range ``[0.0, 1.0]``. Values are clamped.
        disabled: Disabled state.
        size: Outer indicator size in dp. Uses style default when omitted.
        padding: Insets from the allocated rect to the indicator.
        style: Optional style override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._style = style
    style_for_layout = style or CircularProgressIndicatorStyle.default()
    self._size = int(size) if size is not None else max(1, int(round(style_for_layout.size)))
    pad_l, pad_t, pad_r, pad_b = parse_padding(padding)
    super().__init__(
        value=value,
        disabled=disabled,
        width=self._size + pad_l + pad_r,
        height=self._size + pad_t + pad_b,
        padding=(pad_l, pad_t, pad_r, pad_b),
        key=key,
    )

style property

Return effective circular progress indicator style.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:CircularProgressIndicatorStyle's presets.

Source code in src/nuiitivet/material/progress_indicators.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`CircularProgressIndicatorStyle`'s presets."""
    return {"style": CircularProgressIndicatorStyle}

IndeterminateCircularProgressIndicator

IndeterminateCircularProgressIndicator(*, disabled: bool | ObservableProtocol[bool] = False, size: int | None = None, padding: PaddingArg = 0, style: CircularProgressIndicatorStyle | None = None, key: str | None = None)

Bases: _IndeterminateProgressBase

Material Design 3 indeterminate circular progress indicator.

Initialize IndeterminateCircularProgressIndicator.

Parameters:

Name Type Description Default
disabled bool | ObservableProtocol[bool]

Disabled state.

False
size int | None

Outer indicator size in dp. Uses style default when omitted.

None
padding PaddingArg

Insets from the allocated rect to the indicator.

0
style CircularProgressIndicatorStyle | None

Optional style override.

None
key str | None

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

None
Source code in src/nuiitivet/material/progress_indicators.py
def __init__(
    self,
    *,
    disabled: bool | ObservableProtocol[bool] = False,
    size: int | None = None,
    padding: PaddingArg = 0,
    style: CircularProgressIndicatorStyle | None = None,
    key: str | None = None,
) -> None:
    """Initialize IndeterminateCircularProgressIndicator.

    Args:
        disabled: Disabled state.
        size: Outer indicator size in dp. Uses style default when omitted.
        padding: Insets from the allocated rect to the indicator.
        style: Optional style override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._style = style
    style_for_motion = style or CircularProgressIndicatorStyle.default()
    self._size = int(size) if size is not None else max(1, int(round(style_for_motion.size)))
    self._animation_motion = LinearMotion(self._animation_motion_duration())
    pad_l, pad_t, pad_r, pad_b = parse_padding(padding)
    super().__init__(
        disabled=disabled,
        width=self._size + pad_l + pad_r,
        height=self._size + pad_t + pad_b,
        padding=(pad_l, pad_t, pad_r, pad_b),
        key=key,
    )

style property

Return effective circular progress indicator style.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:CircularProgressIndicatorStyle's presets.

Source code in src/nuiitivet/material/progress_indicators.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`CircularProgressIndicatorStyle`'s presets."""
    return {"style": CircularProgressIndicatorStyle}

IndeterminateLinearProgressIndicator

IndeterminateLinearProgressIndicator(*, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = 'wt', padding: PaddingArg = 0, style: LinearProgressIndicatorStyle | None = None, key: str | None = None)

Bases: _IndeterminateProgressBase

Material Design 3 indeterminate linear progress indicator.

Initialize IndeterminateLinearProgressIndicator.

Parameters:

Name Type Description Default
disabled bool | ObservableProtocol[bool]

Disabled state.

False
width SizingLike

Width sizing.

'wt'
padding PaddingArg

Insets from the allocated rect to the indicator.

0
style LinearProgressIndicatorStyle | None

Optional style override.

None
key str | None

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

None
Source code in src/nuiitivet/material/progress_indicators.py
def __init__(
    self,
    *,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = "wt",
    padding: PaddingArg = 0,
    style: LinearProgressIndicatorStyle | None = None,
    key: str | None = None,
) -> None:
    """Initialize IndeterminateLinearProgressIndicator.

    Args:
        disabled: Disabled state.
        width: Width sizing.
        padding: Insets from the allocated rect to the indicator.
        style: Optional style override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._style = style
    style_for_layout = style or LinearProgressIndicatorStyle.default()
    self._animation_motion = LinearMotion(self._animation_motion_duration())
    track_h = max(1, int(round(style_for_layout.track_thickness)))
    pad_l, pad_t, pad_r, pad_b = parse_padding(padding)
    super().__init__(
        disabled=disabled,
        width=width,
        height=track_h + pad_t + pad_b,
        padding=(pad_l, pad_t, pad_r, pad_b),
        key=key,
    )

style property

Return effective linear progress indicator style.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:LinearProgressIndicatorStyle's presets.

Source code in src/nuiitivet/material/progress_indicators.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`LinearProgressIndicatorStyle`'s presets."""
    return {"style": LinearProgressIndicatorStyle}

LinearProgressIndicator

LinearProgressIndicator(value: float | ObservableProtocol[float] = 0.0, *, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = 'wt', padding: PaddingArg = 0, style: LinearProgressIndicatorStyle | None = None, key: str | None = None)

Bases: _DeterminateProgressBase

Material Design 3 determinate linear progress indicator.

Initialize LinearProgressIndicator.

Parameters:

Name Type Description Default
value float | ObservableProtocol[float]

Progress value in range [0.0, 1.0]. Values are clamped.

0.0
disabled bool | ObservableProtocol[bool]

Disabled state.

False
width SizingLike

Width sizing.

'wt'
padding PaddingArg

Insets from the allocated rect to the indicator.

0
style LinearProgressIndicatorStyle | None

Optional style override.

None
key str | None

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

None
Source code in src/nuiitivet/material/progress_indicators.py
def __init__(
    self,
    value: float | ObservableProtocol[float] = 0.0,
    *,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = "wt",
    padding: PaddingArg = 0,
    style: LinearProgressIndicatorStyle | None = None,
    key: str | None = None,
) -> None:
    """Initialize LinearProgressIndicator.

    Args:
        value: Progress value in range ``[0.0, 1.0]``. Values are clamped.
        disabled: Disabled state.
        width: Width sizing.
        padding: Insets from the allocated rect to the indicator.
        style: Optional style override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._style = style
    style_for_layout = style or LinearProgressIndicatorStyle.default()
    track_h = max(1, int(round(style_for_layout.track_thickness)))
    pad_l, pad_t, pad_r, pad_b = parse_padding(padding)
    super().__init__(
        value=value,
        disabled=disabled,
        width=width,
        height=track_h + pad_t + pad_b,
        padding=(pad_l, pad_t, pad_r, pad_b),
        key=key,
    )

style property

Return effective linear progress indicator style.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:LinearProgressIndicatorStyle's presets.

Source code in src/nuiitivet/material/progress_indicators.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`LinearProgressIndicatorStyle`'s presets."""
    return {"style": LinearProgressIndicatorStyle}

Menu

Menu(items: list[MenuItem | SubMenuItem | MenuDivider], *, on_dismiss: Callable[[], None] | None = None, style: MenuStyle | None = None, autofocus: bool = True, parent_item: 'SubMenuItem | None' = None, padding: PaddingLike = 0, key: str | None = None)

Bases: InteractiveWidget

Material Design 3 vertical menu popup surface.

The menu is a focus traversal group. Opening a popup moves the focus onto the menu surface, with no item current — nothing is highlighted, as in a desktop menu — unless it was opened from the keyboard, which focuses the first enabled item. Up/Down rove the items (wrapping), Tab/Shift+Tab rove them too (without wrapping) and dismiss the popup once they step past the end, Right/Left walk into and out of a submenu, and Escape dismisses.

An inline menu (one placed in the page rather than shown as an overlay) is a single Tab stop instead: Tab enters it, roves it, and leaves it for the next widget rather than dismissing anything.

Initialize Menu.

Parameters:

Name Type Description Default
items list[MenuItem | SubMenuItem | MenuDivider]

Flat menu entries list.

required
on_dismiss Callable[[], None] | None

Called when menu is dismissed by keyboard.

None
style MenuStyle | None

Optional menu style override.

None
autofocus bool

Whether opening the menu focuses its first enabled item.

True
parent_item 'SubMenuItem | None'

The SubMenuItem this menu expands from, if it is a submenu.

None
padding PaddingLike

Insets from the allocated rect to the menu surface.

0
key str | None

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

None
Source code in src/nuiitivet/material/menu.py
def __init__(
    self,
    items: list[MenuItem | SubMenuItem | MenuDivider],
    *,
    on_dismiss: Callable[[], None] | None = None,
    style: MenuStyle | None = None,
    autofocus: bool = True,
    parent_item: "SubMenuItem | None" = None,
    padding: PaddingLike = 0,
    key: str | None = None,
) -> None:
    """Initialize Menu.

    Args:
        items: Flat menu entries list.
        on_dismiss: Called when menu is dismissed by keyboard.
        style: Optional menu style override.
        autofocus: Whether opening the menu focuses its first enabled item.
        parent_item: The SubMenuItem this menu expands from, if it is a submenu.
        padding: Insets from the allocated rect to the menu surface.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self.items = list(items)
    self.on_dismiss = on_dismiss
    # Held in a local for everything below ``super().__init__()``: reads that
    # run before the widget is attached must not go through an accessor that
    # could reach for the theme, which is not resolvable yet. The preset is
    # what ``MenuStyle.from_theme`` falls back to, so an unthemed app sees
    # no change; a themed one adopts its style on the first measure.
    effective_style = style or MenuStyle.default()
    #: The style the caller passed, or ``None`` to follow the theme.
    self._user_style: MenuStyle | None = style
    self._applied_style: MenuStyle = effective_style
    self._autofocus = bool(autofocus)
    self._autofocus_pending = False
    self._parent_item = parent_item
    self._focus_index = -1
    self._focusable_items: list[MenuItem] = []

    _shadows = elevation_shadows(effective_style.elevation)

    children = self._materialize_children(effective_style)
    self._column = self._build_column(children, effective_style)

    super().__init__(
        child=self._column,
        on_click=None,
        state_layer_color=effective_style.state_layer_color,
        background_color=effective_style.background,
        corner_radius=effective_style.corner_radius,
        shadows=_shadows,
        # The menu is one group, not a row of stops: Tab neither lands on the
        # surface nor on the items, it leaves the menu (see the scope below).
        traversable=False,
        padding=padding if padding is not None else 0,
        key=key,
    )

    # Tab roves the items like everywhere else in the framework: it is the key
    # the user presses to be given the focus, so the first Tab must land on an
    # item, not close the menu. Only stepping past the last item is a boundary
    # (see _MenuTraversalPolicy.on_boundary). The arrow keys wrap; Tab does not.
    self._focus_scope = FocusScope(_MenuTraversalPolicy(self))
    self.add_node(self._focus_scope)

    # Menu surface itself should not paint state layers.
    self._HOVER_OPACITY = 0.0
    self._PRESS_OPACITY = 0.0
    self._FOCUS_OPACITY = 0.0

should_show_focus_ring property

should_show_focus_ring: bool

Never ring the surface: it holds the focus, but the items show where it is.

style property

style: MenuStyle

The style in effect: the caller's or the parent menu's, else the theme's, with the MD3 baseline numbers.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:MenuStyle's presets.

Source code in src/nuiitivet/material/menu.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`MenuStyle`'s presets."""
    return {"style": MenuStyle}

focus_first_item

focus_first_item() -> bool

Focus the first enabled item.

Returns False if the menu has no enabled item, or if it is not mounted yet: a widget's on_mount runs before its children's, so in that case the request is latched and :meth:_item_mounted completes it.

Source code in src/nuiitivet/material/menu.py
def focus_first_item(self) -> bool:
    """Focus the first enabled item.

    Returns False if the menu has no enabled item, or if it is not mounted
    yet: a widget's ``on_mount`` runs before its children's, so in that case
    the request is latched and :meth:`_item_mounted` completes it.
    """
    item = self._first_enabled_item()
    if item is None:
        return False
    if getattr(item, "_app", None) is None:
        self._autofocus_pending = True
        return False

    self._autofocus_pending = False
    self._focus_item(item)
    return True

MenuDivider

Sentinel that renders a horizontal divider inside a Menu.

MenuItem

MenuItem(label: str, *, on_click: Callable[[], None] | None = None, disabled: bool = False, leading_icon: Symbol | str | None = None, trailing: Symbol | str | None = None, padding: PaddingLike = 0, key: str | None = None)

Bases: InteractiveWidget

Material Design 3 menu item widget.

Initialize MenuItem.

The item height is MD3-fixed (list-item token), so it is not a constructor parameter; customize it via MenuStyle.item_height (SIZE_POLICY: MD3 fixes the axis -> style only).

Parameters:

Name Type Description Default
label str

Item label.

required
on_click Callable[[], None] | None

Click callback.

None
disabled bool

Whether this item is disabled.

False
leading_icon Symbol | str | None

Optional leading icon.

None
trailing Symbol | str | None

Optional trailing icon (Symbol) or trailing text (str).

None
padding PaddingLike

Insets from the allocated rect to the item.

0
key str | None

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

None
Source code in src/nuiitivet/material/menu.py
def __init__(
    self,
    label: str,
    *,
    on_click: Callable[[], None] | None = None,
    disabled: bool = False,
    leading_icon: Symbol | str | None = None,
    trailing: Symbol | str | None = None,
    padding: PaddingLike = 0,
    key: str | None = None,
) -> None:
    """Initialize MenuItem.

    The item height is MD3-fixed (list-item token), so it is not a
    constructor parameter; customize it via ``MenuStyle.item_height``
    (SIZE_POLICY: MD3 fixes the axis -> style only).

    Args:
        label: Item label.
        on_click: Click callback.
        disabled: Whether this item is disabled.
        leading_icon: Optional leading icon.
        trailing: Optional trailing icon (Symbol) or trailing text (str).
        padding: Insets from the allocated rect to the item.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self.label = label
    self.leading_icon = leading_icon
    self.trailing = trailing
    self._menu_style: MenuStyle = MenuStyle.standard()
    self._owner_menu: Menu | None = None
    self._selected = False
    self._leading_icon_widget: Icon | None = None
    self._label_widget: Text | None = None
    self._trailing_text_widget: Text | None = None
    self._trailing_icon_widget: Icon | None = None
    self._content_row: Row | None = None
    self._content_container: Container | None = None
    self._content_icon_size: int | None = None

    # The item height is MD3-fixed, so the fixed sizing carries the padding band.
    _l, pad_top, _r, pad_bottom = parse_padding(padding if padding is not None else 0)
    resolved_height = Sizing.fixed(self._menu_style.item_height + pad_top + pad_bottom)

    super().__init__(
        child=Text(label),
        on_click=on_click,
        on_hover=self._handle_hover_change,
        on_press=self._handle_press,
        on_release=self._handle_release,
        disabled=disabled,
        state_layer_color=self._menu_style.state_layer_color,
        width=Sizing.weight(),
        height=resolved_height,
        background_color=None,
        padding=padding if padding is not None else 0,
        corner_radius=0,
        # The enclosing Menu's FocusScope moves focus between the items; the
        # global Tab sequence must not stop on them (WAI-ARIA menu pattern).
        traversable=False,
        key=key,
    )
    self._build_content(self._menu_style)
    self._apply_style(self._menu_style)

SubMenuItem

SubMenuItem(label: str, items: list[MenuItem | 'SubMenuItem' | MenuDivider], *, leading_icon: Symbol | str | None = None, disabled: bool = False, padding: PaddingLike = 0, key: str | None = None)

Bases: MenuItem

Material Design 3 submenu item that expands a nested menu.

Initialize SubMenuItem.

Parameters:

Name Type Description Default
label str

Item label.

required
items list[MenuItem | 'SubMenuItem' | MenuDivider]

Submenu entries.

required
leading_icon Symbol | str | None

Optional leading icon.

None
disabled bool

Whether this item is disabled.

False
padding PaddingLike

Insets from the allocated rect to the item.

0
key str | None

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

None
Source code in src/nuiitivet/material/menu.py
def __init__(
    self,
    label: str,
    items: list[MenuItem | "SubMenuItem" | MenuDivider],
    *,
    leading_icon: Symbol | str | None = None,
    disabled: bool = False,
    padding: PaddingLike = 0,
    key: str | None = None,
) -> None:
    """Initialize SubMenuItem.

    Args:
        label: Item label.
        items: Submenu entries.
        leading_icon: Optional leading icon.
        disabled: Whether this item is disabled.
        padding: Insets from the allocated rect to the item.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._submenu_items = list(items)
    self._submenu_handle: OverlayHandle[object] | None = None
    self._submenu_tick: Callable[[float], None] | None = None
    self._submenu: Menu | None = None
    self._parent_dismiss: Callable[[], None] | None = None
    self._submenu_pinned = False
    self._suppress_reopen = False

    super().__init__(
        label,
        on_click=self._on_self_click,
        disabled=disabled,
        leading_icon=leading_icon,
        trailing=Symbols.chevron_right,
        padding=padding,
        key=key,
    )

FabMenu

FabMenu(icon: IconLike, items: List[FabMenuItem], *, is_open: Optional[Observable[bool]] = None, auto_close: bool = True, close_icon: Union['Symbol', str] = close, style: Optional[FabStyle] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: Widget

Material Design 3 Expressive FAB Menu.

A Floating Action Button that expands into a vertical list of labelled actions. A single is_open observable is the source of truth. On open the FAB morphs into the MD3 close button: its icon changes (icon -> close_icon) and it shrinks from its closed size to a fixed 56dp (size "s") fully-rounded circle, regardless of the configured size. The overlay -- scrim, outside-tap dismissal, and anchored positioning -- is driven through the same observable via :func:~nuiitivet.modifiers.popup.popup.

The closed-size footprint is reserved for layout stability; the shrinking close button aligns to its top-trailing corner, so larger closed FAB sizes place the menu higher with a larger margin underneath (40dp for medium, 56dp for large per MD3). The menu expands upward from that top-trailing edge with a 4dp gap and a staggered item reveal. Selecting an item invokes its on_click and, by default, closes the menu.

Initialize a FabMenu.

Parameters:

Name Type Description Default
icon IconLike

FAB icon shown while the menu is closed.

required
items List[FabMenuItem]

The actions to display when the menu is open.

required
is_open Optional[Observable[bool]]

Optional external Observable[bool] controlling the open/close state. When None an internal one is created and exposed via :attr:is_open.

None
auto_close bool

When True (default), selecting an item closes the menu after invoking its on_click.

True
close_icon Union['Symbol', str]

Icon the FAB morphs to while the menu is open. Defaults to Symbols.close.

close
style Optional[FabStyle]

FAB style preset selecting the colour family and size. Defaults to :meth:FabStyle.primary. List items use the matching *-container colour set.

None
padding PaddingLike

Insets from the allocated rect to the FAB footprint.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/fab_menu.py
def __init__(
    self,
    icon: IconLike,
    items: List[FabMenuItem],
    *,
    is_open: Optional[Observable[bool]] = None,
    auto_close: bool = True,
    close_icon: Union["Symbol", str] = Symbols.close,
    style: Optional[FabStyle] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize a FabMenu.

    Args:
        icon: FAB icon shown while the menu is closed.
        items: The actions to display when the menu is open.
        is_open: Optional external ``Observable[bool]`` controlling the
            open/close state.  When ``None`` an internal one is created and
            exposed via :attr:`is_open`.
        auto_close: When ``True`` (default), selecting an item closes the
            menu after invoking its ``on_click``.
        close_icon: Icon the FAB morphs to while the menu is open.
            Defaults to ``Symbols.close``.
        style: FAB style preset selecting the colour family and size.
            Defaults to :meth:`FabStyle.primary`.  List items use the
            matching ``*-container`` colour set.
        padding: Insets from the allocated rect to the FAB footprint.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)

    base_style = style if style is not None else FabStyle.primary()
    self._is_open: Observable[bool] = is_open if is_open is not None else Observable(False)
    self._auto_close = bool(auto_close)

    # FAB icon is bound to is_open: there is no separate toggle state.
    def _resolve_icon(is_open_value: bool) -> Union["Symbol", str, IconLike]:
        return close_icon if is_open_value else icon

    icon_source = self._is_open.map(_resolve_icon)

    # The FAB keeps its normal size/shape while closed and morphs into the
    # fixed 56dp circular "close button" while open.  The morph is owned by
    # the FAB and bound to is_open.  The closed footprint is reserved so the
    # surrounding layout stays stable; the shrinking close button aligns to
    # the footprint's top-trailing corner, growing the bottom margin.
    # The close button uses the family's *solid* colour (distinct from the
    # tonal list items) per MD3.
    fab_style = _close_button_style(base_style)
    self._closed_size = float(base_style.container_height)
    morph_fab_cls = _build_morph_fab_class()
    self._fab = morph_fab_cls(
        icon_source,
        on_click=self._toggle,
        style=fab_style,
        is_open=self._is_open,
        closed_size=self._closed_size,
        open_size=_OPEN_FAB_SIZE,
        closed_corner=_scalar_corner(fab_style.corner_radius),
        open_corner=_OPEN_FAB_CORNER,
    )

    self._list = _FabMenuList(
        items,
        item_style=_list_item_style(base_style),
        on_select=self._on_item_selected,
    )

    # Reuse the existing popup overlay for blocking + outside-tap close.
    self._inner = self._fab.modifier(
        popup(
            self._list,
            is_open=self._is_open,
            target_anchor="top-right",
            content_anchor="bottom-right",
            offset=(0.0, -_CLOSE_BUTTON_BETWEEN_SPACE),
        )
    )
    self.add_child(self._inner)

is_open property

is_open: Observable[bool]

Observable that controls (and reflects) the menu's open state.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:FabStyle's presets.

Source code in src/nuiitivet/material/fab_menu.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`FabStyle`'s presets."""
    return {"style": FabStyle}

FabMenuItem dataclass

FabMenuItem(icon: IconLike, label: LabelLike, on_click: Optional[VoidCallback] = None, disabled: Union[bool, ObservableProtocol[bool]] = False)

Declarative spec for a single action inside a :class:FabMenu.

Attributes:

Name Type Description
icon IconLike

Leading icon shown in the menu-item pill.

label LabelLike

Text label rendered next to the icon.

on_click Optional[VoidCallback]

Optional callback invoked when the item is selected.

disabled Union[bool, ObservableProtocol[bool]]

Whether the item is disabled (non-interactive).

LoadingIntent dataclass

LoadingIntent()

Intent for showing a loading indicator via MaterialOverlay.

This is a marker intent with no parameters. Replace the indicator through the intents of the window's MaterialOverlay.

Icon

Icon(name: Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str], *, size: SizingLike = 24, padding: Optional[Tuple[int, int, int, int] | Tuple[int, int] | int] = None, style: Optional['IconStyle'] = None, key: Optional[str] = None)

Bases: IconBase

Material Symbols icon widget.

Create a Material-like icon by ligature name.

Parameters:

Name Type Description Default
name Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str]

Ligature name such as "home", "menu", "search", a Symbol, or an Observable that yields either.

required
size SizingLike

Logical pixel size of the icon (font size used for the glyph).

24
padding Optional[Tuple[int, int, int, int] | Tuple[int, int] | int]

Insets from the allocated rect to the glyph.

None
style Optional['IconStyle']

IconStyle for customization (defaults to theme style).

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/icon.py
def __init__(
    self,
    name: Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str],
    *,
    size: SizingLike = 24,
    padding: Optional[Tuple[int, int, int, int] | Tuple[int, int] | int] = None,
    style: Optional["IconStyle"] = None,
    key: Optional[str] = None,
):
    """Create a Material-like icon by ligature name.

    Args:
        name: Ligature name such as "home", "menu", "search", a Symbol,
            or an Observable that yields either.
        size: Logical pixel size of the icon (font size used for the glyph).
        padding: Insets from the allocated rect to the glyph.
        style: IconStyle for customization (defaults to theme style).
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    # Store style (use provided or get from theme lazily)
    self._style = style

    # Treat `size` as a SizingLike and use it for layout via the
    # base Widget's width/height. Also compute a pixel fallback stored
    # in self._size for paint-time operations.
    super().__init__(size=size, padding=padding if padding is not None else 0, key=key)

    self._name_source: Any = name
    self._symbol: Optional["Symbol"] = None
    self._symbol_codepoint: Optional[str] = None

    resolved_name: Symbol | str
    if hasattr(name, "value"):
        try:
            resolved_name = name.value  # type: ignore[assignment]
        except Exception:
            exception_once(logger, "icon_name_value_exc", "Failed to read icon name.value")
            resolved_name = str(name)
    else:
        resolved_name = name  # type: ignore[assignment]

    self._apply_name(resolved_name)

    self._size = _pixel_size_from_sizing(size)
    self._font_file_candidates: Tuple[str, ...] = ("MaterialIcons-Regular.ttf",)
    # Cache typeface to avoid repeated _load_typeface calls on every paint
    self._cached_typeface: Optional[object] = None

family property

family: str

Return the style-driven icon family.

The family is sourced from IconStyle.family (explicit style= or theme-resolved style). If style resolution fails, this falls back to "outlined".

preferred_size

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

Return preferred size including padding (M3準拠).

Source code in src/nuiitivet/material/icon.py
def preferred_size(self, max_width: Optional[int] = None, max_height: Optional[int] = None) -> tuple[int, int]:
    """Return preferred size including padding (M3準拠)."""
    # Respect explicit Sizing overrides when provided on the widget.
    # Icons are square by default (size x size) when no fixed Sizing
    # is provided.
    w_dim = self.width_sizing
    h_dim = self.height_sizing

    if w_dim.kind == "fixed":
        width = int(w_dim.value)
    else:
        width = self._size

    if h_dim.kind == "fixed":
        height = int(h_dim.value)
    else:
        height = self._size

    # The allocated rect is the glyph plus its padding.
    l, t, r, b = self.padding
    total_w = int(width) + int(l) + int(r)
    total_h = int(height) + int(t) + int(b)

    if max_width is not None:
        total_w = min(int(total_w), int(max_width))
    if max_height is not None:
        total_h = min(int(total_h), int(max_height))

    return (int(total_w), int(total_h))

paint

paint(canvas, x: int, y: int, width: int, height: int)

Paint icon with padding support (M3準拠).

Source code in src/nuiitivet/material/icon.py
@replay_safe
def paint(self, canvas, x: int, y: int, width: int, height: int):
    """Paint icon with padding support (M3準拠)."""
    cx, cy, cw, ch = self.content_rect(x, y, width, height)

    # Determine size to draw (contain behavior).
    # Use the smaller dimension of the content area to maintain aspect ratio.
    draw_size = min(cw, ch)
    if draw_size <= 0:
        return

    # Use cached typeface if available, otherwise load and cache it
    tf = self._cached_typeface
    if tf is None:
        tf = self._load_typeface()
        self._cached_typeface = tf
    # Prefer the resolved typeface. If none was found, try to load a
    # packaged legacy MaterialIcons font before falling back to a
    # Font constructed without an explicit typeface.
    font = None
    if tf is not None:
        try:
            font = make_font(tf, draw_size)
        except Exception:
            exception_once(logger, "icon_make_font_exc", "make_font failed for resolved typeface")
            font = None

    if font is None:
        try:
            legacy_tf = None
            fallback_names = getattr(self, "_font_file_candidates", ("MaterialIcons-Regular.ttf",))
            for fallback_name in fallback_names:
                legacy_fp = self._first_available_font_file(fallback_name)
                if legacy_fp and os.path.isfile(legacy_fp):
                    try:
                        legacy_tf = typeface_from_file(legacy_fp)
                    except Exception:
                        exception_once(
                            logger,
                            "icon_typeface_from_file_primary_exc",
                            "typeface_from_file failed (file=%s)",
                            os.path.basename(legacy_fp),
                        )
                        legacy_tf = None
                if legacy_tf is not None:
                    break
            if legacy_tf is not None:
                font = make_font(legacy_tf, draw_size)
            else:
                font = make_font(None, draw_size)
        except Exception:
            try:
                font = make_font(None, draw_size)
            except Exception:
                exception_once(logger, "icon_make_font_fallback_exc", "make_font(None) failed")
                return

    # Use ligature name as text; Material fonts may convert to glyphs.
    try:
        blob = make_text_blob(self.name, font)
    except Exception:
        exception_once(logger, "icon_make_text_blob_exc", "make_text_blob failed for icon ligature")
        blob = None

    # If we have a mapping for this name, prefer rendering the mapped
    # PUA codepoint with the packaged legacy MaterialIcons font when
    # possible. This is a pragmatic default that makes common icons
    # reliably visible across different Material font variants.
    cp = self._symbol_codepoint
    if cp:
        try:
            # Try current typeface first
            try:
                cp_blob = make_text_blob(cp, font)
            except Exception:
                exception_once(logger, "icon_make_text_blob_codepoint_exc", "make_text_blob failed for codepoint")
                cp_blob = None

            def blob_has_height(b):
                try:
                    return b is not None and b.bounds().height() > 0.5
                except Exception:
                    exception_once(logger, "icon_blob_bounds_exc", "Failed to read text blob bounds")
                    return False

            if blob_has_height(cp_blob):
                blob = cp_blob
            else:
                # Try packaged legacy font
                try:
                    fallback_names = getattr(self, "_font_file_candidates", ("MaterialIcons-Regular.ttf",))
                    legacy_tf = None
                    for fallback_name in fallback_names:
                        legacy_fp = self._first_available_font_file(fallback_name)
                        if legacy_fp and os.path.isfile(legacy_fp):
                            try:
                                legacy_tf = typeface_from_file(legacy_fp)
                            except Exception:
                                exception_once(
                                    logger,
                                    "icon_typeface_from_file_exc",
                                    "typeface_from_file failed",
                                )
                                legacy_tf = None
                        if legacy_tf is not None:
                            break
                    if legacy_tf is not None:
                        legacy_font = make_font(legacy_tf, draw_size)
                        try:
                            legacy_blob = make_text_blob(cp, legacy_font)
                            if blob_has_height(legacy_blob):
                                blob = legacy_blob
                        except Exception:
                            exception_once(
                                logger,
                                "icon_make_text_blob_legacy_exc",
                                "make_text_blob failed for legacy font",
                            )
                except Exception:
                    exception_once(
                        logger,
                        "icon_legacy_font_fallback_exc",
                        "Icon legacy font fallback failed",
                    )
        except Exception:
            # if still no blob, leave as-is (no-op)
            exception_once(logger, "icon_paint_codepoint_exc", "Icon paint failed")
    if blob is None:
        return

    # Use IconBase to draw the blob
    from nuiitivet.theme.theme import Theme

    color = self.style.color
    resolved_color = resolve_color_to_rgba(color, theme=Theme.of(self))
    self.draw_blob(canvas, blob, resolved_color, x, y, width, height)

NavigationRail

NavigationRail(children: Sequence[RailItem], *, index: Union[int, MutableObservableBase[int]] = 0, on_select: Optional[Callable[[int], None]] = None, expanded: Union[bool, MutableObservableBase[bool]] = False, show_menu_button: bool = True, width: Union[SizingLike, ReadOnlyObservableProtocol] = None, height: Union[SizingLike, ReadOnlyObservableProtocol] = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, style: Optional[NavigationRailStyle] = None, key: Optional[str] = None)

Bases: InteractionHostMixin, Widget

Vertical navigation bar for desktop applications.

Material Design 3 component for persistent side navigation. Replaces NavigationDrawer for desktop/tablet layouts.

Display modes: - Collapsed (expanded=False): Icon above label (vertical), 96px wide - Expanded (expanded=True): Icon + label (horizontal), 220-360px wide (the expanded width is set via the width argument; see __init__)

Both modes show labels. The active indicator (selection background) wraps: - Collapsed: Only the icon (56×32dp) - Expanded: Both icon and label

Users can toggle between modes with optional menu button.

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

Initialize NavigationRail.

Parameters:

Name Type Description Default
children Sequence[RailItem]

The rail items to display.

required
index Union[int, MutableObservableBase[int]]

The currently selected index. int or a mutable Observable[int] — the rail writes the selection back, so a read-only/computed observable is rejected.

0
on_select Optional[Callable[[int], None]]

Callback when an item is selected.

None
expanded Union[bool, MutableObservableBase[bool]]

Whether the rail is expanded. bool or a mutable Observable[bool] — the menu button writes it back, so a read-only/computed observable is rejected. To drive expansion from derived state (e.g. window size), mirror it into an Observable and update that.

False
show_menu_button bool

Whether to show the menu toggle button.

True
width Union[SizingLike, ReadOnlyObservableProtocol]

Expanded rail width. A fixed value (e.g. 280 or Sizing.fixed(280)) sets the expanded width, clamped into the MD3 range [220, 360] (see NavigationRailStyle). The rail always animates between the collapsed width and this expanded width; the collapsed width is never overridden here. Any non-fixed value (or None) uses the minimum expanded width; non-fixed values also emit a warning.

None
height Union[SizingLike, ReadOnlyObservableProtocol]

Height specification.

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

Padding specification.

0
style Optional[NavigationRailStyle]

Custom NavigationRailStyle.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/navigation_rail.py
def __init__(
    self,
    children: Sequence[RailItem],
    *,
    index: Union[int, MutableObservableBase[int]] = 0,
    on_select: Optional[Callable[[int], None]] = None,
    expanded: Union[bool, MutableObservableBase[bool]] = False,
    show_menu_button: bool = True,
    width: Union[SizingLike, ReadOnlyObservableProtocol] = None,
    height: Union[SizingLike, ReadOnlyObservableProtocol] = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    style: Optional[NavigationRailStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize NavigationRail.

    Args:
        children: The rail items to display.
        index: The currently selected index. ``int`` or a **mutable**
            ``Observable[int]`` — the rail writes the selection back, so a
            read-only/computed observable is rejected.
        on_select: Callback when an item is selected.
        expanded: Whether the rail is expanded. ``bool`` or a **mutable**
            ``Observable[bool]`` — the menu button writes it back, so a
            read-only/computed observable is rejected. To drive expansion
            from derived state (e.g. window size), mirror it into an
            ``Observable`` and update that.
        show_menu_button: Whether to show the menu toggle button.
        width: Expanded rail width. A *fixed* value (e.g. ``280`` or
            ``Sizing.fixed(280)``) sets the expanded width, clamped into the
            MD3 range ``[220, 360]`` (see ``NavigationRailStyle``). The rail
            always animates between the collapsed width and this expanded
            width; the collapsed width is never overridden here. Any
            non-fixed value (or ``None``) uses the minimum expanded width;
            non-fixed values also emit a warning.
        height: Height specification.
        padding: Padding specification.
        style: Custom NavigationRailStyle.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    _reject_readonly_observable(index, "index")
    _reject_readonly_observable(expanded, "expanded")

    self._is_expanded = expanded.value if isinstance(expanded, MutableObservableBase) else bool(expanded)
    self._style = style
    self._menu_icon_name: Optional[_ObservableValue[str]] = None
    eff_style = style or NavigationRailStyle()

    # Animation setup
    initial_expanded_value = 1.0 if self._is_expanded else 0.0
    self._expand_motion = EXPRESSIVE_DEFAULT_SPATIAL
    self._expand_animation: Animatable[float] = Animatable(initial_expanded_value, motion=self._expand_motion)
    self._label_animation: Animatable[float] = Animatable(initial_expanded_value, motion=EXPRESSIVE_DEFAULT_EFFECTS)
    self._menu_rotation_anim: Animatable[float] = Animatable(
        initial_expanded_value,
        motion=EXPRESSIVE_DEFAULT_EFFECTS,
    )
    self._menu_rotation = self._menu_rotation_anim.map(lambda progress: lerp(180.0, 360.0, progress))
    self._log_instance_id = id(self)
    logger.debug("NavigationRail init id=%s", self._log_instance_id)

    # Resolve the expanded width from `width` (only a fixed value sets it),
    # then always drive the outer width via the collapse animation so the
    # rail animates 96dp <-> expanded width regardless of what was passed.
    self._expanded_width, width_warning = _resolve_expanded_width(width, eff_style)
    collapsed_width = float(eff_style.container_width_collapsed)
    expanded_width = self._expanded_width
    animated_width = self._expand_animation.map(
        lambda progress: Sizing.fixed(
            int(lerp(collapsed_width, expanded_width, progress))
        )
    )

    super().__init__(width=animated_width, height=height, padding=padding, key=key)

    if width_warning is not None:
        warning_once(logger, width_warning[0], width_warning[1])

    # The items are one focus traversal group (WAI-ARIA tabs, manual
    # activation): a single Tab stop entered at the selected item, with
    # Up/Down roving the focus between the items — wrapping at the ends —
    # and Enter/Space selecting the focused one. Roving deliberately does
    # not move the selection: selecting a destination navigates, which is
    # too heavy an action to fire on every arrow press. The nodes are
    # created here but attached to the item-group widget each rebuild (see
    # ``_RailItemGroup``); the menu button is a separate ordinary Tab stop.
    self._focus_node = FocusNode(on_key=self.on_key_event)
    self._focus_scope = FocusScope(_RailTraversalPolicy(self), tab_roves=False)

    self._item_buttons: list[_RailItemButton] = []
    self._item_group: Optional[_RailItemGroup] = None
    self._menu_button: Optional[_RailMenuButton] = None

    self._rail_items: Sequence[RailItem] = list(children)
    self.on_select = on_select
    self.show_menu_button = show_menu_button

    # Handle index.
    self._index_observable: Optional[MutableObservableBase[int]] = None
    self._index_subscription = None
    if isinstance(index, MutableObservableBase):
        self._index_observable = index
        self._current_index = self._validate_index(index.value)
        self._index_subscription = index.subscribe(self._on_index_changed)
    else:
        self._current_index = self._validate_index(int(index))

    # Handle expanded state.
    self._expanded_observable: Optional[MutableObservableBase[bool]] = None
    self._expanded_subscription = None
    if isinstance(expanded, MutableObservableBase):
        self._expanded_observable = expanded
        # _is_expanded already set above
        self._expanded_subscription = expanded.subscribe(self._on_expanded_changed)
    # else: _is_expanded already set above

    # Ensure subscriptions are released when removed from the tree.
    self.on_dispose(self.dispose)

    # Build UI.
    self._rebuild_ui()

style property

style: Optional[NavigationRailStyle]

Get the navigation rail style.

current_index property

current_index: int

Get the currently selected item index.

is_expanded property

is_expanded: bool

Get the current expanded state.

on_key_event

on_key_event(key: str, modifier_keys: int = 0) -> bool

Rove the items with Up/Down, wrapping; Enter/Space acts on the focused item.

Only the vertical axis roves — a rail is always a column. Enter/Space are handled by the focused item itself, so they never reach here.

Source code in src/nuiitivet/material/navigation_rail.py
def on_key_event(self, key: str, modifier_keys: int = 0) -> bool:
    """Rove the items with Up/Down, wrapping; Enter/Space acts on the focused item.

    Only the vertical axis roves — a rail is always a column. Enter/Space
    are handled by the focused item itself, so they never reach here.
    """
    key_name = str(key).lower()

    if key_name == "down":
        return self._focus_scope.move(1, wrap=True)

    if key_name == "up":
        return self._focus_scope.move(-1, wrap=True)

    return False

preferred_size

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

Calculate preferred size for the navigation rail.

Source code in src/nuiitivet/material/navigation_rail.py
def preferred_size(self, max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]:
    """Calculate preferred size for the navigation rail."""
    rail_width = self._calculate_width()

    # Get height from child if present.
    children = self.children_snapshot()
    if children:
        child = children[0]
        l, t, r, b = self.padding
        child_max_h = None if max_height is None else max(0, int(max_height) - t - b)
        child_w, child_height = child.preferred_size(max_width=rail_width, max_height=child_max_h)
        preferred_w = max(int(rail_width), int(child_w)) + l + r
        preferred_h = int(child_height) + t + b

        if max_width is not None:
            preferred_w = min(int(preferred_w), int(max_width))
        if max_height is not None:
            preferred_h = min(int(preferred_h), int(max_height))

        return (int(preferred_w), int(preferred_h))

    # Default minimum height.
    preferred_w = int(rail_width)
    preferred_h = 400
    if max_width is not None:
        preferred_w = min(int(preferred_w), int(max_width))
    if max_height is not None:
        preferred_h = min(int(preferred_h), int(max_height))
    return (int(preferred_w), int(preferred_h))

layout

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

Layout the navigation rail and its child.

Source code in src/nuiitivet/material/navigation_rail.py
def layout(self, width: int, height: int) -> None:
    """Layout the navigation rail and its child."""
    super().layout(width, height)

    children = self.children_snapshot()
    if not children:
        return

    # Layout the single child (Box containing Column).
    child = children[0]
    # Use provided dimensions minus padding.
    l, t, r, b = self.padding
    cw = max(0, width - l - r)
    ch = max(0, height - t - b)

    child.layout(cw, ch)
    child.set_layout_rect(l, t, cw, ch)

paint

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

Paint the NavigationRail.

Source code in src/nuiitivet/material/navigation_rail.py
@replay_safe
def paint(self, canvas, x: int, y: int, width: int, height: int) -> None:
    """Paint the NavigationRail."""
    children = self.children_snapshot()
    if not children:
        return

    # Layout not yet complete; skip this frame.
    if any(c.layout_rect is None for c in children):
        return

    # Paint the child.
    child = children[0]
    rect = 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

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

dispose

dispose() -> None

Clean up subscriptions.

Source code in src/nuiitivet/material/navigation_rail.py
def dispose(self) -> None:
    """Clean up subscriptions."""
    logger.debug("NavigationRail dispose id=%s", self._log_instance_id)
    if self._index_subscription is not None:
        self._index_subscription.dispose()
        self._index_subscription = None
    if self._expanded_subscription is not None:
        self._expanded_subscription.dispose()
        self._expanded_subscription = None
    self._expand_animation.stop()  # Ensure ticker stopped
    self._label_animation.stop()
    self._menu_rotation_anim.stop()

RailItem

RailItem(icon: IconLike, label: LabelLike, *, small_badge: Optional[ReadOnlyObservableProtocol[bool]] = None, large_badge: Optional[ReadOnlyObservableProtocol[Optional[str]]] = None, style: Optional[NavigationRailStyle] = None, key: Optional[str] = None)

Bases: Widget

Navigation rail destination item.

A widget representing a single destination in NavigationRail. Displays an icon and optional label (when rail is expanded).

Initialize RailItem.

Parameters:

Name Type Description Default
icon IconLike

The icon to display. May be a :class:Symbol, a ligature string, or an observable of either (mirroring IconButton).

required
label LabelLike

The label to display. May be a string or an observable string.

required
small_badge Optional[ReadOnlyObservableProtocol[bool]]

Optional Observable controlling small dot badge visibility.

None
large_badge Optional[ReadOnlyObservableProtocol[Optional[str]]]

Optional Observable with badge text. None or "" hides the badge. When both small_badge and large_badge are provided, large_badge takes precedence.

None
style Optional[NavigationRailStyle]

Optional style override for this item.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/navigation_rail.py
def __init__(
    self,
    icon: IconLike,
    label: LabelLike,
    *,
    small_badge: Optional[ReadOnlyObservableProtocol[bool]] = None,
    large_badge: Optional[ReadOnlyObservableProtocol[Optional[str]]] = None,
    style: Optional[NavigationRailStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize RailItem.

    Args:
        icon: The icon to display. May be a :class:`Symbol`, a ligature
            string, or an observable of either (mirroring ``IconButton``).
        label: The label to display. May be a string or an observable string.
        small_badge: Optional Observable controlling small dot badge visibility.
        large_badge: Optional Observable with badge text. ``None`` or ``""`` hides the badge.
            When both ``small_badge`` and ``large_badge`` are provided,
            ``large_badge`` takes precedence.
        style: Optional style override for this item.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(key=key)

    self.icon_spec = icon
    self.label_spec = label
    self._small_badge_observable: Optional[ReadOnlyObservableProtocol[bool]] = small_badge
    self._large_badge_observable: Optional[ReadOnlyObservableProtocol[Optional[str]]] = large_badge
    self._style = style

    self._icon_widget: Widget
    self._label_widget: Widget

    eff_style = style or NavigationRailStyle()
    icon_color = eff_style.icon_color or ColorRole.ON_SURFACE
    icon_size = eff_style.icon_size
    self._icon_widget = Icon(icon, size=icon_size, style=IconStyle(color=icon_color))

    label_color = eff_style.label_color or ColorRole.ON_SURFACE_VARIANT
    if eff_style.label_text_style is not None:
        text_style = eff_style.label_text_style.copy_with(color=label_color)
    else:
        text_style = TextStyle(color=label_color)

    self._label_widget = Text(
        label,
        style=text_style,
        type_scale=TypeScale.LABEL_MEDIUM,
        alignment="center",
        width=Sizing.fixed(eff_style.container_width_collapsed),
        max_lines=1,
        overflow="ellipsis",
        soft_wrap=False,
    )

style property

style: Optional[NavigationRailStyle]

Get the style override.

icon_widget property

icon_widget: Widget

Get the icon widget.

label_widget property

label_widget: Widget

Get the label widget.

small_badge_observable property

small_badge_observable: Optional[ReadOnlyObservableProtocol[bool]]

Get the optional small badge observable.

large_badge_observable property

large_badge_observable: Optional[ReadOnlyObservableProtocol[Optional[str]]]

Get the optional large badge observable.

Checkbox

Checkbox(checked: bool | ObservableProtocol[bool] | ObservableProtocol[Optional[bool]] = False, *, on_toggle: Optional[Callable[[Optional[bool]], None]] = None, indeterminate: bool | ObservableProtocol[bool] = False, disabled: bool | ObservableProtocol[bool] = False, padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None, style: Optional['CheckboxStyle'] = None, key: Optional[str] = None)

Bases: Toggleable, InteractiveWidget

A Material Design 3 checkbox.

Initialize Checkbox.

Parameters:

Name Type Description Default
checked bool | ObservableProtocol[bool] | ObservableProtocol[Optional[bool]]

Checked state, or the observable holding it. An observable of Optional[bool] carries the indeterminate state as None.

False
on_toggle Optional[Callable[[Optional[bool]], None]]

Callback invoked with the new state when toggled.

None
indeterminate bool | ObservableProtocol[bool]

Whether the checkbox shows the indeterminate mark, or an observable of it, kept apart from a two-state checked.

False
disabled bool | ObservableProtocol[bool]

Whether the checkbox ignores interaction, or an observable of it.

False
padding Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]]

Insets from the allocated rect to the touch target.

None
style Optional['CheckboxStyle']

Visual style; the theme's checkbox style when omitted.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/selection_controls.py
def __init__(
    self,
    checked: bool | ObservableProtocol[bool] | ObservableProtocol[Optional[bool]] = False,
    *,
    on_toggle: Optional[Callable[[Optional[bool]], None]] = None,
    indeterminate: bool | ObservableProtocol[bool] = False,
    disabled: bool | ObservableProtocol[bool] = False,
    padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None,
    style: Optional["CheckboxStyle"] = None,
    key: Optional[str] = None,
):
    """Initialize Checkbox.

    Args:
        checked: Checked state, or the observable holding it. An observable
            of ``Optional[bool]`` carries the indeterminate state as ``None``.
        on_toggle: Callback invoked with the new state when toggled.
        indeterminate: Whether the checkbox shows the indeterminate mark, or
            an observable of it, kept apart from a two-state *checked*.
        disabled: Whether the checkbox ignores interaction, or an observable
            of it.
        padding: Insets from the allocated rect to the touch target.
        style: Visual style; the theme's checkbox style when omitted.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._checked_external_tri: ObservableProtocol[Optional[bool]] | None = None
    self._checked_external_bool: ObservableProtocol[bool] | None = None
    self._indeterminate_external: ObservableProtocol[bool] | None = None

    checked_is_obs = hasattr(checked, "subscribe") and hasattr(checked, "value")
    indeterminate_is_obs = hasattr(indeterminate, "subscribe") and hasattr(indeterminate, "value")

    if checked_is_obs and not (indeterminate_is_obs or bool(indeterminate)):
        self._checked_external_tri = cast("ObservableProtocol[Optional[bool]]", checked)
    elif checked_is_obs:
        self._checked_external_bool = cast("ObservableProtocol[bool]", checked)

    if indeterminate_is_obs:
        self._indeterminate_external = cast("ObservableProtocol[bool]", indeterminate)

    # Determine initial value for Toggleable (internal state is the render source-of-truth)
    value: Optional[bool]
    if self._checked_external_tri is not None:
        value = self._checked_external_tri.value
    else:
        if self._checked_external_bool is not None:
            base_checked = bool(self._checked_external_bool.value)
        else:
            base_checked = bool(checked)

        if self._indeterminate_external is not None:
            is_indeterminate = bool(self._indeterminate_external.value)
        else:
            is_indeterminate = bool(indeterminate)

        value = None if is_indeterminate else base_checked

    # Store style (use provided or get from theme lazily)
    self._style = style

    final_padding = padding if padding is not None else 0

    # Initialize Toggleable
    super().__init__(
        value=value,
        on_change=on_toggle,
        tristate=False,  # Checkbox does not cycle to indeterminate
        disabled=disabled,
        padding=final_padding,
        key=key,
    )

    self._look: ThemeKept[_CheckboxLook] = ThemeKept()

    initial_selection = 1.0 if self.value is True or self.value is None else 0.0
    self._state_layer_anim: Animatable[float] = Animatable(0.0, motion=EXPRESSIVE_DEFAULT_EFFECTS)
    self.bind(self._state_layer_anim.subscribe(lambda _: self.invalidate()))
    self._selection_anim: Animatable[float] = Animatable(initial_selection, motion=EXPRESSIVE_DEFAULT_SPATIAL)
    self.bind(self._selection_anim.subscribe(lambda _: self.invalidate()))

preferred_size

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

Return preferred size including padding (M3準拠).

Source code in src/nuiitivet/material/selection_controls.py
def preferred_size(self, max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]:
    """Return preferred size including padding (M3準拠)."""
    w_dim = self.width_sizing
    h_dim = self.height_sizing

    box = _box_size(self.style, self)
    width = int(w_dim.value) if w_dim.kind == "fixed" else box
    height = int(h_dim.value) if h_dim.kind == "fixed" else box

    l, t, r, b = self.padding
    total_w = width + l + r
    total_h = height + t + b

    if max_width is not None:
        total_w = min(int(total_w), int(max_width))
    if max_height is not None:
        total_h = min(int(total_h), int(max_height))

    return (int(total_w), int(total_h))

paint

paint(canvas, x: int, y: int, width: int, height: int)

Paint checkbox with padding support (M3準拠).

Source code in src/nuiitivet/material/selection_controls.py
@replay_safe
def paint(self, canvas, x: int, y: int, width: int, height: int):
    """Paint checkbox with padding support (M3準拠)."""
    try:
        content_x, content_y, content_w, content_h = self.content_rect(x, y, width, height)
        touch_sz = min(content_w, content_h)
        if touch_sz <= 0:
            return

        cx = content_x + (content_w - touch_sz) // 2
        cy = content_y + (content_h - touch_sz) // 2

        self.set_last_rect(x, y, width, height)

        disabled = self.disabled
        look = self._look.get(self, (self._style, disabled, touch_sz), lambda: self._resolve_look(touch_sz))
        sizes = look.sizes
        icon_sz = sizes["icon_size"]
        corner = sizes["corner_radius"]
        stroke_w = sizes["stroke_width"]
        state_diam = sizes["state_layer_size"]

        icon_x = cx + (touch_sz - icon_sz) // 2
        icon_y = cy + (touch_sz - icon_sz) // 2

        stroke_p = shared_paint(look.outline, "stroke", stroke_w)
        rect = make_rect(icon_x, icon_y, icon_sz, icon_sz)

        # Check for keyboard focus (Ring visible)
        is_keyboard_focus = self.should_show_focus_ring

        # Determine State Layer opacity (a disabled checkbox has no state layer per M3)
        overlay_alpha = 0.0 if disabled else self._get_active_state_layer_opacity()

        if overlay_alpha > 0.0:
            cx_center = float(cx + touch_sz / 2.0)
            cy_center = float(cy + touch_sz / 2.0)
            r = float(state_diam / 2.0)

            # State Layer color (Checked=Primary, Unchecked=OnSurface)
            is_checked = self.value is True or self.value is None
            base_color = look.overlay_checked if is_checked else look.overlay_unchecked

            p_ov = shared_paint(skcolor(base_color, overlay_alpha))
            try:
                canvas.drawCircle(cx_center, cy_center, r, p_ov)
            except Exception:
                draw_oval(canvas, make_rect(cx_center - r, cy_center - r, state_diam, state_diam), p_ov)

        if rect is not None and stroke_p is not None:
            draw_round_rect(canvas, rect, corner, stroke_p)

        if not disabled and is_keyboard_focus:
            self.draw_focus_indicator(canvas, x, y, width, height)

        val = self.value
        selection_progress = self._get_selection_progress()
        # The colours at full selection are kept; a frame of the animation scales the alpha.
        settled = selection_progress >= 1.0
        if selection_progress > 1e-6:
            fill_p = shared_paint(
                look.container_full
                if settled
                else rgba_to_skia_color(_scale_alpha(look.container, selection_progress))
            )
            if rect is not None and fill_p is not None:
                draw_round_rect(canvas, rect, corner, fill_p)

        # Secondary overlay check (legacy or box-specific?)
        # We use the same opacity logic
        overlay_alpha_box = overlay_alpha

        if overlay_alpha_box and overlay_alpha_box > 0.0:
            base = "#000000" if self.state.pressed else "#FFFFFF"
            p_ov = shared_paint(skcolor(base, overlay_alpha_box))
            if rect is not None and p_ov is not None:
                draw_round_rect(canvas, rect, corner, p_ov)

        if (val is True or val is None) and selection_progress > 1e-6:
            mark_is_none = val is None
            mark_p = shared_paint(
                look.mark_full if settled else rgba_to_skia_color(_scale_alpha(look.mark, selection_progress)),
                "stroke" if not mark_is_none else "fill",
                max(1.0, icon_sz * 0.12),
            )
            if mark_p is None:
                return

            if mark_is_none:
                bar_w = icon_sz * 0.5
                bar_h = max(1.0, icon_sz * 0.12)
                bx = icon_x + (icon_sz - bar_w) / 2.0
                by = icon_y + (icon_sz - bar_h) / 2.0
                r_bar = make_rect(bx, by, bar_w, bar_h)
                if r_bar is not None:
                    canvas.drawRect(r_bar, mark_p)
            else:
                x1 = icon_x + icon_sz * 0.18
                y1 = icon_y + icon_sz * 0.52
                x2 = icon_x + icon_sz * 0.42
                y2 = icon_y + icon_sz * 0.72
                x3 = icon_x + icon_sz * 0.78
                y3 = icon_y + icon_sz * 0.30
                try:
                    canvas.drawLine(x1, y1, x2, y2, mark_p)
                    canvas.drawLine(x2, y2, x3, y3, mark_p)
                except Exception:
                    path = make_path()
                    if path_move_to(path, x1, y1) and path_line_to(path, x2, y2) and path_line_to(path, x3, y3):
                        canvas.drawPath(path, mark_p)
    except Exception:
        exception_once(_logger, "checkbox_paint_exc", "Checkbox paint raised")
        return

draw_focus_indicator

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

Draw the standard focus ring around the state-layer circle.

Source code in src/nuiitivet/material/selection_controls.py
@replay_safe
def draw_focus_indicator(self, canvas, x: int, y: int, width: int, height: int) -> None:
    """Draw the standard focus ring around the state-layer circle."""
    content_x, content_y, content_w, content_h = self.content_rect(x, y, width, height)
    touch_sz = min(content_w, content_h)
    if touch_sz <= 0:
        return
    cx = content_x + (content_w - touch_sz) // 2
    cy = content_y + (content_h - touch_sz) // 2
    sizes = self.style.compute_sizes(_size_basis(self.style, self, touch_sz))
    diameter = float(cast(float, sizes["state_layer_size"]))
    ring_x = cx + (touch_sz - diameter) / 2.0
    ring_y = cy + (touch_sz - diameter) / 2.0
    self.draw_focus_ring(canvas, ring_x, ring_y, diameter, diameter, [diameter / 2.0] * 4)

RadioButton

RadioButton(value: object | None, *, disabled: bool | ObservableProtocol[bool] = False, padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None, style: Optional['RadioButtonStyle'] = None, key: Optional[str] = None)

Bases: Toggleable, InteractiveWidget

Material Design 3 RadioButton controlled by nearest RadioGroup.

Initialize RadioButton.

Parameters:

Name Type Description Default
value object | None

Option value represented by this radio button.

required
disabled bool | ObservableProtocol[bool]

Disable interaction when True.

False
padding Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]]

Insets from the allocated rect to the touch target.

None
style Optional['RadioButtonStyle']

Style override. Uses theme style when omitted.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/selection_controls.py
def __init__(
    self,
    value: object | None,
    *,
    disabled: bool | ObservableProtocol[bool] = False,
    padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None,
    style: Optional["RadioButtonStyle"] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize RadioButton.

    Args:
        value: Option value represented by this radio button.
        disabled: Disable interaction when True.
        padding: Insets from the allocated rect to the touch target.
        style: Style override. Uses theme style when omitted.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self.option_value = value
    self._style = style

    final_padding = padding if padding is not None else 0

    super().__init__(
        value=False,
        on_change=None,
        tristate=False,
        disabled=disabled,
        padding=final_padding,
        key=key,
    )

    self._state_layer_anim: Animatable[float] = Animatable(0.0, motion=EXPRESSIVE_DEFAULT_EFFECTS)
    self.bind(self._state_layer_anim.subscribe(lambda _: self.invalidate()))

    self._selection_anim: Animatable[float] = Animatable(0.0, motion=EXPRESSIVE_DEFAULT_SPATIAL)
    self.bind(self._selection_anim.subscribe(lambda _: self.invalidate()))

style property

style: 'RadioButtonStyle'

Resolved style for this RadioButton.

preferred_size

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

Return preferred size including padding.

Source code in src/nuiitivet/material/selection_controls.py
def preferred_size(self, max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]:
    """Return preferred size including padding."""
    w_dim = self.width_sizing
    h_dim = self.height_sizing

    box = _box_size(self.style, self)
    width = int(w_dim.value) if w_dim.kind == "fixed" else box
    height = int(h_dim.value) if h_dim.kind == "fixed" else box

    l, t, r, b = self.padding
    total_w = width + l + r
    total_h = height + t + b

    if max_width is not None:
        total_w = min(int(total_w), int(max_width))
    if max_height is not None:
        total_h = min(int(total_h), int(max_height))
    return (int(total_w), int(total_h))

paint

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

Paint radio button with MD3-like visuals.

Source code in src/nuiitivet/material/selection_controls.py
@replay_safe
def paint(self, canvas, x: int, y: int, width: int, height: int) -> None:
    """Paint radio button with MD3-like visuals."""
    try:
        from nuiitivet.rendering.skia import draw_oval, draw_ring, make_paint, make_rect, skcolor
        from nuiitivet.material.theme.color_role import ColorRole
        from nuiitivet.material.theme.theme_data import MaterialThemeData
        from nuiitivet.theme.theme import Theme

        content_x, content_y, content_w, content_h = self.content_rect(x, y, width, height)
        touch_sz = min(content_w, content_h)
        if touch_sz <= 0:
            return

        cx = content_x + (content_w - touch_sz) // 2
        cy = content_y + (content_h - touch_sz) // 2

        self.set_last_rect(x, y, width, height)

        sizes = self.style.compute_sizes(_size_basis(self.style, self, touch_sz))
        icon_diameter = float(cast(float, sizes["icon_diameter"]))
        inner_dot = float(cast(float, sizes["inner_dot"]))
        stroke_width = float(cast(float, sizes["stroke_width"]))
        state_layer_size = float(cast(float, sizes["state_layer_size"]))

        icon_x = cx + (touch_sz - icon_diameter) / 2.0
        icon_y = cy + (touch_sz - icon_diameter) / 2.0

        mat = Theme.of(self).extension(MaterialThemeData)
        roles = mat.roles if mat is not None else {}

        selected = bool(self.value)
        if self.disabled:
            stroke_hex = roles.get(ColorRole.ON_SURFACE, "#000000")
            stroke_alpha = self.style.disabled_alpha
        else:
            stroke_hex = roles.get(
                ColorRole.PRIMARY if selected else ColorRole.ON_SURFACE_VARIANT,
                "#000000",
            )
            stroke_alpha = 1.0

        overlay_alpha = self._get_active_state_layer_opacity()
        if overlay_alpha > 0.0:
            base = roles.get(ColorRole.PRIMARY if selected else ColorRole.ON_SURFACE, "#000000")
            layer_paint = make_paint(color=skcolor(base, overlay_alpha), style="fill", aa=True)
            layer_rect = make_rect(
                cx + (touch_sz - state_layer_size) / 2.0,
                cy + (touch_sz - state_layer_size) / 2.0,
                state_layer_size,
                state_layer_size,
            )
            if layer_rect is not None and layer_paint is not None:
                draw_oval(canvas, layer_rect, layer_paint)

        ring_paint = make_paint(color=skcolor(stroke_hex, stroke_alpha), style="fill", aa=True)
        ring_rect = make_rect(icon_x, icon_y, icon_diameter, icon_diameter)
        if ring_rect is not None and ring_paint is not None:
            draw_ring(canvas, ring_rect, stroke_width, ring_paint)

        progress = self._get_selection_progress()
        if progress > 1e-6:
            if self.disabled:
                dot_color = roles.get(ColorRole.ON_SURFACE, "#000000")
                dot_alpha = progress * self.style.disabled_alpha
            else:
                dot_color = roles.get(ColorRole.PRIMARY, "#000000")
                dot_alpha = progress
            dot_paint = make_paint(
                color=skcolor(dot_color, dot_alpha),
                style="fill",
                aa=True,
            )
            dot_size = inner_dot * progress
            dot_rect = make_rect(
                cx + (touch_sz - dot_size) / 2.0,
                cy + (touch_sz - dot_size) / 2.0,
                dot_size,
                dot_size,
            )
            if dot_rect is not None and dot_paint is not None:
                draw_oval(canvas, dot_rect, dot_paint)

        if not self.disabled and self.should_show_focus_ring:
            self.draw_focus_indicator(canvas, x, y, width, height)
    except Exception:
        exception_once(_logger, "radio_button_paint_exc", "RadioButton paint raised")

draw_focus_indicator

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

Draw the standard focus ring around the state-layer circle.

Source code in src/nuiitivet/material/selection_controls.py
@replay_safe
def draw_focus_indicator(self, canvas, x: int, y: int, width: int, height: int) -> None:
    """Draw the standard focus ring around the state-layer circle."""
    content_x, content_y, content_w, content_h = self.content_rect(x, y, width, height)
    touch_sz = min(content_w, content_h)
    if touch_sz <= 0:
        return
    cx = content_x + (content_w - touch_sz) // 2
    cy = content_y + (content_h - touch_sz) // 2
    sizes = self.style.compute_sizes(_size_basis(self.style, self, touch_sz))
    diameter = float(cast(float, sizes["state_layer_size"]))
    ring_x = cx + (touch_sz - diameter) / 2.0
    ring_y = cy + (touch_sz - diameter) / 2.0
    self.draw_focus_ring(canvas, ring_x, ring_y, diameter, diameter, [diameter / 2.0] * 4)

RadioGroup

RadioGroup(child: Widget, *, value: object | ObservableProtocol[object | None] | None = None, on_change: Optional[Callable[[object | None], None]] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: InteractionHostMixin, Container

Container that manages a single selected value for descendant RadioButtons.

The group is one focus traversal group (WAI-ARIA): a single Tab stop, entered at the selected radio, with the arrow keys roving between the radios. Roving also moves the selection ("selection follows focus"), so the arrows are how the keyboard picks an option; Space and Enter select the current one as well. The arrows wrap at the ends, and either axis roves — a radio group may be laid out as a Row or a Column, and the keys must work whichever it is.

Initialize RadioGroup.

Parameters:

Name Type Description Default
child Widget

Root child subtree that contains radio options.

required
value object | ObservableProtocol[object | None] | None

Selected value or external observable selected value.

None
on_change Optional[Callable[[object | None], None]]

Callback invoked when selection changes.

None
padding PaddingLike

Insets from the allocated rect to the group.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/selection_controls.py
def __init__(
    self,
    child: Widget,
    *,
    value: object | ObservableProtocol[object | None] | None = None,
    on_change: Optional[Callable[[object | None], None]] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize RadioGroup.

    Args:
        child: Root child subtree that contains radio options.
        value: Selected value or external observable selected value.
        on_change: Callback invoked when selection changes.
        padding: Insets from the allocated rect to the group.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    if not isinstance(child, Widget):
        raise TypeError(f"child must be Widget, got {type(child)}")
    super().__init__(child=child, padding=padding, key=key)

    self._value_external: ObservableProtocol[object | None] | None = None
    if hasattr(value, "subscribe") and hasattr(value, "value"):
        self._value_external = cast("ObservableProtocol[object | None]", value)
        initial_value = self._value_external.value
    else:
        initial_value = value

    self._value_internal: Observable[object | None] = Observable(initial_value)
    self._on_change = on_change

    # The group is the Tab stop; the radios inside it are not (see
    # RadioButton.on_mount). Tab lands here, the scope hands the focus to the
    # selected radio, and the arrow keys take over from there.
    self._focus_node = FocusNode(on_key=self.on_key_event)
    self.add_node(self._focus_node)
    self._focus_scope = FocusScope(_RadioTraversalPolicy(self), tab_roves=False)
    self.add_node(self._focus_scope)

value property writable

value: object | None

Current selected value.

radios

radios() -> list['RadioButton']

Return the radios the keyboard can rove, in tree order.

Disabled radios are left out: they are not selectable, so the arrow keys skip over them rather than roving onto a dead option. A disabled radio has no FocusNode either (see :class:~nuiitivet.widgets.clickable.Clickable), which keeps this list and :meth:_radio_focus_nodes index-aligned.

Source code in src/nuiitivet/material/selection_controls.py
def radios(self) -> list["RadioButton"]:
    """Return the radios the keyboard can rove, in tree order.

    Disabled radios are left out: they are not selectable, so the arrow keys
    skip over them rather than roving onto a dead option. A disabled radio has
    no FocusNode either (see :class:`~nuiitivet.widgets.clickable.Clickable`),
    which keeps this list and :meth:`_radio_focus_nodes` index-aligned.
    """
    found: list[RadioButton] = []

    def _walk(node: Widget) -> None:
        for child in node.children_snapshot():
            if not isinstance(child, Widget):
                continue
            if isinstance(child, RadioGroup):
                continue
            if isinstance(child, RadioButton):
                if isinstance(child.get_node(FocusNode), FocusNode):
                    found.append(child)
            _walk(child)

    _walk(self)
    return found

on_key_event

on_key_event(key: str, modifier_keys: int = 0) -> bool

Rove the radios with the arrow keys, moving the selection with the focus.

Source code in src/nuiitivet/material/selection_controls.py
def on_key_event(self, key: str, modifier_keys: int = 0) -> bool:
    """Rove the radios with the arrow keys, moving the selection with the focus."""
    key_name = str(key).lower()

    if key_name in ("down", "right"):
        return self._move_focus(1)

    if key_name in ("up", "left"):
        return self._move_focus(-1)

    return False

select

select(new_value: object | None) -> None

Select a new value and notify listeners.

Source code in src/nuiitivet/material/selection_controls.py
def select(self, new_value: object | None) -> None:
    """Select a new value and notify listeners."""
    self._set_value(new_value, emit=True)

Switch

Switch(checked: bool | ObservableProtocol[bool] = False, *, on_change: Optional[Callable[[bool], None]] = None, disabled: bool | ObservableProtocol[bool] = False, padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None, style: Optional['SwitchStyle'] = None, key: Optional[str] = None)

Bases: Toggleable, InteractiveWidget

Material Design 3 Switch widget.

Initialize Switch.

Parameters:

Name Type Description Default
checked bool | ObservableProtocol[bool]

Checked state source (bool or observable bool).

False
on_change Optional[Callable[[bool], None]]

Callback invoked when checked state changes.

None
disabled bool | ObservableProtocol[bool]

Disable interaction when True.

False
padding Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]]

Insets from the allocated rect to the touch target.

None
style Optional['SwitchStyle']

Style override. Uses theme style when omitted.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/selection_controls.py
def __init__(
    self,
    checked: bool | ObservableProtocol[bool] = False,
    *,
    on_change: Optional[Callable[[bool], None]] = None,
    disabled: bool | ObservableProtocol[bool] = False,
    padding: Optional[Union[int, Tuple[int, int], Tuple[int, int, int, int]]] = None,
    style: Optional["SwitchStyle"] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize Switch.

    Args:
        checked: Checked state source (bool or observable bool).
        on_change: Callback invoked when checked state changes.
        disabled: Disable interaction when True.
        padding: Insets from the allocated rect to the touch target.
        style: Style override. Uses theme style when omitted.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._style = style
    self._on_change_bool = on_change

    final_padding = padding if padding is not None else 0

    def _on_toggle(next_val: Optional[bool]) -> None:
        if self._on_change_bool is not None:
            self._on_change_bool(bool(next_val))

    toggleable_value = cast("bool | ObservableProtocol[Optional[bool]]", checked)

    super().__init__(
        value=toggleable_value,
        on_change=_on_toggle,
        tristate=False,
        disabled=disabled,
        padding=final_padding,
        key=key,
    )

    self._state_layer_anim: Animatable[float] = Animatable(0.0, motion=EXPRESSIVE_DEFAULT_EFFECTS)
    self.bind(self._state_layer_anim.subscribe(lambda _: self.invalidate()))
    initial_selection = 1.0 if bool(self.value) else 0.0
    self._selection_anim: Animatable[float] = Animatable(initial_selection, motion=EXPRESSIVE_DEFAULT_SPATIAL)
    self.bind(self._selection_anim.subscribe(lambda _: self.invalidate()))
    self._look: ThemeKept[_SwitchLook] = ThemeKept()

style property

style: 'SwitchStyle'

Resolved style for this Switch.

preferred_size

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

Return preferred size including padding.

Source code in src/nuiitivet/material/selection_controls.py
def preferred_size(self, max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]:
    """Return preferred size including padding."""
    w_dim = self.width_sizing
    h_dim = self.height_sizing

    width = int(w_dim.value) if w_dim.kind == "fixed" else self._default_width()
    height = int(h_dim.value) if h_dim.kind == "fixed" else _box_size(self.style, self)

    l, t, r, b = self.padding
    total_w = width + l + r
    total_h = height + t + b

    if max_width is not None:
        total_w = min(int(total_w), int(max_width))
    if max_height is not None:
        total_h = min(int(total_h), int(max_height))
    return (int(total_w), int(total_h))

paint_outsets

paint_outsets() -> Tuple[int, int, int, int]

Extend the overflow allowance for the track's sideways overhang.

The track is wider than the touch target and the focus ring sits outside the track, so the base ring-only allowance would clip the ring's left and right edges.

Source code in src/nuiitivet/material/selection_controls.py
def paint_outsets(self) -> Tuple[int, int, int, int]:
    """Extend the overflow allowance for the track's sideways overhang.

    The track is wider than the touch target and the focus ring sits
    outside the track, so the base ring-only allowance would clip the
    ring's left and right edges.
    """
    base = super().paint_outsets()
    try:
        track_overflow = (self._track_width() - float(self._default_width())) / 2.0
    except Exception:
        track_overflow = 0.0
    if track_overflow <= 0:
        return base
    extra = int(math.ceil(track_overflow))
    return (base[0] + extra, base[1], base[2] + extra, base[3])

paint

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

Paint switch with animated thumb and track.

Source code in src/nuiitivet/material/selection_controls.py
@replay_safe
def paint(self, canvas, x: int, y: int, width: int, height: int) -> None:
    """Paint switch with animated thumb and track."""
    try:
        content_x, content_y, content_w, content_h = self.content_rect(x, y, width, height)
        touch_sz = min(content_w, content_h)
        if touch_sz <= 0:
            return

        cx = content_x + (content_w - touch_sz) // 2
        cy = content_y + (content_h - touch_sz) // 2
        self.set_last_rect(x, y, width, height)

        disabled = self.disabled
        checked = bool(self.value)
        look = self._look.get(
            self,
            (self._style, touch_sz, disabled, checked),
            lambda: self._resolve_look(touch_sz, disabled, checked),
        )
        track_w = look.track_width
        track_h = look.track_height
        state_layer_size = look.state_layer_size
        track_radius = track_h / 2.0

        track_x = cx + (touch_sz - track_w) / 2.0
        track_y = cy + (touch_sz - track_h) / 2.0

        progress = self._get_selection_progress()
        state = self.state
        if state.pressed or state.dragging:
            thumb_d = look.thumb_pressed
        else:
            thumb_d = look.thumb_selected if checked else look.thumb_unselected

        track_paint = shared_paint(look.track)
        track_rect = make_rect(track_x, track_y, track_w, track_h)
        if track_rect is not None and track_paint is not None:
            draw_round_rect(canvas, track_rect, track_radius, track_paint)

        if look.outline is not None:
            outline_paint = shared_paint(look.outline, "stroke", look.outline_width)
            if track_rect is not None and outline_paint is not None:
                draw_round_rect(canvas, track_rect, track_radius, outline_paint)

        thumb_center_start = track_x + (track_h / 2.0)
        thumb_center_end = track_x + track_w - (track_h / 2.0)
        thumb_center_x = thumb_center_start + (thumb_center_end - thumb_center_start) * progress
        thumb_x = thumb_center_x - (thumb_d / 2.0)
        thumb_y = track_y + (track_h - thumb_d) / 2.0

        overlay_alpha = self._get_active_state_layer_opacity()
        if overlay_alpha > 0.0:
            overlay_rect = make_rect(
                thumb_x + (thumb_d - state_layer_size) / 2.0,
                thumb_y + (thumb_d - state_layer_size) / 2.0,
                state_layer_size,
                state_layer_size,
            )
            overlay_paint = shared_paint(skcolor(look.overlay, overlay_alpha))
            if overlay_rect is not None and overlay_paint is not None:
                draw_oval(canvas, overlay_rect, overlay_paint)

        thumb_paint = shared_paint(look.thumb)
        thumb_rect = make_rect(thumb_x, thumb_y, thumb_d, thumb_d)
        if thumb_rect is not None and thumb_paint is not None:
            draw_oval(canvas, thumb_rect, thumb_paint)

        if not disabled and self.should_show_focus_ring:
            self.draw_focus_indicator(canvas, x, y, width, height)
    except Exception:
        exception_once(_logger, "switch_paint_exc", "Switch paint raised")

draw_focus_indicator

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

Draw the standard focus ring around the track.

Unlike Checkbox/RadioButton, the MD3 switch ring hugs the track outline rather than the thumb's state-layer circle, so it is a pill shape that stays put as the thumb moves.

Source code in src/nuiitivet/material/selection_controls.py
@replay_safe
def draw_focus_indicator(self, canvas, x: int, y: int, width: int, height: int) -> None:
    """Draw the standard focus ring around the track.

    Unlike Checkbox/RadioButton, the MD3 switch ring hugs the track
    outline rather than the thumb's state-layer circle, so it is a pill
    shape that stays put as the thumb moves.
    """
    content_x, content_y, content_w, content_h = self.content_rect(x, y, width, height)
    touch_sz = min(content_w, content_h)
    if touch_sz <= 0:
        return
    cx = content_x + (content_w - touch_sz) // 2
    cy = content_y + (content_h - touch_sz) // 2

    sizes = self.style.compute_sizes(_size_basis(self.style, self, touch_sz))
    track_w = float(cast(float, sizes["track_width"]))
    track_h = float(cast(float, sizes["track_height"]))

    track_x = cx + (touch_sz - track_w) / 2.0
    track_y = cy + (touch_sz - track_h) / 2.0

    self.draw_focus_ring(canvas, track_x, track_y, track_w, track_h, [track_h / 2.0] * 4)

HorizontalCenteredSlider

HorizontalCenteredSlider(value: float | ObservableProtocol[float] = 0.0, *, on_change: Optional[Callable[[float], None]] = None, min_value: float = -1.0, max_value: float = 1.0, stops: Optional[int] = None, show_value_indicator: bool = False, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = 'wt', padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None, style: Optional['SliderStyle'] = None, key: Optional[str] = None)

Bases: _CenteredSlider

Material Design 3 horizontal centered slider. Sized with width.

Initialize HorizontalCenteredSlider.

Parameters:

Name Type Description Default
value float | ObservableProtocol[float]

Current slider value or observable value.

0.0
on_change Optional[Callable[[float], None]]

Callback invoked when value changes.

None
min_value float

Minimum value (default: -1.0).

-1.0
max_value float

Maximum value (default: 1.0).

1.0
stops Optional[int]

Discrete stop count. None means continuous.

None
show_value_indicator bool

Whether to show value indicator during drag.

False
disabled bool | ObservableProtocol[bool]

Disabled state.

False
width SizingLike

Main-axis (width) sizing.

'wt'
padding Optional[Tuple[int, int] | Tuple[int, int, int, int] | int]

Slider padding.

None
style Optional['SliderStyle']

Optional SliderStyle override.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/slider.py
def __init__(
    self,
    value: float | ObservableProtocol[float] = 0.0,
    *,
    on_change: Optional[Callable[[float], None]] = None,
    min_value: float = -1.0,
    max_value: float = 1.0,
    stops: Optional[int] = None,
    show_value_indicator: bool = False,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = "wt",
    padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None,
    style: Optional["SliderStyle"] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize HorizontalCenteredSlider.

    Args:
        value: Current slider value or observable value.
        on_change: Callback invoked when value changes.
        min_value: Minimum value (default: -1.0).
        max_value: Maximum value (default: 1.0).
        stops: Discrete stop count. ``None`` means continuous.
        show_value_indicator: Whether to show value indicator during drag.
        disabled: Disabled state.
        width: Main-axis (width) sizing.
        padding: Slider padding.
        style: Optional SliderStyle override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        value=value,
        on_change=on_change,
        min_value=min_value,
        max_value=max_value,
        stops=stops,
        show_value_indicator=show_value_indicator,
        disabled=disabled,
        orientation=Orientation.HORIZONTAL,
        length=width,
        padding=padding,
        style=style,
        key=key,
    )

HorizontalRangeSlider

HorizontalRangeSlider(value_start: float | ObservableProtocol[float] = 0.0, value_end: float | ObservableProtocol[float] = 1.0, *, on_change: Optional[Callable[[Tuple[float, float]], None]] = None, min_value: float = 0.0, max_value: float = 1.0, stops: Optional[int] = None, show_value_indicator: bool = False, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = 'wt', padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None, style: Optional['SliderStyle'] = None, key: Optional[str] = None)

Bases: _RangeSlider

Material Design 3 horizontal range slider. Sized with width.

Initialize HorizontalRangeSlider.

Parameters:

Name Type Description Default
value_start float | ObservableProtocol[float]

Start value or observable value.

0.0
value_end float | ObservableProtocol[float]

End value or observable value.

1.0
on_change Optional[Callable[[Tuple[float, float]], None]]

Callback invoked when range changes.

None
min_value float

Minimum value.

0.0
max_value float

Maximum value.

1.0
stops Optional[int]

Discrete stop count. None means continuous.

None
show_value_indicator bool

Whether to show value indicator during drag.

False
disabled bool | ObservableProtocol[bool]

Disabled state.

False
width SizingLike

Main-axis (width) sizing.

'wt'
padding Optional[Tuple[int, int] | Tuple[int, int, int, int] | int]

Slider padding.

None
style Optional['SliderStyle']

Optional SliderStyle override.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/slider.py
def __init__(
    self,
    value_start: float | ObservableProtocol[float] = 0.0,
    value_end: float | ObservableProtocol[float] = 1.0,
    *,
    on_change: Optional[Callable[[Tuple[float, float]], None]] = None,
    min_value: float = 0.0,
    max_value: float = 1.0,
    stops: Optional[int] = None,
    show_value_indicator: bool = False,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = "wt",
    padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None,
    style: Optional["SliderStyle"] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize HorizontalRangeSlider.

    Args:
        value_start: Start value or observable value.
        value_end: End value or observable value.
        on_change: Callback invoked when range changes.
        min_value: Minimum value.
        max_value: Maximum value.
        stops: Discrete stop count. ``None`` means continuous.
        show_value_indicator: Whether to show value indicator during drag.
        disabled: Disabled state.
        width: Main-axis (width) sizing.
        padding: Slider padding.
        style: Optional SliderStyle override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        value_start,
        value_end,
        on_change=on_change,
        min_value=min_value,
        max_value=max_value,
        stops=stops,
        show_value_indicator=show_value_indicator,
        disabled=disabled,
        orientation=Orientation.HORIZONTAL,
        length=width,
        padding=padding,
        style=style,
        key=key,
    )

HorizontalSlider

HorizontalSlider(value: float | ObservableProtocol[float] = 0.0, *, on_change: Optional[Callable[[float], None]] = None, min_value: float = 0.0, max_value: float = 1.0, stops: Optional[int] = None, show_value_indicator: bool = False, disabled: bool | ObservableProtocol[bool] = False, width: SizingLike = 'wt', padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None, style: Optional['SliderStyle'] = None, key: Optional[str] = None)

Bases: _Slider

Material Design 3 horizontal slider. Sized with width.

Initialize HorizontalSlider.

Parameters:

Name Type Description Default
value float | ObservableProtocol[float]

Current slider value or observable value.

0.0
on_change Optional[Callable[[float], None]]

Callback invoked when value changes.

None
min_value float

Minimum value.

0.0
max_value float

Maximum value.

1.0
stops Optional[int]

Discrete stop count. None means continuous.

None
show_value_indicator bool

Whether to show value indicator during drag.

False
disabled bool | ObservableProtocol[bool]

Disabled state.

False
width SizingLike

Main-axis (width) sizing.

'wt'
padding Optional[Tuple[int, int] | Tuple[int, int, int, int] | int]

Slider padding.

None
style Optional['SliderStyle']

Optional SliderStyle override.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/slider.py
def __init__(
    self,
    value: float | ObservableProtocol[float] = 0.0,
    *,
    on_change: Optional[Callable[[float], None]] = None,
    min_value: float = 0.0,
    max_value: float = 1.0,
    stops: Optional[int] = None,
    show_value_indicator: bool = False,
    disabled: bool | ObservableProtocol[bool] = False,
    width: SizingLike = "wt",
    padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None,
    style: Optional["SliderStyle"] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize HorizontalSlider.

    Args:
        value: Current slider value or observable value.
        on_change: Callback invoked when value changes.
        min_value: Minimum value.
        max_value: Maximum value.
        stops: Discrete stop count. ``None`` means continuous.
        show_value_indicator: Whether to show value indicator during drag.
        disabled: Disabled state.
        width: Main-axis (width) sizing.
        padding: Slider padding.
        style: Optional SliderStyle override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        value,
        on_change=on_change,
        min_value=min_value,
        max_value=max_value,
        stops=stops,
        show_value_indicator=show_value_indicator,
        disabled=disabled,
        orientation=Orientation.HORIZONTAL,
        length=width,
        padding=padding,
        style=style,
        key=key,
    )

VerticalCenteredSlider

VerticalCenteredSlider(value: float | ObservableProtocol[float] = 0.0, *, on_change: Optional[Callable[[float], None]] = None, min_value: float = -1.0, max_value: float = 1.0, stops: Optional[int] = None, show_value_indicator: bool = False, disabled: bool | ObservableProtocol[bool] = False, height: SizingLike = 'wt', padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None, style: Optional['SliderStyle'] = None, key: Optional[str] = None)

Bases: _CenteredSlider

Material Design 3 vertical centered slider. Sized with height.

Initialize VerticalCenteredSlider.

Parameters:

Name Type Description Default
value float | ObservableProtocol[float]

Current slider value or observable value.

0.0
on_change Optional[Callable[[float], None]]

Callback invoked when value changes.

None
min_value float

Minimum value (default: -1.0).

-1.0
max_value float

Maximum value (default: 1.0).

1.0
stops Optional[int]

Discrete stop count. None means continuous.

None
show_value_indicator bool

Whether to show value indicator during drag.

False
disabled bool | ObservableProtocol[bool]

Disabled state.

False
height SizingLike

Main-axis (height) sizing.

'wt'
padding Optional[Tuple[int, int] | Tuple[int, int, int, int] | int]

Slider padding.

None
style Optional['SliderStyle']

Optional SliderStyle override.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/slider.py
def __init__(
    self,
    value: float | ObservableProtocol[float] = 0.0,
    *,
    on_change: Optional[Callable[[float], None]] = None,
    min_value: float = -1.0,
    max_value: float = 1.0,
    stops: Optional[int] = None,
    show_value_indicator: bool = False,
    disabled: bool | ObservableProtocol[bool] = False,
    height: SizingLike = "wt",
    padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None,
    style: Optional["SliderStyle"] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize VerticalCenteredSlider.

    Args:
        value: Current slider value or observable value.
        on_change: Callback invoked when value changes.
        min_value: Minimum value (default: -1.0).
        max_value: Maximum value (default: 1.0).
        stops: Discrete stop count. ``None`` means continuous.
        show_value_indicator: Whether to show value indicator during drag.
        disabled: Disabled state.
        height: Main-axis (height) sizing.
        padding: Slider padding.
        style: Optional SliderStyle override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        value=value,
        on_change=on_change,
        min_value=min_value,
        max_value=max_value,
        stops=stops,
        show_value_indicator=show_value_indicator,
        disabled=disabled,
        orientation=Orientation.VERTICAL,
        length=height,
        padding=padding,
        style=style,
        key=key,
    )

VerticalRangeSlider

VerticalRangeSlider(value_start: float | ObservableProtocol[float] = 0.0, value_end: float | ObservableProtocol[float] = 1.0, *, on_change: Optional[Callable[[Tuple[float, float]], None]] = None, min_value: float = 0.0, max_value: float = 1.0, stops: Optional[int] = None, show_value_indicator: bool = False, disabled: bool | ObservableProtocol[bool] = False, height: SizingLike = 'wt', padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None, style: Optional['SliderStyle'] = None, key: Optional[str] = None)

Bases: _RangeSlider

Material Design 3 vertical range slider. Sized with height.

Initialize VerticalRangeSlider.

Parameters:

Name Type Description Default
value_start float | ObservableProtocol[float]

Start value or observable value.

0.0
value_end float | ObservableProtocol[float]

End value or observable value.

1.0
on_change Optional[Callable[[Tuple[float, float]], None]]

Callback invoked when range changes.

None
min_value float

Minimum value.

0.0
max_value float

Maximum value.

1.0
stops Optional[int]

Discrete stop count. None means continuous.

None
show_value_indicator bool

Whether to show value indicator during drag.

False
disabled bool | ObservableProtocol[bool]

Disabled state.

False
height SizingLike

Main-axis (height) sizing.

'wt'
padding Optional[Tuple[int, int] | Tuple[int, int, int, int] | int]

Slider padding.

None
style Optional['SliderStyle']

Optional SliderStyle override.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/slider.py
def __init__(
    self,
    value_start: float | ObservableProtocol[float] = 0.0,
    value_end: float | ObservableProtocol[float] = 1.0,
    *,
    on_change: Optional[Callable[[Tuple[float, float]], None]] = None,
    min_value: float = 0.0,
    max_value: float = 1.0,
    stops: Optional[int] = None,
    show_value_indicator: bool = False,
    disabled: bool | ObservableProtocol[bool] = False,
    height: SizingLike = "wt",
    padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None,
    style: Optional["SliderStyle"] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize VerticalRangeSlider.

    Args:
        value_start: Start value or observable value.
        value_end: End value or observable value.
        on_change: Callback invoked when range changes.
        min_value: Minimum value.
        max_value: Maximum value.
        stops: Discrete stop count. ``None`` means continuous.
        show_value_indicator: Whether to show value indicator during drag.
        disabled: Disabled state.
        height: Main-axis (height) sizing.
        padding: Slider padding.
        style: Optional SliderStyle override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        value_start,
        value_end,
        on_change=on_change,
        min_value=min_value,
        max_value=max_value,
        stops=stops,
        show_value_indicator=show_value_indicator,
        disabled=disabled,
        orientation=Orientation.VERTICAL,
        length=height,
        padding=padding,
        style=style,
        key=key,
    )

VerticalSlider

VerticalSlider(value: float | ObservableProtocol[float] = 0.0, *, on_change: Optional[Callable[[float], None]] = None, min_value: float = 0.0, max_value: float = 1.0, stops: Optional[int] = None, show_value_indicator: bool = False, disabled: bool | ObservableProtocol[bool] = False, height: SizingLike = 'wt', padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None, style: Optional['SliderStyle'] = None, key: Optional[str] = None)

Bases: _Slider

Material Design 3 vertical slider. Sized with height.

Initialize VerticalSlider.

Parameters:

Name Type Description Default
value float | ObservableProtocol[float]

Current slider value or observable value.

0.0
on_change Optional[Callable[[float], None]]

Callback invoked when value changes.

None
min_value float

Minimum value.

0.0
max_value float

Maximum value.

1.0
stops Optional[int]

Discrete stop count. None means continuous.

None
show_value_indicator bool

Whether to show value indicator during drag.

False
disabled bool | ObservableProtocol[bool]

Disabled state.

False
height SizingLike

Main-axis (height) sizing.

'wt'
padding Optional[Tuple[int, int] | Tuple[int, int, int, int] | int]

Slider padding.

None
style Optional['SliderStyle']

Optional SliderStyle override.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/slider.py
def __init__(
    self,
    value: float | ObservableProtocol[float] = 0.0,
    *,
    on_change: Optional[Callable[[float], None]] = None,
    min_value: float = 0.0,
    max_value: float = 1.0,
    stops: Optional[int] = None,
    show_value_indicator: bool = False,
    disabled: bool | ObservableProtocol[bool] = False,
    height: SizingLike = "wt",
    padding: Optional[Tuple[int, int] | Tuple[int, int, int, int] | int] = None,
    style: Optional["SliderStyle"] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize VerticalSlider.

    Args:
        value: Current slider value or observable value.
        on_change: Callback invoked when value changes.
        min_value: Minimum value.
        max_value: Maximum value.
        stops: Discrete stop count. ``None`` means continuous.
        show_value_indicator: Whether to show value indicator during drag.
        disabled: Disabled state.
        height: Main-axis (height) sizing.
        padding: Slider padding.
        style: Optional SliderStyle override.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        value,
        on_change=on_change,
        min_value=min_value,
        max_value=max_value,
        stops=stops,
        show_value_indicator=show_value_indicator,
        disabled=disabled,
        orientation=Orientation.VERTICAL,
        length=height,
        padding=padding,
        style=style,
        key=key,
    )

Symbol dataclass

Symbol(name: str, codepoint: str)

Material symbol descriptor.

Symbols

Material Symbols constants (auto-generated).

TextField

TextField(value: Union[str, ReadOnlyObservableProtocol[str]] = '', *, on_change: Optional[Callable[[str], None]] = None, on_submit: Optional[Callable[[str], None]] = None, on_focus_change: Optional[FocusChangeCallback] = None, input_filter: Optional[InputFilterLike] = None, label: str | ReadOnlyObservableProtocol[str] | None = None, leading_icon: Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None = None, on_tap_leading_icon: Optional[Callable[[], None]] = None, trailing_icon: Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None = None, on_tap_trailing_icon: Optional[Callable[[], None]] = None, obscure_text: bool = False, supporting_text: str | ReadOnlyObservableProtocol[str | None] | None = None, is_error: bool | ReadOnlyObservableProtocol[bool] = False, disabled: bool | ReadOnlyObservableProtocol[bool] = False, width: SizingLike = 200, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, style: Optional[TextFieldStyle] = None, key: Optional[str] = None)

Bases: InteractiveWidget

A Material Design 3 text field; style picks filled or outlined.

An observable passed as value holds the field's value, the same as for every other input widget: it is displayed, and edits are written back to it. A read-only observable (.map(...), a computed value) has nowhere to write, so it displays only -- pair it with disabled=True to make that visible to the user.

For a field that takes several lines: multiline.

Initialize TextField.

Parameters:

Name Type Description Default
value Union[str, ReadOnlyObservableProtocol[str]]

Initial text, or the observable holding the field's value. Edits are written back to a writable observable.

''
on_change Optional[Callable[[str], None]]

Callback invoked with the text as it changes, for a side effect of the change. The observable bound to value is already updated without it, and carries the same signal: the two are announced together, so neither reports the provisional text of an unconfirmed IME composition.

None
on_submit Optional[Callable[[str], None]]

Callback invoked with the text when the user presses Enter. Fires on every press, including a repeat on an unchanged value, and never on focus loss -- it reports a request to act, not a value settling. To react to the user leaving the field, use on_focus_change. Shift+Enter never submits.

None
on_focus_change Optional[FocusChangeCallback]

Callback invoked as focus arrives and leaves, with (focused, source) -- the same signature as the focusable() modifier. This is where blur-triggered work belongs: validating once the user has left, saving an inline edit, finishing a half-typed value. It can arrive more than once with focused=True for a single acquisition, because the source is re-announced when the user switches from keyboard to pointer; focused=False arrives once.

None
input_filter Optional[InputFilterLike]

Rule applied to text as the user types it -- see :mod:nuiitivet.widgets.input_filter. It governs what is typeable; whether a finished value is acceptable belongs in is_error / supporting_text, and reshaping a finished value belongs in on_submit.

None
label str | ReadOnlyObservableProtocol[str] | None

Floating label text. A filled field shorter than 52dp shows it only while the field is empty.

None
leading_icon Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None

Icon displayed before the text.

None
on_tap_leading_icon Optional[Callable[[], None]]

Callback invoked when the leading icon is tapped. With it the icon is a standard icon button -- hover, focus and pressed feedback, keyboard focusable; without it the icon is a plain glyph.

None
trailing_icon Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None

Icon displayed after the text.

None
on_tap_trailing_icon Optional[Callable[[], None]]

Callback invoked when the trailing icon is tapped; the icon changes as for on_tap_leading_icon.

None
obscure_text bool

Whether to mask text display (password-style).

False
supporting_text str | ReadOnlyObservableProtocol[str | None] | None

Supporting text displayed below the field.

None
is_error bool | ReadOnlyObservableProtocol[bool]

Whether the field is in its error state. This is a visual axis of the whole field -- outline, label, cursor and supporting text all change -- and is independent of what supporting_text says, so a field can be flagged without a message and carry a message without being flagged.

False
disabled bool | ReadOnlyObservableProtocol[bool]

Whether the text field is disabled.

False
width SizingLike

Width specification.

200
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Insets from the allocated rect to the field container.

0
style Optional[TextFieldStyle]

Custom style configuration.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/text_fields.py
def __init__(
    self,
    value: Union[str, ReadOnlyObservableProtocol[str]] = "",
    *,
    on_change: Optional[Callable[[str], None]] = None,
    on_submit: Optional[Callable[[str], None]] = None,
    on_focus_change: Optional[FocusChangeCallback] = None,
    input_filter: Optional[InputFilterLike] = None,
    label: str | ReadOnlyObservableProtocol[str] | None = None,
    leading_icon: Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None = None,
    on_tap_leading_icon: Optional[Callable[[], None]] = None,
    trailing_icon: (
        Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None
    ) = None,
    on_tap_trailing_icon: Optional[Callable[[], None]] = None,
    obscure_text: bool = False,
    supporting_text: str | ReadOnlyObservableProtocol[str | None] | None = None,
    is_error: bool | ReadOnlyObservableProtocol[bool] = False,
    disabled: bool | ReadOnlyObservableProtocol[bool] = False,
    width: SizingLike = 200,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    style: Optional[TextFieldStyle] = None,
    key: Optional[str] = None,
):
    """Initialize TextField.

    Args:
        value: Initial text, or the observable holding the field's value.
            Edits are written back to a writable observable.
        on_change: Callback invoked with the text as it changes, for a
            side effect of the change. The observable bound to *value* is
            already updated without it, and carries the same signal: the
            two are announced together, so neither reports the provisional
            text of an unconfirmed IME composition.
        on_submit: Callback invoked with the text when the user presses
            Enter. Fires on every press, including a repeat on an unchanged
            value, and never on focus loss -- it reports a request to act,
            not a value settling. To react to the user leaving the field,
            use *on_focus_change*. Shift+Enter never submits.
        on_focus_change: Callback invoked as focus arrives and leaves,
            with ``(focused, source)`` -- the same signature as the
            ``focusable()`` modifier. This is where blur-triggered work
            belongs: validating once the user has left, saving an inline
            edit, finishing a half-typed value. It can arrive more than
            once with ``focused=True`` for a single acquisition, because
            the *source* is re-announced when the user switches from
            keyboard to pointer; ``focused=False`` arrives once.
        input_filter: Rule applied to text as the user types it -- see
            :mod:`nuiitivet.widgets.input_filter`. It governs what is
            typeable; whether a finished value is acceptable belongs in
            *is_error* / *supporting_text*, and reshaping a finished
            value belongs in *on_submit*.
        label: Floating label text. A filled field shorter than 52dp shows it
            only while the field is empty.
        leading_icon: Icon displayed before the text.
        on_tap_leading_icon: Callback invoked when the leading icon is
            tapped. With it the icon is a standard icon button -- hover,
            focus and pressed feedback, keyboard focusable; without it
            the icon is a plain glyph.
        trailing_icon: Icon displayed after the text.
        on_tap_trailing_icon: Callback invoked when the trailing icon is
            tapped; the icon changes as for *on_tap_leading_icon*.
        obscure_text: Whether to mask text display (password-style).
        supporting_text: Supporting text displayed below the field.
        is_error: Whether the field is in its error state. This is a
            visual axis of the whole field -- outline, label, cursor and
            supporting text all change -- and is independent of what
            *supporting_text* says, so a field can be flagged without a
            message and carry a message without being flagged.
        disabled: Whether the text field is disabled.
        width: Width specification.
        padding: Insets from the allocated rect to the field container.
        style: Custom style configuration.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        key=key,
        width=width,
        padding=padding,
        state_layer_color=ColorRole.ON_SURFACE,
        disabled=False,  # Set initial disabled state below
        # The TextField itself does not host a FocusNode. The focus
        # subject is the inner EditableText; mirroring it here would
        # cause focus ping-pong between two FocusNodes during pointer
        # press handling and produce stale ``_focus_from_pointer``
        # state. The visual focus indicator (ring / state layer) reads
        # the editable's focus state directly via
        # ``should_show_focus_ring``. Pointer presses are routed to the
        # editable in ``_handle_press`` below; keyboard Tab traversal
        # collects the editable's FocusNode directly because the
        # TextField has none.
        focusable=False,
    )

    self._label_source: ReadOnlyObservableProtocol[str] | None = None
    self._supporting_text_source: ReadOnlyObservableProtocol[str | None] | None = None
    self._is_error_source: ReadOnlyObservableProtocol[bool] | None = None
    self._disabled_source: ReadOnlyObservableProtocol[bool] | None = None

    label_value: str | None
    if hasattr(label, "subscribe") and hasattr(label, "value"):
        self._label_source = cast("ReadOnlyObservableProtocol[str]", label)
        try:
            label_value = str(self._label_source.value)
        except Exception:
            label_value = None
    else:
        label_value = str(label) if label is not None else None

    supporting_text_value: str | None
    if hasattr(supporting_text, "subscribe") and hasattr(supporting_text, "value"):
        self._supporting_text_source = cast("ReadOnlyObservableProtocol[str | None]", supporting_text)
        try:
            v = self._supporting_text_source.value
            supporting_text_value = str(v) if v is not None else None
        except Exception:
            supporting_text_value = None
    else:
        supporting_text_value = str(supporting_text) if supporting_text is not None else None

    initial_is_error: bool
    if hasattr(is_error, "subscribe") and hasattr(is_error, "value"):
        self._is_error_source = cast("ReadOnlyObservableProtocol[bool]", is_error)
        try:
            initial_is_error = bool(self._is_error_source.value)
        except Exception:
            initial_is_error = False
    else:
        initial_is_error = bool(is_error)

    initial_disabled: bool
    if hasattr(disabled, "subscribe") and hasattr(disabled, "value"):
        self._disabled_source = cast("ReadOnlyObservableProtocol[bool]", disabled)
        try:
            initial_disabled = bool(self._disabled_source.value)
        except Exception:
            initial_disabled = False
    else:
        initial_disabled = bool(disabled)

    self.label = label_value
    self._on_tap_leading_icon = on_tap_leading_icon
    self._on_tap_trailing_icon = on_tap_trailing_icon

    if on_tap_leading_icon is not None and leading_icon is None:
        raise ValueError("on_tap_leading_icon requires leading_icon to be provided")
    if on_tap_trailing_icon is not None and trailing_icon is None:
        raise ValueError("on_tap_trailing_icon requires trailing_icon to be provided")

    # A tap callback upgrades the icon to a standard IconButton (state
    # layers + keyboard focus); a decorative icon stays a plain Icon.
    self.leading_icon = _build_text_field_icon(
        leading_icon, arg_name="leading_icon", on_tap=on_tap_leading_icon
    )
    self.trailing_icon = _build_text_field_icon(
        trailing_icon, arg_name="trailing_icon", on_tap=on_tap_trailing_icon
    )
    self.supporting_text = supporting_text_value
    self.is_error = initial_is_error

    self._user_style = style
    self._dense_style: Optional[Tuple[TextFieldStyle, int, TextFieldStyle]] = None

    self._on_change = on_change
    self._on_submit = on_submit
    self._on_focus_change = on_focus_change

    # Children
    if self.leading_icon is not None:
        self.add_child(self.leading_icon)
    if self.trailing_icon is not None:
        self.add_child(self.trailing_icon)

    # EditableText
    style = self._dense
    self._editable = EditableText(
        value=value,
        on_change=self._handle_editable_change,
        on_focus_change=self._on_editable_focus_change,
        # Only forward a handler when there is something to submit to.
        # EditableText claims Enter iff it has an on_submit, and declines it
        # otherwise so the key can reach a shortcut — a dialog's default
        # action. Wrapping unconditionally would claim Enter and drop it.
        on_submit=self._handle_editable_submit if on_submit is not None else None,
        input_filter=input_filter,
        text_color=style.text_color,
        cursor_color=style.error_cursor_color if self.is_error else style.cursor_color,
        selection_color=style.selection_color,
        font_size=16,  # BodyLarge
        disabled=initial_disabled,
        obscure_text=bool(obscure_text),
    )
    self.add_child(self._editable)

    # Animation state
    has_text = bool(self._editable.value)
    # Each of these is subscribed on mount, not here: an Animatable that is
    # running holds its ticker on the clock, so a bare subscribe left every
    # unmounted TextField reachable and repainting. See on_mount below.
    self._label_progress = Animatable(
        1.0 if has_text else 0.0,
        motion=EXPRESSIVE_DEFAULT_EFFECTS,
    )

    # Indicator Animations
    init_ind_width = style.indicator_width
    self._anim_indicator_width = Animatable(
        float(init_ind_width),
        motion=EXPRESSIVE_DEFAULT_EFFECTS,
    )

    init_ind_color = resolve_color_to_rgba(style.indicator_color, theme=None)
    self._anim_indicator_color = Animatable.vector(
        init_ind_color,
        converter=RgbaTupleConverter(),
        motion=EXPRESSIVE_DEFAULT_EFFECTS,
    )

    init_label_color = resolve_color_to_rgba(style.label_color, theme=None)
    self._anim_label_color = Animatable.vector(
        init_label_color,
        converter=RgbaTupleConverter(),
        motion=EXPRESSIVE_DEFAULT_EFFECTS,
    )

    # Handle initial disabled state
    if initial_disabled:
        self._apply_disabled(True)

    # Initialize label state
    self._update_label_state()

    # Preserve existing click behavior while adding press handling for icon taps.
    self.enable_click(on_press=self._handle_press)

should_show_focus_ring property

should_show_focus_ring: bool

Show the focus ring only when focus arrived via keyboard navigation.

The actual focus subject is self._editable; the host TextField does not own a FocusNode (see focusable=False in init). EditableText exposes is_focus_from_pointer so that, per MD3 spec, the ring is suppressed for clicks.

style property

The style in effect: the caller's, else the theme's, else the default, with the MD3 baseline numbers.

is_multiline property

is_multiline: bool

Whether the field takes several lines, as built by multiline.

min_lines property

min_lines: int

Lines shown when the text has fewer.

max_lines property

max_lines: Optional[int]

Lines shown before the field scrolls; None is unbounded.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:TextFieldStyle's presets.

Source code in src/nuiitivet/material/text_fields.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`TextFieldStyle`'s presets."""
    return {"style": TextFieldStyle}

multiline classmethod

multiline(value: Union[str, ReadOnlyObservableProtocol[str]] = '', *, min_lines: int = 1, max_lines: Optional[int] = None, on_change: Optional[Callable[[str], None]] = None, on_submit: Optional[Callable[[str], None]] = None, on_focus_change: Optional[FocusChangeCallback] = None, input_filter: Optional[InputFilterLike] = None, label: str | ReadOnlyObservableProtocol[str] | None = None, leading_icon: Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None = None, on_tap_leading_icon: Optional[Callable[[], None]] = None, trailing_icon: Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None = None, on_tap_trailing_icon: Optional[Callable[[], None]] = None, supporting_text: str | ReadOnlyObservableProtocol[str | None] | None = None, is_error: bool | ReadOnlyObservableProtocol[bool] = False, disabled: bool | ReadOnlyObservableProtocol[bool] = False, width: SizingLike = 200, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, style: Optional[TextFieldStyle] = None, key: Optional[str] = None) -> 'TextField'

A text field that takes several lines.

The text wraps at the width. Shift+Enter breaks the line; Enter submits through on_submit and does nothing without one. The field shows min_lines lines when the text has fewer, grows a line at a time up to max_lines, and then scrolls the caret's line into view. The first row is laid out as a single-line field: the resting label and the icons sit there.

Parameters:

Name Type Description Default
value Union[str, ReadOnlyObservableProtocol[str]]

Initial text, or the observable holding the field's value. Edits are written back to a writable observable.

''
min_lines int

Lines shown when the text has fewer, at least 1.

1
max_lines Optional[int]

Lines shown before the field scrolls, at least min_lines; None grows without bound. Neither bounds the text's own line count: that is an input_filter.

None
on_change Optional[Callable[[str], None]]

Callback invoked with the text as it changes, for a side effect of the change. Not during an IME composition.

None
on_submit Optional[Callable[[str], None]]

Callback invoked with the text on a bare Enter, on every press, and never on focus loss.

None
on_focus_change Optional[FocusChangeCallback]

Callback invoked as focus arrives and leaves, with (focused, source).

None
input_filter Optional[InputFilterLike]

Rule applied to text as the user types it, a line break included: deny(r"\n") keeps a wrapping field to one paragraph.

None
label str | ReadOnlyObservableProtocol[str] | None

Floating label text. A filled field shorter than 52dp shows it only while the field is empty.

None
leading_icon Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None

Icon displayed before the text, in the first row.

None
on_tap_leading_icon Optional[Callable[[], None]]

Callback invoked when the leading icon is tapped; with it the icon is a standard icon button.

None
trailing_icon Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None

Icon displayed after the text, in the first row.

None
on_tap_trailing_icon Optional[Callable[[], None]]

Callback invoked when the trailing icon is tapped; the icon changes as for on_tap_leading_icon.

None
supporting_text str | ReadOnlyObservableProtocol[str | None] | None

Supporting text displayed below the field.

None
is_error bool | ReadOnlyObservableProtocol[bool]

Whether the field is in its error state.

False
disabled bool | ReadOnlyObservableProtocol[bool]

Whether the text field is disabled.

False
width SizingLike

Width specification.

200
padding Union[int, Tuple[int, int], Tuple[int, int, int, int]]

Insets from the allocated rect to the field container.

0
style Optional[TextFieldStyle]

Custom style configuration.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/text_fields.py
@classmethod
def multiline(
    cls,
    value: Union[str, ReadOnlyObservableProtocol[str]] = "",
    *,
    min_lines: int = 1,
    max_lines: Optional[int] = None,
    on_change: Optional[Callable[[str], None]] = None,
    on_submit: Optional[Callable[[str], None]] = None,
    on_focus_change: Optional[FocusChangeCallback] = None,
    input_filter: Optional[InputFilterLike] = None,
    label: str | ReadOnlyObservableProtocol[str] | None = None,
    leading_icon: Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None = None,
    on_tap_leading_icon: Optional[Callable[[], None]] = None,
    trailing_icon: (
        Symbol | str | ReadOnlyObservableProtocol[Symbol] | ReadOnlyObservableProtocol[str] | None
    ) = None,
    on_tap_trailing_icon: Optional[Callable[[], None]] = None,
    supporting_text: str | ReadOnlyObservableProtocol[str | None] | None = None,
    is_error: bool | ReadOnlyObservableProtocol[bool] = False,
    disabled: bool | ReadOnlyObservableProtocol[bool] = False,
    width: SizingLike = 200,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    style: Optional[TextFieldStyle] = None,
    key: Optional[str] = None,
) -> "TextField":
    """A text field that takes several lines.

    The text wraps at the width. Shift+Enter breaks the line; Enter
    submits through *on_submit* and does nothing without one. The field
    shows *min_lines* lines when the text has fewer, grows a line at a
    time up to *max_lines*, and then scrolls the caret's line into view.
    The first row is laid out as a single-line field: the resting label
    and the icons sit there.

    Args:
        value: Initial text, or the observable holding the field's value.
            Edits are written back to a writable observable.
        min_lines: Lines shown when the text has fewer, at least 1.
        max_lines: Lines shown before the field scrolls, at least
            *min_lines*; ``None`` grows without bound. Neither bounds the
            text's own line count: that is an *input_filter*.
        on_change: Callback invoked with the text as it changes, for a
            side effect of the change. Not during an IME composition.
        on_submit: Callback invoked with the text on a bare Enter, on
            every press, and never on focus loss.
        on_focus_change: Callback invoked as focus arrives and leaves,
            with ``(focused, source)``.
        input_filter: Rule applied to text as the user types it, a line
            break included: ``deny(r"\\n")`` keeps a wrapping field to one
            paragraph.
        label: Floating label text. A filled field shorter than 52dp shows it
            only while the field is empty.
        leading_icon: Icon displayed before the text, in the first row.
        on_tap_leading_icon: Callback invoked when the leading icon is
            tapped; with it the icon is a standard icon button.
        trailing_icon: Icon displayed after the text, in the first row.
        on_tap_trailing_icon: Callback invoked when the trailing icon is
            tapped; the icon changes as for *on_tap_leading_icon*.
        supporting_text: Supporting text displayed below the field.
        is_error: Whether the field is in its error state.
        disabled: Whether the text field is disabled.
        width: Width specification.
        padding: Insets from the allocated rect to the field container.
        style: Custom style configuration.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    field = cls(
        value,
        on_change=on_change,
        on_submit=on_submit,
        on_focus_change=on_focus_change,
        input_filter=input_filter,
        label=label,
        leading_icon=leading_icon,
        on_tap_leading_icon=on_tap_leading_icon,
        trailing_icon=trailing_icon,
        on_tap_trailing_icon=on_tap_trailing_icon,
        supporting_text=supporting_text,
        is_error=is_error,
        disabled=disabled,
        width=width,
        padding=padding,
        style=style,
        key=key,
    )
    field._editable.set_lines(True, min_lines, max_lines)
    return field

preferred_size

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

Return the preferred (width, height) for this TextField.

Source code in src/nuiitivet/material/text_fields.py
def preferred_size(self, max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]:
    """Return the preferred (width, height) for this TextField."""
    w_dim = self.width_sizing
    h_dim = self.height_sizing

    default_width = 200

    font = self._get_font()
    style = self._dense
    if not style:
        return (default_width, TextFieldStyle.container_height)
    default_height = style.container_height

    pl, pt, pr, pb = style.content_insets

    icon_w = 0
    if self.leading_icon:
        lw, _ = self.leading_icon.preferred_size()
        icon_w += lw + 24
    if self.trailing_icon:
        tw, _ = self.trailing_icon.preferred_size()
        icon_w += tw + 24

    if w_dim.kind == "fixed":
        width = int(w_dim.value)
    else:
        if font:
            char_width = font.measureText("M")
            width = int(char_width * 15) + pl + pr + icon_w
        else:
            width = default_width

    if h_dim.kind == "fixed":
        height = int(h_dim.value)
    else:
        height = default_height
        if self.is_multiline:
            # The first row is the single-line height; each further line
            # adds one line of text.
            text_w = width - pl - pr - icon_w
            height += self._editable.line_height() * (self._grown_lines(text_w) - 1)
        if self.supporting_text and font:
            font.setSize(12)
            metrics = font.getMetrics()
            error_h = -metrics.fAscent + metrics.fDescent
            height += int(error_h + 4)

    l, t, r, b = self.padding
    total_w = width + l + r
    total_h = height + t + b

    if max_width is not None:
        total_w = min(int(total_w), int(max_width))
    if max_height is not None:
        total_h = min(int(total_h), int(max_height))

    return (int(total_w), int(total_h))

focus

focus() -> None

Programmatically focus the TextField (keyboard-style focus).

Delegates to the inner EditableText. This path does NOT mark the focus as pointer-driven, so the focus ring will be shown — use this for keyboard / API-initiated focus only. Pointer presses go through _handle_press which calls EditableText.request_focus_from_pointer to suppress the ring per MD3 spec.

Source code in src/nuiitivet/material/text_fields.py
def focus(self) -> None:
    """Programmatically focus the TextField (keyboard-style focus).

    Delegates to the inner EditableText. This path does NOT mark the
    focus as pointer-driven, so the focus ring will be shown — use
    this for keyboard / API-initiated focus only. Pointer presses go
    through ``_handle_press`` which calls
    ``EditableText.request_focus_from_pointer`` to suppress the ring
    per MD3 spec.
    """
    self._editable.focus()

TextFieldStyle dataclass

TextFieldStyle(mode: TextFieldMode = 'filled', container_color: ColorSpec = SURFACE_CONTAINER_HIGHEST, indicator_color: ColorSpec = ON_SURFACE_VARIANT, indicator_width: float = 1.0, focused_indicator_color: ColorSpec = PRIMARY, focused_indicator_width: float = 2.0, error_indicator_color: ColorSpec = ERROR, text_color: ColorSpec = ON_SURFACE, label_color: ColorSpec = ON_SURFACE_VARIANT, focused_label_color: ColorSpec = PRIMARY, error_label_color: ColorSpec = ERROR, supporting_text_color: ColorSpec = ON_SURFACE_VARIANT, error_supporting_text_color: ColorSpec = ERROR, cursor_color: ColorSpec = PRIMARY, error_cursor_color: ColorSpec = ERROR, selection_color: ColorSpec = PRIMARY_CONTAINER, border_radius: float = 4.0, container_height: int = 56, content_insets: Tuple[int, int, int, int] = (16, 16, 16, 16), density: Optional[int] = None)

Style configuration for :class:TextField (M3-compliant).

The visual variant is captured by the :attr:mode field: "filled" draws an underline indicator with top-rounded container corners while "outlined" draws a full rectangular border. Use the :meth:filled and :meth:outlined factory methods to obtain the standard presets.

copy_with

copy_with(**changes) -> 'TextFieldStyle'

Create a new style instance with specified fields changed.

Source code in src/nuiitivet/material/styles/text_field_style.py
def copy_with(self, **changes) -> "TextFieldStyle":
    """Create a new style instance with specified fields changed."""
    return replace(self, **changes)

default classmethod

default() -> 'TextFieldStyle'

Return the framework default, ignoring any theme.

This is what a text field renders with before it is mounted, and what :meth:from_theme falls back to when no Material theme is installed.

Returns:

Type Description
'TextFieldStyle'

The filled text field style.

Source code in src/nuiitivet/material/styles/text_field_style.py
@classmethod
def default(cls) -> "TextFieldStyle":
    """Return the framework default, ignoring any theme.

    This is what a text field renders with before it is mounted, and
    what :meth:`from_theme` falls back to when no Material theme is
    installed.

    Returns:
        The filled text field style.
    """
    return cls.filled()

filled classmethod

filled() -> 'TextFieldStyle'

Default M3 Filled TextField style.

Source code in src/nuiitivet/material/styles/text_field_style.py
@classmethod
def filled(cls) -> "TextFieldStyle":
    """Default M3 Filled TextField style."""
    return cls(
        mode="filled",
        container_color=ColorRole.SURFACE_CONTAINER_HIGHEST,
        indicator_color=ColorRole.ON_SURFACE_VARIANT,
        border_radius=4.0,
        content_insets=(16, 8, 16, 8),  # Adjusted for label
    )

outlined classmethod

outlined() -> 'TextFieldStyle'

Default M3 Outlined TextField style.

Source code in src/nuiitivet/material/styles/text_field_style.py
@classmethod
def outlined(cls) -> "TextFieldStyle":
    """Default M3 Outlined TextField style."""
    return cls(
        mode="outlined",
        container_color=(0, 0, 0, 0),  # Transparent
        indicator_color=ColorRole.OUTLINE,
        focused_indicator_width=3.0,  # MD3: focused outline width = 3dp
        border_radius=4.0,
        content_insets=(16, 16, 16, 16),
    )

from_theme classmethod

from_theme(theme: 'Theme') -> 'TextFieldStyle'

Resolve the default :class:TextFieldStyle for the given theme.

Returns the theme's filled text field style if a Material theme extension is present, otherwise a fresh :meth:filled preset.

Source code in src/nuiitivet/material/styles/text_field_style.py
@classmethod
def from_theme(cls, theme: "Theme") -> "TextFieldStyle":
    """Resolve the default :class:`TextFieldStyle` for the given theme.

    Returns the theme's filled text field style if a Material theme
    extension is present, otherwise a fresh :meth:`filled` preset.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    theme_data = theme.extension(MaterialThemeData)
    if theme_data:
        return theme_data.filled_text_field_style
    return cls.default()

DockedSearchBar

DockedSearchBar(value: Union[str, ReadOnlyObservableProtocol[str]] = '', *, content: Widget, is_open: Optional[Observable[bool]] = None, close_on_enter: bool = True, placeholder: str | None = None, leading_icon: IconLike = 'search', on_tap_leading_icon: Optional[Callable[[], None]] = None, trailing_icon: IconLike = None, on_tap_trailing_icon: Optional[Callable[[], None]] = None, on_change: Optional[Callable[[str], None]] = None, on_submit: Optional[Callable[[str], None]] = None, on_focus_change: Optional[FocusChangeCallback] = None, input_filter: Optional[InputFilterLike] = None, width: SizingLike = None, style: Optional[DockedSearchBarStyle] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget

Material Design 3 docked search (contained variant).

A :class:SearchBar with a container anchored 2dp below it, holding whatever the query currently calls for — recent searches, suggestions, results, a spinner, "no matches". There is one slot, content, and the application swaps what is inside it from its own observables; the widget only shows and hides the container. That also means the query pipeline keeps running while the container is closed, so gate it yourself if that matters.

When the container opens and closes. The state is a single observable, writable at any time by the application, that this widget drives from these triggers:

==================================== ================================== Trigger Effect ==================================== ================================== Focus gained Open — including on an empty query, which is where MD3 shows recent searches A tap on the bar Open — focus cannot re-fire when the bar already holds it, so the tap itself is a trigger User edits the text Open, even if it was just closed Enter Close when close_on_enter Escape Close, leaving the bar focused — typing reopens. With nothing to close, Escape is not claimed Focus lost, or a tap outside Close. The bar is not outside: a tap there moves the caret and the container stays up ==================================== ==================================

The edit trigger is what makes "Enter closes the panel, results render on the page" work: focus never changed, so without it the panel could not come back. It counts user edits only — assigning to value, or a write to the bound observable, does not reopen the container, so filling the bar in after a pick stays closed. The tap trigger covers the pointer half of the same gap.

Escape rides the window's back-event path, which closes the topmost overlay entry — this container, when it is open — without moving focus. When the container is closed the path declines the key, so an enclosing handler (a dialog, the navigator) still sees it.

Initialize DockedSearchBar.

Parameters:

Name Type Description Default
value Union[str, ReadOnlyObservableProtocol[str]]

Initial query text, or the observable holding it.

''
content Widget

Widget rendered inside the docked container.

required
is_open Optional[Observable[bool]]

Observable holding whether the container is open. Pass one to drive or observe it; when omitted an internal one is created and exposed as :attr:is_open.

None
close_on_enter bool

Whether Enter closes the container. The default suits a page that renders its own results; pass False to keep the container up and swap content to the results instead. The close runs before on_submit, so a search that wants the container to stay up can reopen it from inside its own callback.

True
placeholder str | None

Supporting text shown inside the bar while it is empty.

None
leading_icon IconLike

Icon source for the leading slot.

'search'
on_tap_leading_icon Optional[Callable[[], None]]

Makes the leading icon a tappable icon button.

None
trailing_icon IconLike

Icon source for the trailing slot.

None
on_tap_trailing_icon Optional[Callable[[], None]]

Makes the trailing icon a tappable icon button.

None
on_change Optional[Callable[[str], None]]

Callback invoked with the query as it changes, for a side effect of the change. The observable bound to value carries the same signal without it.

None
on_submit Optional[Callable[[str], None]]

Callback invoked with the query when Enter is pressed. Fires on every press, including a repeat on an unchanged query, and never on focus loss.

None
on_focus_change Optional[FocusChangeCallback]

Callback invoked as focus arrives and leaves, with (focused, source) -- the same signature as focusable(). It can arrive more than once with focused=True for a single acquisition, because the source is re-announced when the user switches from keyboard to pointer; focused=False arrives once.

None
input_filter Optional[InputFilterLike]

Rule applied to text as the user types it.

None
width SizingLike

Sizing for the box — see :class:SearchBar.

None
padding PaddingLike

Insets from the allocated rect to the box.

0
style Optional[DockedSearchBarStyle]

Custom style configuration.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/search.py
def __init__(
    self,
    value: Union[str, ReadOnlyObservableProtocol[str]] = "",
    *,
    content: Widget,
    is_open: Optional[Observable[bool]] = None,
    close_on_enter: bool = True,
    placeholder: str | None = None,
    leading_icon: IconLike = "search",
    on_tap_leading_icon: Optional[Callable[[], None]] = None,
    trailing_icon: IconLike = None,
    on_tap_trailing_icon: Optional[Callable[[], None]] = None,
    on_change: Optional[Callable[[str], None]] = None,
    on_submit: Optional[Callable[[str], None]] = None,
    on_focus_change: Optional[FocusChangeCallback] = None,
    input_filter: Optional[InputFilterLike] = None,
    width: SizingLike = None,
    style: Optional[DockedSearchBarStyle] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize DockedSearchBar.

    Args:
        value: Initial query text, or the observable holding it.
        content: Widget rendered inside the docked container.
        is_open: Observable holding whether the container is open. Pass one to
            drive or observe it; when omitted an internal one is created and
            exposed as :attr:`is_open`.
        close_on_enter: Whether Enter closes the container. The default suits
            a page that renders its own results; pass ``False`` to keep the
            container up and swap ``content`` to the results instead. The
            close runs before *on_submit*, so a search that wants the
            container to stay up can reopen it from inside its own callback.
        placeholder: Supporting text shown inside the bar while it is empty.
        leading_icon: Icon source for the leading slot.
        on_tap_leading_icon: Makes the leading icon a tappable icon button.
        trailing_icon: Icon source for the trailing slot.
        on_tap_trailing_icon: Makes the trailing icon a tappable icon button.
        on_change: Callback invoked with the query as it changes, for a side
            effect of the change. The observable bound to *value* carries the
            same signal without it.
        on_submit: Callback invoked with the query when Enter is pressed.
            Fires on every press, including a repeat on an unchanged query,
            and never on focus loss.
        on_focus_change: Callback invoked as focus arrives and leaves, with
            ``(focused, source)`` -- the same signature as ``focusable()``. It
            can arrive more than once with ``focused=True`` for a single
            acquisition, because the *source* is re-announced when the user
            switches from keyboard to pointer; ``focused=False`` arrives once.
        input_filter: Rule applied to text as the user types it.
        width: Sizing for the box — see :class:`SearchBar`.
        padding: Insets from the allocated rect to the box.
        style: Custom style configuration.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)
    self._width = width
    self._style = style if style is not None else DockedSearchBarStyle()
    self._close_on_enter = bool(close_on_enter)
    self._on_submit = on_submit
    self._on_focus_change = on_focus_change

    # Kept separate from ``core.focused`` so that an outside tap can close
    # the container (popup writes False here) without claiming the bar lost
    # focus.
    self._is_open: Observable[bool] = is_open if is_open is not None else Observable(False)

    # EditableText declines Enter when it has no on_submit, so the key
    # stays available to a shortcut. The wrapper is withheld unless Enter
    # has something to do here -- the app's callback, closing the
    # container, or both.
    wants_enter = on_submit is not None or self._close_on_enter

    self._core = _SearchBarCore(
        value=value,
        placeholder=placeholder,
        leading_icon=leading_icon,
        on_tap_leading_icon=on_tap_leading_icon,
        trailing_icon=trailing_icon,
        on_tap_trailing_icon=on_tap_trailing_icon,
        on_change=on_change,
        on_user_edit=self._handle_user_edit,
        on_submit=self._handle_submit if wants_enter else None,
        on_focus_change=self._handle_focus_change,
        input_filter=input_filter,
        style=self._style.bar,
    )

    self._container = _DockedContainer(
        content,
        style=self._style,
        bar_rect=lambda: self._core.global_visual_rect,
        viewport_height=self._viewport_height,
    )

value property

value: str

Return the current query text.

is_open property

is_open: Observable[bool]

Observable holding whether the docked container is open.

Writable: setting it opens or closes the container directly. The widget writes it too, on the triggers listed in the class docstring.

focus

focus() -> None

Programmatically focus the bar.

Source code in src/nuiitivet/material/search.py
def focus(self) -> None:
    """Programmatically focus the bar."""
    self._core.focus()

build

build() -> Widget

Return the pane, with a popup anchored to the inset bar.

The popup's offset is constant: OverlayPosition.anchored re-resolves the anchor rect on every layout pass, so anchoring to the bar (rather than to the pane) tracks the margin animation for free.

flip=False keeps the container below the bar even when the window is too short for it: it overflows downwards rather than opening upwards. Opening above would also be correct MD3 — turn it on if that is wanted. Either way the bar is never covered, because popup only ever shifts content along the cross axis.

Source code in src/nuiitivet/material/search.py
def build(self) -> Widget:
    """Return the pane, with a popup anchored to the inset bar.

    The popup's ``offset`` is constant: ``OverlayPosition.anchored``
    re-resolves the anchor rect on every layout pass, so anchoring to the
    bar (rather than to the pane) tracks the margin animation for free.

    ``flip=False`` keeps the container below the bar even when the window
    is too short for it: it overflows downwards rather than opening
    upwards. Opening above would also be correct MD3 — turn it on if that
    is wanted. Either way the bar is never covered, because ``popup`` only
    ever shifts content along the cross axis.
    """
    anchored = self._core.modifier(
        popup(
            self._container,
            is_open=self._is_open,
            anchor_passthrough=True,
            target_anchor="bottom-left",
            content_anchor="top-left",
            offset=(0.0, self._style.gap),
            flip=False,
        )
    )
    return _SearchPane(anchored, core=self._core, width=self._width)

SearchBar

SearchBar(value: Union[str, ReadOnlyObservableProtocol[str]] = '', *, placeholder: str | None = None, leading_icon: IconLike = 'search', on_tap_leading_icon: Optional[Callable[[], None]] = None, trailing_icon: IconLike = None, on_tap_trailing_icon: Optional[Callable[[], None]] = None, on_change: Optional[Callable[[str], None]] = None, on_submit: Optional[Callable[[str], None]] = None, on_focus_change: Optional[FocusChangeCallback] = None, input_filter: Optional[InputFilterLike] = None, width: SizingLike = None, style: Optional[SearchBarStyle] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget

Material Design 3 search bar (contained variant).

The bar is drawn inset inside the box this widget is given: 24dp on each side, animating to 12dp while focused. width therefore names the box, not the bar — which keeps the widget's footprint stable when it is focused, instead of reflowing its siblings.

There is no full-screen search widget. To build one, lay out your own screen and place a SearchBar in it; the bar brings its own margin animation with it.

Initialize SearchBar.

Parameters:

Name Type Description Default
value Union[str, ReadOnlyObservableProtocol[str]]

Initial query text, or the observable holding it. Edits are written back to a writable observable, exactly as for TextField.

''
placeholder str | None

Supporting text shown inside the bar while it is empty.

None
leading_icon IconLike

Icon source (Symbol/str, or an Observable of them).

'search'
on_tap_leading_icon Optional[Callable[[], None]]

Makes the leading icon a tappable icon button.

None
trailing_icon IconLike

Icon source for the trailing slot.

None
on_tap_trailing_icon Optional[Callable[[], None]]

Makes the trailing icon a tappable icon button. The slot is generic — clearing the query is one use of it, not a built-in behaviour.

None
on_change Optional[Callable[[str], None]]

Callback invoked with the query as it changes, for a side effect of the change. The observable bound to value carries the same signal without it.

None
on_submit Optional[Callable[[str], None]]

Callback invoked with the query when Enter is pressed. Fires on every press, including a repeat on an unchanged query, and never on focus loss.

None
on_focus_change Optional[FocusChangeCallback]

Callback invoked as focus arrives and leaves, with (focused, source) -- the same signature as focusable(). It can arrive more than once with focused=True for a single acquisition, because the source is re-announced when the user switches from keyboard to pointer; focused=False arrives once.

None
input_filter Optional[InputFilterLike]

Rule applied to text as the user types it.

None
width SizingLike

Sizing for the box, not for the bar drawn inside it. The bar is the box minus the margins, so width=440 draws a 392dp bar that grows to 416dp when the user clicks into it, while the widget's own footprint stays 440dp and nothing beside it moves. The bar is capped at 720dp and centred when the box is wider; in a box too narrow for the 360dp minimum it shrinks to fit rather than overflowing.

None
padding PaddingLike

Insets from the allocated rect to the box.

0
style Optional[SearchBarStyle]

Custom style configuration.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/search.py
def __init__(
    self,
    value: Union[str, ReadOnlyObservableProtocol[str]] = "",
    *,
    placeholder: str | None = None,
    leading_icon: IconLike = "search",
    on_tap_leading_icon: Optional[Callable[[], None]] = None,
    trailing_icon: IconLike = None,
    on_tap_trailing_icon: Optional[Callable[[], None]] = None,
    on_change: Optional[Callable[[str], None]] = None,
    on_submit: Optional[Callable[[str], None]] = None,
    on_focus_change: Optional[FocusChangeCallback] = None,
    input_filter: Optional[InputFilterLike] = None,
    width: SizingLike = None,
    style: Optional[SearchBarStyle] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize SearchBar.

    Args:
        value: Initial query text, or the observable holding it. Edits are
            written back to a writable observable, exactly as for ``TextField``.
        placeholder: Supporting text shown inside the bar while it is empty.
        leading_icon: Icon source (Symbol/str, or an Observable of them).
        on_tap_leading_icon: Makes the leading icon a tappable icon button.
        trailing_icon: Icon source for the trailing slot.
        on_tap_trailing_icon: Makes the trailing icon a tappable icon button.
            The slot is generic — clearing the query is one use of it, not a
            built-in behaviour.
        on_change: Callback invoked with the query as it changes, for a side
            effect of the change. The observable bound to *value* carries the
            same signal without it.
        on_submit: Callback invoked with the query when Enter is pressed.
            Fires on every press, including a repeat on an unchanged query,
            and never on focus loss.
        on_focus_change: Callback invoked as focus arrives and leaves, with
            ``(focused, source)`` -- the same signature as ``focusable()``. It
            can arrive more than once with ``focused=True`` for a single
            acquisition, because the *source* is re-announced when the user
            switches from keyboard to pointer; ``focused=False`` arrives once.
        input_filter: Rule applied to text as the user types it.
        width: Sizing for the **box**, not for the bar drawn inside it. The bar
            is the box minus the margins, so ``width=440`` draws a 392dp bar
            that grows to 416dp when the user clicks into it, while the
            widget's own footprint stays 440dp and nothing beside it moves. The
            bar is capped at 720dp and centred when the box is wider; in a box
            too narrow for the 360dp minimum it shrinks to fit rather than
            overflowing.
        padding: Insets from the allocated rect to the box.
        style: Custom style configuration.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)
    self._width = width
    # Built once and reused across rebuilds so focus and cursor position
    # survive recomposition.
    self._core = _SearchBarCore(
        value=value,
        placeholder=placeholder,
        leading_icon=leading_icon,
        on_tap_leading_icon=on_tap_leading_icon,
        trailing_icon=trailing_icon,
        on_tap_trailing_icon=on_tap_trailing_icon,
        on_change=on_change,
        on_submit=on_submit,
        on_focus_change=on_focus_change,
        input_filter=input_filter,
        style=style,
    )

value property

value: str

Return the current query text.

focus

focus() -> None

Programmatically focus the bar.

Source code in src/nuiitivet/material/search.py
def focus(self) -> None:
    """Programmatically focus the bar."""
    self._core.focus()

build

build() -> Widget

Return the pane, with the bar inset inside it.

Source code in src/nuiitivet/material/search.py
def build(self) -> Widget:
    """Return the pane, with the bar inset inside it."""
    return _SearchPane(self._core, core=self._core, width=self._width)

DockedSearchBarStyle dataclass

DockedSearchBarStyle(bar: SearchBarStyle = SearchBarStyle(), container_color: ColorSpec = SURFACE_CONTAINER_HIGH, corner_radius: float = 12.0, gap: float = 2.0, min_height: float = 240.0, max_height_ratio: float = 2.0 / 3.0)

Style configuration for :class:~nuiitivet.material.search.DockedSearchBar.

MD3 reference: md.comp.search-view.contained.docked.*.

copy_with

copy_with(**changes) -> 'DockedSearchBarStyle'

Create a new style instance with specified fields changed.

Source code in src/nuiitivet/material/styles/search_bar_style.py
def copy_with(self, **changes) -> "DockedSearchBarStyle":
    """Create a new style instance with specified fields changed."""
    return replace(self, **changes)

SearchBarStyle dataclass

SearchBarStyle(container_color: ColorSpec = SURFACE_CONTAINER_HIGH, container_height: float = 56.0, density: Optional[int] = None, margin: float = 24.0, focused_margin: float = 12.0, min_width: float = 360.0, max_width: float = 720.0, input_text_color: ColorSpec = ON_SURFACE, supporting_text_color: ColorSpec = ON_SURFACE_VARIANT, font_size: int = 16, leading_icon_color: ColorSpec = ON_SURFACE, trailing_icon_color: ColorSpec = ON_SURFACE_VARIANT, icon_size: int = 24, icon_target: float = 48.0, cursor_color: ColorSpec = PRIMARY, selection_color: ColorSpec = PRIMARY_CONTAINER, state_layer_color: ColorSpec = ON_SURFACE, focus_indicator_color: ColorSpec = SECONDARY, leading_space: float = 4.0, trailing_space: float = 4.0, icon_label_gap: float = 4.0)

Style configuration for :class:~nuiitivet.material.search.SearchBar.

MD3 reference: md.comp.search-bar.*, contained rows only.

copy_with

copy_with(**changes) -> 'SearchBarStyle'

Create a new style instance with specified fields changed.

Source code in src/nuiitivet/material/styles/search_bar_style.py
def copy_with(self, **changes) -> "SearchBarStyle":
    """Create a new style instance with specified fields changed."""
    return replace(self, **changes)

from_theme classmethod

from_theme(theme: 'Theme') -> 'SearchBarStyle'

Resolve the default :class:SearchBarStyle for the given theme.

The defaults are already theme roles rather than literal colours, so this returns the preset unless a Material theme extension overrides it.

Source code in src/nuiitivet/material/styles/search_bar_style.py
@classmethod
def from_theme(cls, theme: "Theme") -> "SearchBarStyle":
    """Resolve the default :class:`SearchBarStyle` for the given theme.

    The defaults are already theme roles rather than literal colours, so
    this returns the preset unless a Material theme extension overrides it.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    theme_data = theme.extension(MaterialThemeData)
    if theme_data:
        return theme_data.search_bar_style
    return cls()

TextStyle dataclass

TextStyle(color: ColorSpec = ON_SURFACE, font_family: str | None = None)

Immutable visual style for Material Text widgets (M3-compliant).

Use copy_with() to create style variants.

Material Design 3 Text specifications: - Default color: ON_SURFACE

Typography comes from the widget's type_scale and alignment from the widget itself, so neither lives here.

copy_with

copy_with(**changes) -> TextStyle

Create a new style instance with specified fields changed.

Example

error_style = TextStyle().copy_with(color=ColorRole.ERROR)

Source code in src/nuiitivet/material/styles/text_style.py
def copy_with(self, **changes) -> "TextStyle":
    """Create a new style instance with specified fields changed.

    Example:
        error_style = TextStyle().copy_with(color=ColorRole.ERROR)
    """
    return replace(self, **changes)

IconStyle dataclass

IconStyle(default_size: int = 24, family: str = 'outlined', color: ColorSpec = ON_SURFACE, font_family_priority: Tuple[str, ...] = ('Material Symbols Outlined', 'Material Symbols Rounded', 'Material Symbols Sharp', 'Material Icons'), custom_font_family: Optional[str] = None, style_to_family: dict[str, str] = None)

Immutable style for Icon widgets (M3-compliant).

Attributes:

Name Type Description
default_size int

Icon size in dp when the widget gives none.

family str

Material Symbols family: "outlined", "rounded", "sharp", or "icons" for the legacy Material Icons font.

color ColorSpec

Glyph color.

font_family_priority Tuple[str, ...]

Font families tried in order for the glyph.

custom_font_family Optional[str]

Icon font registered with :meth:Fonts.register; used in place of the built-in lookup.

style_to_family dict[str, str]

Font family for each family value; filled in on construction.

copy_with

copy_with(**changes) -> IconStyle

Create a new style instance with specified fields changed.

Source code in src/nuiitivet/material/styles/icon_style.py
def copy_with(self, **changes) -> "IconStyle":
    """Create a new style instance with specified fields changed."""
    return replace(self, **changes)

resolve_color

resolve_color(color: ColorSpec, theme: Theme | None = None) -> tuple[int, int, int, int]

Resolve ColorRole to an (r,g,b,a) tuple using the theme resolver.

Source code in src/nuiitivet/material/styles/icon_style.py
def resolve_color(self, color: ColorSpec, theme: "Theme | None" = None) -> tuple[int, int, int, int]:
    """Resolve ColorRole to an (r,g,b,a) tuple using the theme resolver."""
    from nuiitivet.theme.resolver import resolve_color_to_rgba

    return resolve_color_to_rgba(color, theme=theme)

get_font_family

get_font_family(style: str) -> str

Get font family name for given style.

Source code in src/nuiitivet/material/styles/icon_style.py
def get_font_family(self, style: str) -> str:
    """Get font family name for given style."""
    return self.style_to_family.get(style, self.font_family_priority[0])

DividerStyle dataclass

DividerStyle(color: ColorSpec = OUTLINE_VARIANT, thickness: int = 1, inset_left: int = 0, inset_right: int = 0)

Immutable style for the Divider widget.

Attributes:

Name Type Description
color ColorSpec

Line color. Defaults to the M3 Outline Variant color role.

thickness int

Line thickness in pixels. Defaults to 1 (1dp per M3 spec).

inset_left int

Left-side inset in pixels. Defaults to 0. For vertical orientation this is applied to the top side.

inset_right int

Right-side inset in pixels. Defaults to 0. For vertical orientation this is applied to the bottom side.

copy_with

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

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

Parameters:

Name Type Description Default
**changes Any

Fields to override.

{}

Returns:

Type Description
'DividerStyle'

A new :class:DividerStyle with the specified changes applied.

Source code in src/nuiitivet/material/styles/divider_style.py
def copy_with(self, **changes: Any) -> "DividerStyle":
    """Return a copy of this style with the given fields overridden.

    Args:
        **changes: Fields to override.

    Returns:
        A new :class:`DividerStyle` with the specified changes applied.
    """
    return replace(self, **changes)

ToolbarStyle dataclass

ToolbarStyle(color_scheme: ToolbarColorScheme = 'standard', background: ColorSpec = SURFACE_CONTAINER_HIGHEST, foreground: ColorSpec = ON_SURFACE_VARIANT, selected_background: ColorSpec = SECONDARY_CONTAINER, selected_foreground: ColorSpec = ON_SECONDARY_CONTAINER, container_height: int = 64, content_insets: tuple[int, int, int, int] = (16, 0, 16, 0), item_gap: int = 8, fab_gap: int = 8, corner_radius: int = 0, border_color: Optional[ColorSpec] = None, border_width: float = 0.0, elevation: float = 0.0)

Immutable style for Material toolbar widgets.

Presets: :meth:standard, :meth:vibrant. The *_style methods derive the styles a toolbar pushes onto the buttons it hosts.

Attributes:

Name Type Description
color_scheme ToolbarColorScheme

Toolbar color scheme variant.

background ColorSpec

Toolbar container background color.

foreground ColorSpec

Icon and label color of the toolbar's buttons.

selected_background ColorSpec

Container color of a selected toggle button.

selected_foreground ColorSpec

Icon and label color of a selected toggle button.

container_height int

Visual container height in pixels.

content_insets tuple[int, int, int, int]

Internal content insets.

item_gap int

Gap between action buttons.

fab_gap int

Gap between a floating toolbar and the FAB beside it.

corner_radius int

Container corner radius in pixels.

border_color Optional[ColorSpec]

Optional border color.

border_width float

Border width in pixels.

elevation float

Elevation level for shadow rendering.

copy_with

copy_with(**changes) -> 'ToolbarStyle'

Return a copy of this style with changed fields.

Source code in src/nuiitivet/material/styles/toolbar_style.py
def copy_with(self, **changes) -> "ToolbarStyle":
    """Return a copy of this style with changed fields."""
    return replace(self, **changes)

fab_style

fab_style(size: FabSize = 's') -> FabStyle

Return the style of a FAB placed beside a floating toolbar of this scheme.

The colour follows :attr:color_scheme: secondary-container for standard, tertiary-container for vibrant. The elevation is level 1 at size "s" and level 2 otherwise, one level higher while hovered.

Parameters:

Name Type Description Default
size FabSize

FAB size. MD3 pairs a floating toolbar with "s" (56dp) and "m" (80dp); "l" is styled like "m".

's'

Returns:

Type Description
FabStyle

The derived FAB style.

Source code in src/nuiitivet/material/styles/toolbar_style.py
def fab_style(self, size: FabSize = "s") -> FabStyle:
    """Return the style of a FAB placed beside a floating toolbar of this scheme.

    The colour follows :attr:`color_scheme`: secondary-container for
    ``standard``, tertiary-container for ``vibrant``. The elevation is
    level 1 at size ``"s"`` and level 2 otherwise, one level higher while
    hovered.

    Args:
        size: FAB size. MD3 pairs a floating toolbar with ``"s"`` (56dp)
            and ``"m"`` (80dp); ``"l"`` is styled like ``"m"``.

    Returns:
        The derived FAB style.
    """
    base = FabStyle.tertiary(size) if self.color_scheme == "vibrant" else FabStyle.secondary(size)
    elevation = 1 if size == "s" else 2
    return base.copy_with(
        elevation=elevation,
        focused_elevation=elevation,
        hovered_elevation=elevation + 1,
        pressed_elevation=elevation,
    )

icon_button_style

icon_button_style() -> ButtonStyle

Return the style of an icon button hosted by a toolbar of this scheme.

The container shows the toolbar through; the icon and the state layer take :attr:foreground.

Returns:

Type Description
ButtonStyle

The derived icon button style at size "s".

Source code in src/nuiitivet/material/styles/toolbar_style.py
def icon_button_style(self) -> ButtonStyle:
    """Return the style of an icon button hosted by a toolbar of this scheme.

    The container shows the toolbar through; the icon and the state layer
    take :attr:`foreground`.

    Returns:
        The derived icon button style at size ``"s"``.
    """
    return IconButtonStyle.standard().copy_with(
        background=None,
        foreground=self.foreground,
        overlay_color=self.foreground,
    )

icon_toggle_button_style

icon_toggle_button_style() -> IconToggleButtonStyle

Return the style pair of an icon toggle button hosted by a toolbar of this scheme.

Unselected is :meth:icon_button_style; selected takes :attr:selected_background and :attr:selected_foreground.

Returns:

Type Description
IconToggleButtonStyle

The derived icon toggle style at size "s".

Source code in src/nuiitivet/material/styles/toolbar_style.py
def icon_toggle_button_style(self) -> IconToggleButtonStyle:
    """Return the style pair of an icon toggle button hosted by a toolbar of this scheme.

    Unselected is :meth:`icon_button_style`; selected takes
    :attr:`selected_background` and :attr:`selected_foreground`.

    Returns:
        The derived icon toggle style at size ``"s"``.
    """
    selected = IconToggleButtonStyle.standard().selected.copy_with(
        background=self.selected_background,
        foreground=self.selected_foreground,
        overlay_color=self.selected_foreground,
    )
    return IconToggleButtonStyle(selected=selected, unselected=self.icon_button_style())

button_style

button_style() -> ButtonStyle

Return the style of a text button hosted by a toolbar of this scheme.

The container shows the toolbar through; the label, icon and state layer take :attr:foreground.

Returns:

Type Description
ButtonStyle

The derived button style at size "s".

Source code in src/nuiitivet/material/styles/toolbar_style.py
def button_style(self) -> ButtonStyle:
    """Return the style of a text button hosted by a toolbar of this scheme.

    The container shows the toolbar through; the label, icon and state
    layer take :attr:`foreground`.

    Returns:
        The derived button style at size ``"s"``.
    """
    return ButtonStyle.text().copy_with(
        background=None,
        foreground=self.foreground,
        overlay_color=self.foreground,
    )

toggle_button_style

toggle_button_style() -> ToggleButtonStyle

Return the style of a text toggle button hosted by a toolbar of this scheme.

Unselected matches :meth:button_style; selected takes :attr:selected_background and :attr:selected_foreground.

Returns:

Type Description
ToggleButtonStyle

The derived toggle button style at size "s".

Source code in src/nuiitivet/material/styles/toolbar_style.py
def toggle_button_style(self) -> ToggleButtonStyle:
    """Return the style of a text toggle button hosted by a toolbar of this scheme.

    Unselected matches :meth:`button_style`; selected takes
    :attr:`selected_background` and :attr:`selected_foreground`.

    Returns:
        The derived toggle button style at size ``"s"``.
    """
    return ToggleButtonStyle.filled().copy_with(
        unselected_background=None,
        unselected_foreground=self.foreground,
        unselected_overlay_color=self.foreground,
        selected_background=self.selected_background,
        selected_foreground=self.selected_foreground,
        selected_overlay_color=self.selected_foreground,
    )

standard classmethod

standard() -> 'ToolbarStyle'

Return the standard toolbar style.

Source code in src/nuiitivet/material/styles/toolbar_style.py
@classmethod
def standard(cls) -> "ToolbarStyle":
    """Return the standard toolbar style."""
    return cls(
        color_scheme="standard",
        background=ColorRole.SURFACE_CONTAINER_HIGHEST,
        foreground=ColorRole.ON_SURFACE_VARIANT,
        selected_background=ColorRole.SECONDARY_CONTAINER,
        selected_foreground=ColorRole.ON_SECONDARY_CONTAINER,
        container_height=64,
        content_insets=(16, 0, 16, 0),
        item_gap=8,
        fab_gap=8,
        corner_radius=0,
        border_color=None,
        border_width=0.0,
        elevation=0.0,
    )

vibrant classmethod

vibrant() -> 'ToolbarStyle'

Return the vibrant toolbar style.

Source code in src/nuiitivet/material/styles/toolbar_style.py
@classmethod
def vibrant(cls) -> "ToolbarStyle":
    """Return the vibrant toolbar style."""
    return cls(
        color_scheme="vibrant",
        background=ColorRole.PRIMARY_CONTAINER,
        foreground=ColorRole.ON_PRIMARY_CONTAINER,
        selected_background=ColorRole.SURFACE_CONTAINER,
        selected_foreground=ColorRole.ON_SURFACE,
        container_height=64,
        content_insets=(16, 0, 16, 0),
        item_gap=8,
        fab_gap=8,
        corner_radius=0,
        border_color=None,
        border_width=0.0,
        elevation=0.0,
    )

default classmethod

default(variant: ToolbarColorScheme = 'standard') -> 'ToolbarStyle'

Return the framework default for variant, ignoring any theme.

This is what a toolbar renders with before it is mounted, and what :meth:from_theme falls back to when no Material theme is installed.

Parameters:

Name Type Description Default
variant ToolbarColorScheme

One of standard or vibrant. Unknown values fall back to standard.

'standard'

Returns:

Type Description
'ToolbarStyle'

The variant preset style.

Source code in src/nuiitivet/material/styles/toolbar_style.py
@classmethod
def default(cls, variant: ToolbarColorScheme = "standard") -> "ToolbarStyle":
    """Return the framework default for ``variant``, ignoring any theme.

    This is what a toolbar renders with before it is mounted, and what
    :meth:`from_theme` falls back to when no Material theme is installed.

    Args:
        variant: One of ``standard`` or ``vibrant``. Unknown values fall
            back to ``standard``.

    Returns:
        The variant preset style.
    """
    if str(variant or "standard").lower() == "vibrant":
        return cls.vibrant()
    return cls.standard()

from_theme classmethod

from_theme(theme: 'Theme', variant: ToolbarColorScheme = 'standard') -> 'ToolbarStyle'

Resolve the toolbar style from theme.

Parameters:

Name Type Description Default
theme 'Theme'

Theme instance.

required
variant ToolbarColorScheme

One of standard or vibrant. Only standard is carried by :class:MaterialThemeData; vibrant is an explicit opt-in and always returns its preset.

'standard'

Returns:

Type Description
'ToolbarStyle'

Resolved toolbar style.

Source code in src/nuiitivet/material/styles/toolbar_style.py
@classmethod
def from_theme(cls, theme: "Theme", variant: ToolbarColorScheme = "standard") -> "ToolbarStyle":
    """Resolve the toolbar style from ``theme``.

    Args:
        theme: Theme instance.
        variant: One of ``standard`` or ``vibrant``. Only ``standard`` is
            carried by :class:`MaterialThemeData`; ``vibrant`` is an
            explicit opt-in and always returns its preset.

    Returns:
        Resolved toolbar style.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    variant_name = str(variant or "standard").lower()
    if variant_name == "standard":
        theme_data = theme.extension(MaterialThemeData)
        if theme_data is not None:
            return theme_data.toolbar_style
    return cls.default(variant)

CircularProgressIndicatorStyle dataclass

CircularProgressIndicatorStyle(active_indicator_color: ColorSpec = PRIMARY, track_color: ColorSpec = SECONDARY_CONTAINER, stop_indicator_color: ColorSpec = PRIMARY, disabled_active_alpha: float = 0.38, disabled_track_alpha: float = 0.12, size: float = 40.0, with_wave_size: float = 48.0, track_thickness: float = 4.0, track_active_space: float = 4.0, wave_amplitude: float = 1.6, wave_wavelength: float = 15.0, motion: Motion = EXPRESSIVE_DEFAULT_EFFECTS)

Bases: ProgressIndicatorStyle

Circular progress indicator geometry and motion tokens.

Presets: :meth:default (wavy), :meth:flat.

default classmethod

default() -> 'CircularProgressIndicatorStyle'

Create the default circular progress style.

Source code in src/nuiitivet/material/styles/progress_indicator_style.py
@classmethod
def default(cls) -> "CircularProgressIndicatorStyle":
    """Create the default circular progress style."""
    return cls()

flat classmethod

flat() -> 'CircularProgressIndicatorStyle'

Create a flat circular progress style preset.

Source code in src/nuiitivet/material/styles/progress_indicator_style.py
@classmethod
def flat(cls) -> "CircularProgressIndicatorStyle":
    """Create a flat circular progress style preset."""
    return cls(
        with_wave_size=40.0,
        wave_amplitude=0.0,
    )

from_theme classmethod

from_theme(theme: 'Theme', variant: str = 'default') -> 'CircularProgressIndicatorStyle'

Get circular progress style from theme.

Parameters:

Name Type Description Default
theme 'Theme'

Theme to load style from.

required
variant str

Variant name ("default" or "flat").

'default'

Returns:

Type Description
'CircularProgressIndicatorStyle'

CircularProgressIndicatorStyle resolved from theme or fallback preset.

Source code in src/nuiitivet/material/styles/progress_indicator_style.py
@classmethod
def from_theme(cls, theme: "Theme", variant: str = "default") -> "CircularProgressIndicatorStyle":
    """Get circular progress style from theme.

    Args:
        theme: Theme to load style from.
        variant: Variant name ("default" or "flat").

    Returns:
        CircularProgressIndicatorStyle resolved from theme or fallback preset.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    theme_data = theme.extension(MaterialThemeData)
    v = (variant or "").lower()

    if theme_data is not None:
        base = theme_data.circular_progress_indicator_style
        if v == "flat":
            return base.copy_with(with_wave_size=base.size, wave_amplitude=0.0)
        return base

    if v == "flat":
        return cls.flat()
    return cls.default()

LinearProgressIndicatorStyle dataclass

LinearProgressIndicatorStyle(active_indicator_color: ColorSpec = PRIMARY, track_color: ColorSpec = SECONDARY_CONTAINER, stop_indicator_color: ColorSpec = PRIMARY, disabled_active_alpha: float = 0.38, disabled_track_alpha: float = 0.12, track_thickness: float = 4.0, with_wave_height: float = 10.0, stop_indicator_size: float = 4.0, track_active_space: float = 4.0, stop_indicator_trailing_space: float = 0.0, wave_amplitude: float = 3.0, wave_wavelength: float = 40.0, indeterminate_wave_wavelength: float = 20.0, motion: Motion = EXPRESSIVE_DEFAULT_EFFECTS)

Bases: ProgressIndicatorStyle

Linear progress indicator geometry and motion tokens.

Presets: :meth:default (wavy), :meth:flat.

default classmethod

default() -> 'LinearProgressIndicatorStyle'

Create the default linear progress style.

Source code in src/nuiitivet/material/styles/progress_indicator_style.py
@classmethod
def default(cls) -> "LinearProgressIndicatorStyle":
    """Create the default linear progress style."""
    return cls()

flat classmethod

flat() -> 'LinearProgressIndicatorStyle'

Create a flat linear progress style preset.

Source code in src/nuiitivet/material/styles/progress_indicator_style.py
@classmethod
def flat(cls) -> "LinearProgressIndicatorStyle":
    """Create a flat linear progress style preset."""
    return cls(
        with_wave_height=4.0,
        wave_amplitude=0.0,
    )

from_theme classmethod

from_theme(theme: 'Theme', variant: str = 'default') -> 'LinearProgressIndicatorStyle'

Get linear progress style from theme.

Parameters:

Name Type Description Default
theme 'Theme'

Theme to load style from.

required
variant str

Variant name ("default" or "flat").

'default'

Returns:

Type Description
'LinearProgressIndicatorStyle'

LinearProgressIndicatorStyle resolved from theme or fallback preset.

Source code in src/nuiitivet/material/styles/progress_indicator_style.py
@classmethod
def from_theme(cls, theme: "Theme", variant: str = "default") -> "LinearProgressIndicatorStyle":
    """Get linear progress style from theme.

    Args:
        theme: Theme to load style from.
        variant: Variant name ("default" or "flat").

    Returns:
        LinearProgressIndicatorStyle resolved from theme or fallback preset.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    theme_data = theme.extension(MaterialThemeData)
    v = (variant or "").lower()

    if theme_data is not None:
        base = theme_data.linear_progress_indicator_style
        if v == "flat":
            return base.copy_with(with_wave_height=base.track_thickness, wave_amplitude=0.0)
        return base

    if v == "flat":
        return cls.flat()
    return cls.default()

Snackbar

Snackbar(message: str, *, padding: Optional[Union[int, Tuple[int, int, int, int]]] = None, style: Optional[SnackbarStyle] = None, key: Optional[str] = None)

Bases: ComposableWidget

Material Design Snackbar.

Displays a brief message at the bottom of the screen.

Initialize Snackbar.

Parameters:

Name Type Description Default
message str

Text to display.

required
padding Optional[Union[int, Tuple[int, int, int, int]]]

Insets from the allocated rect to the container.

None
style Optional[SnackbarStyle]

Visual style; defaults to :class:SnackbarStyle.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/snackbar.py
def __init__(
    self,
    message: str,
    *,
    padding: Optional[Union[int, Tuple[int, int, int, int]]] = None,
    style: Optional[SnackbarStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize Snackbar.

    Args:
        message: Text to display.
        padding: Insets from the allocated rect to the container.
        style: Visual style; defaults to :class:`SnackbarStyle`.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)
    self.message = str(message)
    self.style = style if style is not None else SnackbarStyle()

BasicDialogIntent dataclass

BasicDialogIntent(title: str | None = None, message: str | None = None, icon: Any | None = None)

Intent for showing a Material Basic Dialog.

Attributes:

Name Type Description
title str | None

The title of the dialog.

message str | None

The message body of the dialog.

icon Any | None

The icon to display. Can be a Widget or other supported type.

ColorRole

Bases: Enum

Material 3 Color Roles — canonical 26 roles used by M3.

FormFactor dataclass

FormFactor(touch_targets: bool = True, text_field: int = 0, search_bar: int = 0, menu: int = 0)

The component sizes of one kind of device.

Colors, shapes, motion and font sizes do not depend on it. A density step takes 4dp off a component's height. A step below the component's lowest stops there. A style's own density wins over the step set here.

Button, IconButton, ToggleButton, ButtonGroup, SplitButton and Fab take their height from their size ("xs" to "xl") only. Dialog, Snackbar, the date picker surface, Tooltip, Sheet, NavigationRail and Toolbar do not change with the form factor.

Built-in form factors: :meth:desktop, :meth:mobile.

Attributes:

Name Type Description
touch_targets bool

Whether a control smaller than 48dp reserves a 48dp touch target. Applies to Button, IconButton, ToggleButton, Chip, Checkbox, RadioButton, Switch and the SearchBar icons.

text_field int

Density step of TextField. The lowest is -5. A filled field below 52dp shows its label only while empty.

search_bar int

Density step of SearchBar. The lowest is -4.

menu int

Density step of a menu item. The lowest is -3.

copy_with

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

Create a new form factor with specified fields changed.

Source code in src/nuiitivet/material/theme/form_factor.py
def copy_with(self, **changes: Any) -> "FormFactor":
    """Create a new form factor with specified fields changed."""
    return replace(self, **changes)

desktop classmethod

desktop() -> 'FormFactor'

Sizes for a pointer: the controls of one row are 40dp tall.

Source code in src/nuiitivet/material/theme/form_factor.py
@classmethod
def desktop(cls) -> "FormFactor":
    """Sizes for a pointer: the controls of one row are 40dp tall."""
    return _DESKTOP

mobile classmethod

mobile() -> 'FormFactor'

The MD3 baseline sizes, built for touch.

Source code in src/nuiitivet/material/theme/form_factor.py
@classmethod
def mobile(cls) -> "FormFactor":
    """The MD3 baseline sizes, built for touch."""
    return _MOBILE

of classmethod

of(value: 'FormFactorLike') -> 'FormFactor'

Return value as a :class:FormFactor.

Parameters:

Name Type Description Default
value 'FormFactorLike'

A form factor or the name of a built-in one.

required

Raises:

Type Description
ValueError

If value names no built-in form factor.

Source code in src/nuiitivet/material/theme/form_factor.py
@classmethod
def of(cls, value: "FormFactorLike") -> "FormFactor":
    """Return ``value`` as a :class:`FormFactor`.

    Args:
        value: A form factor or the name of a built-in one.

    Raises:
        ValueError: If ``value`` names no built-in form factor.
    """
    if isinstance(value, FormFactor):
        return value
    if value == "desktop":
        return _DESKTOP
    if value == "mobile":
        return _MOBILE
    raise ValueError(f"Unknown form factor {value!r}; expected 'desktop', 'mobile' or a FormFactor")

SchemeVariant

Bases: Enum

Algorithm used to derive a tonal palette from a seed color.

Each member's value is the corresponding materialyoucolor.Variant attribute name.

Text

Text(label: Union[str, ReadOnlyObservableProtocol[Any]], *, width: SizingLike = None, height: SizingLike = None, padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0, style: Optional['TextStyle'] = None, type_scale: Optional[TypeScaleToken] = None, alignment: Literal['start', 'center', 'end'] = 'start', max_lines: Optional[int] = None, overflow: Literal['visible', 'clip', 'ellipsis'] = 'visible', truncation: Literal['tail', 'head', 'middle'] = 'tail', soft_wrap: bool = True, key: Optional[str] = None)

Bases: TextBase

Material text widget.

Defaults to the current Material theme TextStyle.

Initialize Material Text widget.

Parameters:

Name Type Description Default
label Union[str, ReadOnlyObservableProtocol[Any]]

The text content to display. Can be a string or an Observable.

required
width SizingLike

Width specification.

None
height SizingLike

Height specification.

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

Insets from the allocated rect to the text.

0
style Optional['TextStyle']

Custom Material TextStyle (color, font_family).

None
type_scale Optional[TypeScaleToken]

MD3 type-scale token supplying typography. Defaults to Body Medium.

None
alignment Literal['start', 'center', 'end']

Horizontal text alignment ("start", "center", "end").

'start'
max_lines Optional[int]

Maximum number of lines (None = unbounded).

None
overflow Literal['visible', 'clip', 'ellipsis']

Overflow handling: "visible", "clip" or "ellipsis".

'visible'
truncation Literal['tail', 'head', 'middle']

Ellipsis position: "tail", "head" or "middle".

'tail'
soft_wrap bool

Whether to wrap at soft line breaks when width is bounded.

True
key Optional[str]

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

None
Source code in src/nuiitivet/material/text.py
def __init__(
    self,
    label: Union[str, ReadOnlyObservableProtocol[Any]],
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    padding: Union[int, Tuple[int, int], Tuple[int, int, int, int]] = 0,
    style: Optional["TextStyle"] = None,
    type_scale: Optional[TypeScaleToken] = None,
    alignment: Literal["start", "center", "end"] = "start",
    max_lines: Optional[int] = None,
    overflow: Literal["visible", "clip", "ellipsis"] = "visible",
    truncation: Literal["tail", "head", "middle"] = "tail",
    soft_wrap: bool = True,
    key: Optional[str] = None,
):
    """Initialize Material Text widget.

    Args:
        label: The text content to display. Can be a string or an Observable.
        width: Width specification.
        height: Height specification.
        padding: Insets from the allocated rect to the text.
        style: Custom Material TextStyle (color, font_family).
        type_scale: MD3 type-scale token supplying typography. Defaults to
            Body Medium.
        alignment: Horizontal text alignment (``"start"``, ``"center"``,
            ``"end"``).
        max_lines: Maximum number of lines (``None`` = unbounded).
        overflow: Overflow handling: ``"visible"``, ``"clip"`` or ``"ellipsis"``.
        truncation: Ellipsis position: ``"tail"``, ``"head"`` or ``"middle"``.
        soft_wrap: Whether to wrap at soft line breaks when width is bounded.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    from nuiitivet.material.styles.text_style import TextStyle

    if style is not None and not isinstance(style, TextStyle):
        raise TypeError("style must be a material TextStyle")

    super().__init__(
        label=label,
        style=style,
        width=width,
        height=height,
        padding=padding,
        type_scale=type_scale,
        alignment=alignment,
        max_lines=max_lines,
        overflow=overflow,
        truncation=truncation,
        soft_wrap=soft_wrap,
        key=key,
    )

style property

style: TextStyleProtocol

Return the current text style, resolving from theme if necessary.

Navigator

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

Bases: Navigator

Navigator whose screens default to the Material page transition.

It also paints transitions with the Material layer composer, so :meth:Navigator.routes and :meth:Navigator.intents animate too.

Initialize a MaterialNavigator with a single initial screen.

Parameters:

Name Type Description Default
screen Widget | None

The initial screen. None starts with an empty stack.

None
transition TransitionSpec | None

The transition screen moves with. Defaults to MaterialTransitions.page().

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/material/navigator.py
def __init__(
    self,
    screen: Widget | None = None,
    *,
    transition: TransitionSpec | None = None,
    layer_composer: NavigationLayerComposer | None = None,
    key: str | None = None,
) -> None:
    """Initialize a MaterialNavigator with a single initial screen.

    Args:
        screen: The initial screen. ``None`` starts with an empty stack.
        transition: The transition ``screen`` moves with. Defaults to
            ``MaterialTransitions.page()``.
        layer_composer: Optional custom layer composer.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(
        screen,
        transition=transition,
        layer_composer=layer_composer or MaterialNavigationLayerComposer(),
        key=key,
    )

Overlay

Overlay(*, intent_resolver: IntentResolver | None = None, intents: Mapping[type[Any], Callable[[Any], Widget]] | None = None, key: str | None = None)

Bases: Overlay

Overlay subclass that provides Material-specific helpers.

Source code in src/nuiitivet/material/overlay.py
def __init__(
    self,
    *,
    intent_resolver: IntentResolver | None = None,
    intents: Mapping[type[Any], Callable[[Any], Widget]] | None = None,
    key: str | None = None,
) -> None:
    super().__init__(layer_composer=MaterialOverlayLayerComposer(), key=key)

    if intent_resolver is not None and intents is not None:
        raise ValueError("Specify only one of intent_resolver or intents")

    if intent_resolver is None:
        defaults: dict[type[Any], Callable[[Any], Widget]] = {
            BasicDialogIntent: lambda i: BasicDialog(
                title=i.title,
                message=i.message,
                icon=i.icon,
                actions=[
                    Button(
                        "OK",
                        # This resolver belongs to the overlay hosting the
                        # dialog, so close it directly rather than looking
                        # one up.
                        on_click=lambda: self.close(None),
                        width=80,
                        style=ButtonStyle.text(),
                    )
                ],
            ),
            LoadingIntent: lambda _: LoadingIndicator(),
        }
        if intents:
            defaults.update(intents)
        intent_resolver = MappingIntentResolver(defaults)

    self._intent_resolver = intent_resolver

dialog

dialog(dialog: Widget | Any, *, dismiss_on_outside_tap: bool = True) -> OverlayHandle[Any]

Display a modal Material dialog.

Parameters:

Name Type Description Default
dialog Widget | Any

A :class:Widget to display as the dialog, or an intent resolved by the overlay's intent resolver (e.g. :class:BasicDialogIntent). For a transition other than the MD3 dialog one, call :meth:show directly.

required
dismiss_on_outside_tap bool

Whether tapping the scrim dismisses the dialog. Defaults to True.

True

Returns:

Name Type Description
An OverlayHandle[Any]

class:OverlayHandle for manual dismissal. A dialog shown from an

OverlayHandle[Any]

intent stays open across a hot reload, and the handle follows it.

Source code in src/nuiitivet/material/overlay.py
def dialog(
    self,
    dialog: Widget | Any,
    *,
    dismiss_on_outside_tap: bool = True,
) -> OverlayHandle[Any]:
    """Display a modal Material dialog.

    Args:
        dialog: A :class:`Widget` to display as the dialog, or an intent
            resolved by the overlay's intent resolver (e.g.
            :class:`BasicDialogIntent`). For a transition other than the
            MD3 dialog one, call :meth:`show` directly.
        dismiss_on_outside_tap: Whether tapping the scrim dismisses the
            dialog. Defaults to ``True``.

    Returns:
        An :class:`OverlayHandle` for manual dismissal. A dialog shown from an
        intent stays open across a hot reload, and the handle follows it.
    """
    handle = self.show(
        self._resolve(dialog),
        backdrop=True,
        dismiss_on_outside_tap=dismiss_on_outside_tap,
        transition=MaterialTransitions.dialog(),
    )
    if not isinstance(dialog, Widget):
        replay = partial(MaterialOverlay.dialog, dismiss_on_outside_tap=dismiss_on_outside_tap)
        self._record_for_restore(handle, dialog, replay)
    return handle

snackbar

snackbar(message: str | Snackbar, *, duration: float = 3.0) -> OverlayHandle[None]

Display a brief, non-blocking Material snackbar.

Parameters:

Name Type Description Default
message str | Snackbar

The message text, or a pre-built :class:Snackbar widget.

required
duration float

Seconds before the snackbar auto-dismisses. Defaults to 3.0.

3.0

Returns:

Name Type Description
An OverlayHandle[None]

class:OverlayHandle for the shown snackbar.

Source code in src/nuiitivet/material/overlay.py
def snackbar(
    self,
    message: str | Snackbar,
    *,
    duration: float = 3.0,
) -> OverlayHandle[None]:
    """Display a brief, non-blocking Material snackbar.

    Args:
        message: The message text, or a pre-built :class:`Snackbar` widget.
        duration: Seconds before the snackbar auto-dismisses. Defaults to ``3.0``.

    Returns:
        An :class:`OverlayHandle` for the shown snackbar.
    """
    widget: Widget = message if isinstance(message, Snackbar) else Snackbar(str(message))
    return self.show(
        widget,
        passthrough=True,
        timeout=float(duration),
        position=OverlayPosition.aligned("bottom-center", offset=(0.0, -24.0)),
        transition=MaterialTransitions.snackbar(),
    )

loading

loading(indicator: Widget | Any | None = None) -> OverlayHandle[Any]

Show a loading indicator overlay and return a handle for manual dismissal.

Parameters:

Name Type Description Default
indicator Widget | Any | None

Widget or intent to display as the loading indicator. Defaults to the built-in :class:LoadingIndicator, resolved through the :class:LoadingIntent (overridable via the overlay's intents).

None

Returns:

Name Type Description
An OverlayHandle[Any]

class:OverlayHandle that can be closed via handle.close(None).

Source code in src/nuiitivet/material/overlay.py
def loading(
    self,
    indicator: Widget | Any | None = None,
) -> OverlayHandle[Any]:
    """Show a loading indicator overlay and return a handle for manual dismissal.

    Args:
        indicator: Widget or intent to display as the loading indicator.
            Defaults to the built-in :class:`LoadingIndicator`, resolved
            through the :class:`LoadingIntent` (overridable via the
            overlay's ``intents``).

    Returns:
        An :class:`OverlayHandle` that can be closed via ``handle.close(None)``.
    """
    return self.show(
        self._resolve(LoadingIntent() if indicator is None else indicator),
        passthrough=True,
        timeout=None,
        position=OverlayPosition.aligned("center"),
    )

while_loading

while_loading(indicator: Widget | Any | None = None) -> WhileLoading

Return a context manager that shows a loading indicator for the duration of a block.

Use this form when the loading state is scoped to a with or async with block::

with MaterialOverlay.of(self).while_loading():
    do_work()

async with MaterialOverlay.of(self).while_loading():
    await fetch_data()

Internally delegates show/close to :meth:loading.

Parameters:

Name Type Description Default
indicator Widget | Any | None

Widget or intent to display as the loading indicator. Defaults to the built-in :class:LoadingIndicator.

None

Returns:

Name Type Description
A WhileLoading

class:WhileLoading context manager that shows the indicator on entry and closes it on exit.

Source code in src/nuiitivet/material/overlay.py
def while_loading(
    self,
    indicator: Widget | Any | None = None,
) -> WhileLoading:
    """Return a context manager that shows a loading indicator for the duration of a block.

    Use this form when the loading state is scoped to a ``with`` or ``async with`` block::

        with MaterialOverlay.of(self).while_loading():
            do_work()

        async with MaterialOverlay.of(self).while_loading():
            await fetch_data()

    Internally delegates show/close to :meth:`loading`.

    Args:
        indicator: Widget or intent to display as the loading indicator.
            Defaults to the built-in :class:`LoadingIndicator`.

    Returns:
        A :class:`WhileLoading` context manager that shows the indicator on entry and closes it on exit.
    """
    return WhileLoading(self, indicator)

side_sheet

side_sheet(sheet: Widget | Any, *, side: Literal['right', 'left'] = 'right', dismiss_on_outside_tap: bool = True) -> OverlayHandle[Any]

Display a modal side sheet.

side sets the edge the sheet slides in from, its alignment, and which corners are rounded. The rounding uses SideSheet.style.

Parameters:

Name Type Description Default
sheet Widget | Any

A widget containing a :class:SideSheet, possibly wrapped by modifiers, or an intent resolved by the overlay's intent resolver.

required
side Literal['right', 'left']

Edge the sheet slides in from ("right" or "left"). Defaults to "right".

'right'
dismiss_on_outside_tap bool

Whether tapping the scrim dismisses the sheet. Defaults to True.

True

Returns:

Name Type Description
An OverlayHandle[Any]

class:OverlayHandle for manual dismissal. A sheet shown from an

OverlayHandle[Any]

intent stays open across a hot reload, and the handle follows it.

Raises:

Type Description
TypeError

If the widget, or the one an intent resolves to, contains no :class:SideSheet.

Source code in src/nuiitivet/material/overlay.py
def side_sheet(
    self,
    sheet: Widget | Any,
    *,
    side: Literal["right", "left"] = "right",
    dismiss_on_outside_tap: bool = True,
) -> OverlayHandle[Any]:
    """Display a modal side sheet.

    ``side`` sets the edge the sheet slides in from, its alignment, and
    which corners are rounded. The rounding uses ``SideSheet.style``.

    Args:
        sheet: A widget containing a :class:`SideSheet`, possibly wrapped
            by modifiers, or an intent resolved by the overlay's intent
            resolver.
        side: Edge the sheet slides in from (``"right"`` or ``"left"``).
            Defaults to ``"right"``.
        dismiss_on_outside_tap: Whether tapping the scrim dismisses the sheet.
            Defaults to ``True``.

    Returns:
        An :class:`OverlayHandle` for manual dismissal. A sheet shown from an
        intent stays open across a hot reload, and the handle follows it.

    Raises:
        TypeError: If the widget, or the one an intent resolves to, contains
            no :class:`SideSheet`.
    """
    widget = self._resolve(sheet)
    inner = _find_descendant(widget, SideSheet)
    if inner is None:
        raise TypeError("side_sheet() requires a SideSheet widget (possibly wrapped by modifiers)")

    cr = float(inner.style.corner_radius)
    # Round only the inner (away-from-edge) corners: (tl, tr, br, bl).
    radius = (cr, 0.0, 0.0, cr) if side == "right" else (0.0, cr, cr, 0.0)
    alignment = "top-right" if side == "right" else "top-left"

    handle = self.show(
        widget.modifier(corner_radius(radius)),
        backdrop=True,
        dismiss_on_outside_tap=bool(dismiss_on_outside_tap),
        position=OverlayPosition.aligned(alignment),
        transition=MaterialTransitions.side_sheet(side=side),
    )
    if not isinstance(sheet, Widget):
        replay = partial(MaterialOverlay.side_sheet, side=side, dismiss_on_outside_tap=dismiss_on_outside_tap)
        self._record_for_restore(handle, sheet, replay)
    return handle

bottom_sheet

bottom_sheet(sheet: Widget | Any, *, dismiss_on_outside_tap: bool = True) -> OverlayHandle[Any]

Display a modal bottom sheet sliding up from the bottom edge.

The :class:BottomSheet widget owns the background, size and corner radius.

Parameters:

Name Type Description Default
sheet Widget | Any

A widget containing a :class:BottomSheet, possibly wrapped by modifiers, or an intent resolved by the overlay's intent resolver.

required
dismiss_on_outside_tap bool

Whether tapping the scrim dismisses the sheet. Defaults to True.

True

Returns:

Name Type Description
An OverlayHandle[Any]

class:OverlayHandle for manual dismissal. A sheet shown from an

OverlayHandle[Any]

intent stays open across a hot reload, and the handle follows it.

Raises:

Type Description
TypeError

If the widget, or the one an intent resolves to, contains no :class:BottomSheet.

Source code in src/nuiitivet/material/overlay.py
def bottom_sheet(
    self,
    sheet: Widget | Any,
    *,
    dismiss_on_outside_tap: bool = True,
) -> OverlayHandle[Any]:
    """Display a modal bottom sheet sliding up from the bottom edge.

    The :class:`BottomSheet` widget owns the background, size and corner radius.

    Args:
        sheet: A widget containing a :class:`BottomSheet`, possibly wrapped
            by modifiers, or an intent resolved by the overlay's intent
            resolver.
        dismiss_on_outside_tap: Whether tapping the scrim dismisses the sheet.
            Defaults to ``True``.

    Returns:
        An :class:`OverlayHandle` for manual dismissal. A sheet shown from an
        intent stays open across a hot reload, and the handle follows it.

    Raises:
        TypeError: If the widget, or the one an intent resolves to, contains
            no :class:`BottomSheet`.
    """
    widget = self._resolve(sheet)
    if _find_descendant(widget, BottomSheet) is None:
        raise TypeError("bottom_sheet() requires a BottomSheet widget (possibly wrapped by modifiers)")
    handle = self.show(
        widget,
        backdrop=True,
        dismiss_on_outside_tap=bool(dismiss_on_outside_tap),
        position=OverlayPosition.aligned("bottom-center"),
        transition=MaterialTransitions.bottom_sheet(),
    )
    if not isinstance(sheet, Widget):
        replay = partial(MaterialOverlay.bottom_sheet, dismiss_on_outside_tap=dismiss_on_outside_tap)
        self._record_for_restore(handle, sheet, replay)
    return handle

WhileLoading

WhileLoading(overlay: _LoadingHost, indicator: Widget | Any | None)

Bases: AbstractContextManager[None], AbstractAsyncContextManager[None]

Context manager that shows a loading indicator for the duration of a block.

Returned by :meth:MaterialOverlay.while_loading. Supports both with and async with usage::

with MaterialOverlay.of(self).while_loading():
    do_work()

async with MaterialOverlay.of(self).while_loading():
    await fetch_data()
Source code in src/nuiitivet/material/overlay.py
def __init__(self, overlay: _LoadingHost, indicator: Widget | Any | None) -> None:
    self._overlay = overlay
    self._indicator = indicator
    self._handle: OverlayHandle[Any] | None = None

OverlayProtocol

Bases: OverlayProtocol, Protocol

The Material overlay surface a ViewModel depends on.

Exported as nuiitivet.material.OverlayProtocol, mirroring how nuiitivet.material.Overlay names :class:~nuiitivet.material.overlay.MaterialOverlay. Annotate an injected overlay with it so the ViewModel presents content without owning widgets::

class ItemViewModel:
    def __init__(self, overlay: nv.OverlayProtocol) -> None:
        self._overlay = overlay

    async def delete(self) -> None:
        await self._overlay.dialog(BasicDialogIntent(title="Delete?"))

Prefer passing intents rather than widgets: dialog, loading and the sheet methods resolve them through the overlay's intent resolver, keeping widget construction in the View layer.

dialog

dialog(dialog: Widget | Any, *, dismiss_on_outside_tap: bool = True) -> OverlayHandle[Any]

Display a modal dialog from a widget or an intent.

Source code in src/nuiitivet/material/protocols.py
def dialog(
    self,
    dialog: Widget | Any,
    *,
    dismiss_on_outside_tap: bool = True,
) -> OverlayHandle[Any]:
    """Display a modal dialog from a widget or an intent."""
    ...

snackbar

snackbar(message: str | Snackbar, *, duration: float = 3.0) -> OverlayHandle[None]

Display a brief, non-blocking snackbar.

Source code in src/nuiitivet/material/protocols.py
def snackbar(
    self,
    message: str | Snackbar,
    *,
    duration: float = 3.0,
) -> OverlayHandle[None]:
    """Display a brief, non-blocking snackbar."""
    ...

loading

loading(indicator: Widget | Any | None = None) -> OverlayHandle[Any]

Show a loading indicator and return a handle for manual dismissal.

Source code in src/nuiitivet/material/protocols.py
def loading(
    self,
    indicator: Widget | Any | None = None,
) -> OverlayHandle[Any]:
    """Show a loading indicator and return a handle for manual dismissal."""
    ...

while_loading

while_loading(indicator: Widget | Any | None = None) -> WhileLoading

Return a (sync or async) context manager that shows a loading indicator.

Source code in src/nuiitivet/material/protocols.py
def while_loading(
    self,
    indicator: Widget | Any | None = None,
) -> WhileLoading:
    """Return a (sync or async) context manager that shows a loading indicator."""
    ...

side_sheet

side_sheet(sheet: Widget | Any, *, side: Literal['right', 'left'] = 'right', dismiss_on_outside_tap: bool = True) -> OverlayHandle[Any]

Display a modal side sheet from a widget or an intent.

Source code in src/nuiitivet/material/protocols.py
def side_sheet(
    self,
    sheet: Widget | Any,
    *,
    side: Literal["right", "left"] = "right",
    dismiss_on_outside_tap: bool = True,
) -> OverlayHandle[Any]:
    """Display a modal side sheet from a widget or an intent."""
    ...

bottom_sheet

bottom_sheet(sheet: Widget | Any, *, dismiss_on_outside_tap: bool = True) -> OverlayHandle[Any]

Display a modal bottom sheet from a widget or an intent.

Source code in src/nuiitivet/material/protocols.py
def bottom_sheet(
    self,
    sheet: Widget | Any,
    *,
    dismiss_on_outside_tap: bool = True,
) -> OverlayHandle[Any]:
    """Display a modal bottom sheet from a widget or an intent."""
    ...

ThemeFactory

Factory for creating Themes with Material Design configuration.

from_seed staticmethod

from_seed(seed_color: str, mode: str = 'light', name: str = '', *, variant: SchemeVariant = DEFAULT_VARIANT, contrast_level: float = DEFAULT_CONTRAST_LEVEL, form_factor: Optional[FormFactorLike] = None) -> Theme

Create a Material theme from a seed color.

variant and contrast_level default to the Material 3 defaults; see nuiitivet.material.theme.palette.from_seed. form_factor sets the component sizes: "desktop", "mobile" or a FormFactor. Omitted, it is "desktop".

Source code in src/nuiitivet/material/theme/material_theme.py
@staticmethod
def from_seed(
    seed_color: str,
    mode: str = "light",
    name: str = "",
    *,
    variant: SchemeVariant = DEFAULT_VARIANT,
    contrast_level: float = DEFAULT_CONTRAST_LEVEL,
    form_factor: Optional[FormFactorLike] = None,
) -> Theme:
    """Create a Material theme from a seed color.

    `variant` and `contrast_level` default to the Material 3 defaults; see
    `nuiitivet.material.theme.palette.from_seed`.
    `form_factor` sets the component sizes: `"desktop"`, `"mobile"` or a
    `FormFactor`. Omitted, it is `"desktop"`.
    """
    roles = from_seed(
        seed_color, dark=(mode == "dark"), variant=variant, contrast_level=contrast_level
    )

    resolved_form_factor = default_form_factor() if form_factor is None else FormFactor.of(form_factor)
    material_data = MaterialThemeData(roles=roles, form_factor=resolved_form_factor)
    return Theme(
        mode=mode,
        extensions=[
            material_data,
            _material_scrollbar_theme_data(),
            _material_menubar_theme_data(),
        ],
        name=name,
    )

from_seed_pair staticmethod

from_seed_pair(seed_color: str, name: str = '', *, variant: SchemeVariant = DEFAULT_VARIANT, contrast_level: float = DEFAULT_CONTRAST_LEVEL, form_factor: Optional[FormFactorLike] = None) -> Tuple[Theme, Theme]

Create light and dark themes from a seed color.

Source code in src/nuiitivet/material/theme/material_theme.py
@staticmethod
def from_seed_pair(
    seed_color: str,
    name: str = "",
    *,
    variant: SchemeVariant = DEFAULT_VARIANT,
    contrast_level: float = DEFAULT_CONTRAST_LEVEL,
    form_factor: Optional[FormFactorLike] = None,
) -> Tuple[Theme, Theme]:
    """Create light and dark themes from a seed color."""
    return (
        MaterialThemeFactory.from_seed(
            seed_color,
            mode="light",
            name=name,
            variant=variant,
            contrast_level=contrast_level,
            form_factor=form_factor,
        ),
        MaterialThemeFactory.from_seed(
            seed_color,
            mode="dark",
            name=name,
            variant=variant,
            contrast_level=contrast_level,
            form_factor=form_factor,
        ),
    )

DockedToolbar

DockedToolbar(buttons: Sequence[Widget], *, style: Optional[ToolbarStyle] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: _ToolbarBase

Material Design 3 docked toolbar.

Edge-to-edge by default: padding is 0 unless the caller sets it.

Initialize DockedToolbar.

Parameters:

Name Type Description Default
buttons Sequence[Widget]

Widgets placed inside the toolbar. Prefer Button or IconButton; other widgets (including tooltip-wrapped buttons) are laid out as-is, but the edge-inset heuristic assumes button-sized children and degrades gracefully for larger ones. A Material button without an explicit style, tooltip-wrapped or not, takes the colours the toolbar's colour scheme prescribes; a button with one keeps it.

required
style Optional[ToolbarStyle]

Optional toolbar style. Defaults to the theme's toolbar style, which itself falls back to ToolbarStyle.standard().

None
padding PaddingLike

Insets from the allocated rect to the toolbar.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/toolbar.py
def __init__(
    self,
    buttons: Sequence[Widget],
    *,
    style: Optional[ToolbarStyle] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize DockedToolbar.

    Args:
        buttons: Widgets placed inside the toolbar. Prefer ``Button`` or
            ``IconButton``; other widgets (including tooltip-wrapped buttons)
            are laid out as-is, but the edge-inset heuristic assumes
            button-sized children and degrades gracefully for larger ones.
            A Material button without an explicit style, tooltip-wrapped
            or not, takes the colours the toolbar's colour scheme
            prescribes; a button with one keeps it.
        style: Optional toolbar style. Defaults to the theme's toolbar
            style, which itself falls back to ``ToolbarStyle.standard()``.
        padding: Insets from the allocated rect to the toolbar.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    self._user_style = style
    self._applied_style = None
    # Read the preset directly rather than through ``self.style``: the theme
    # is unreachable until the widget is attached. The preset is what
    # ``ToolbarStyle.from_theme`` falls back to, so an unthemed app sees no
    # change; a themed one adopts its style on the first measure.
    effective_style = style if style is not None else ToolbarStyle.default()
    row_children: list[Widget] = list(buttons)

    self._content = Row(
        row_children,
        width="wt",
        gap=effective_style.item_gap,
        main_alignment="space-between",
        cross_alignment="center",
        padding=effective_style.content_insets,
    )

    # The container height is MD3-fixed, so the fixed sizing carries the padding band.
    _l, pad_top, _r, pad_bottom = parse_padding(padding if padding is not None else 0)
    super().__init__(
        child=self._content,
        height=effective_style.container_height + pad_top + pad_bottom,
        padding=padding,
        background_color=effective_style.background,
        border_color=effective_style.border_color,
        border_width=effective_style.border_width,
        corner_radius=effective_style.corner_radius,
        alignment="center",
        key=key,
    )

HorizontalFloatingToolbar

HorizontalFloatingToolbar(buttons: Sequence[Widget], *, fab: Optional[Fab] = None, padding: PaddingLike = 0, style: Optional[ToolbarStyle] = None, key: Optional[str] = None)

Bases: _FloatingToolbarBase

Material Design 3 horizontal floating toolbar.

Lays out action buttons in a row inside a fully rounded floating container. An optional FAB sits to the right of the container.

Initialize HorizontalFloatingToolbar.

Parameters:

Name Type Description Default
buttons Sequence[Widget]

Widgets placed inside the toolbar. Prefer Button or IconButton; other widgets (including tooltip-wrapped buttons) are laid out as-is, but the edge-inset heuristic assumes button-sized children and degrades gracefully for larger ones. A Material button without an explicit style, tooltip-wrapped or not, takes the colours the toolbar's colour scheme prescribes; a button with one keeps it.

required
fab Optional[Fab]

FAB placed to the right of the container, fab_gap of the style away. A FAB without an explicit style takes the colour and elevation the toolbar's colour scheme prescribes; a FAB with one keeps it.

None
padding PaddingLike

External padding around the floating toolbar and its FAB.

0
style Optional[ToolbarStyle]

Optional toolbar style. Defaults to ToolbarStyle.standard().

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/toolbar.py
def __init__(
    self,
    buttons: Sequence[Widget],
    *,
    fab: Optional[Fab] = None,
    padding: PaddingLike = 0,
    style: Optional[ToolbarStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize HorizontalFloatingToolbar.

    Args:
        buttons: Widgets placed inside the toolbar. Prefer ``Button`` or
            ``IconButton``; other widgets (including tooltip-wrapped buttons)
            are laid out as-is, but the edge-inset heuristic assumes
            button-sized children and degrades gracefully for larger ones.
            A Material button without an explicit style, tooltip-wrapped
            or not, takes the colours the toolbar's colour scheme
            prescribes; a button with one keeps it.
        fab: FAB placed to the right of the container, ``fab_gap`` of the
            style away. A FAB without an explicit style takes the colour
            and elevation the toolbar's colour scheme prescribes; a FAB
            with one keeps it.
        padding: External padding around the floating toolbar and its FAB.
        style: Optional toolbar style. Defaults to ``ToolbarStyle.standard()``.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(buttons, fab=fab, orientation="horizontal", padding=padding, style=style, key=key)

VerticalFloatingToolbar

VerticalFloatingToolbar(buttons: Sequence[Widget], *, fab: Optional[Fab] = None, padding: PaddingLike = 0, style: Optional[ToolbarStyle] = None, key: Optional[str] = None)

Bases: _FloatingToolbarBase

Material Design 3 vertical floating toolbar.

Lays out action buttons in a column inside a fully rounded floating container. An optional FAB sits below the container.

Initialize VerticalFloatingToolbar.

Parameters:

Name Type Description Default
buttons Sequence[Widget]

Widgets placed inside the toolbar. Prefer Button or IconButton; other widgets (including tooltip-wrapped buttons) are laid out as-is, but the edge-inset heuristic assumes button-sized children and degrades gracefully for larger ones. A Material button without an explicit style, tooltip-wrapped or not, takes the colours the toolbar's colour scheme prescribes; a button with one keeps it.

required
fab Optional[Fab]

FAB placed below the container, fab_gap of the style away. A FAB without an explicit style takes the colour and elevation the toolbar's colour scheme prescribes; a FAB with one keeps it.

None
padding PaddingLike

External padding around the floating toolbar and its FAB.

0
style Optional[ToolbarStyle]

Optional toolbar style. Defaults to ToolbarStyle.standard().

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/toolbar.py
def __init__(
    self,
    buttons: Sequence[Widget],
    *,
    fab: Optional[Fab] = None,
    padding: PaddingLike = 0,
    style: Optional[ToolbarStyle] = None,
    key: Optional[str] = None,
) -> None:
    """Initialize VerticalFloatingToolbar.

    Args:
        buttons: Widgets placed inside the toolbar. Prefer ``Button`` or
            ``IconButton``; other widgets (including tooltip-wrapped buttons)
            are laid out as-is, but the edge-inset heuristic assumes
            button-sized children and degrades gracefully for larger ones.
            A Material button without an explicit style, tooltip-wrapped
            or not, takes the colours the toolbar's colour scheme
            prescribes; a button with one keeps it.
        fab: FAB placed below the container, ``fab_gap`` of the style
            away. A FAB without an explicit style takes the colour and
            elevation the toolbar's colour scheme prescribes; a FAB with
            one keeps it.
        padding: External padding around the floating toolbar and its FAB.
        style: Optional toolbar style. Defaults to ``ToolbarStyle.standard()``.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(buttons, fab=fab, orientation="vertical", padding=padding, style=style, key=key)

Tooltip

Tooltip(message: str, *, width: SizingLike = None, height: SizingLike = None, style: TooltipStyle | None = None, padding: PaddingLike = 0, key: str | None = None)

Bases: ComposableWidget

Material Design 3 plain tooltip widget.

Initialize Tooltip.

Parameters:

Name Type Description Default
message str

Short plain-text tooltip message.

required
width SizingLike

Optional width sizing.

None
height SizingLike

Optional height sizing.

None
style TooltipStyle | None

Optional style token set. Defaults to TooltipStyle.standard().

None
padding PaddingLike

Insets from the allocated rect to the tooltip.

0
key str | None

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

None
Source code in src/nuiitivet/material/tooltip_widgets.py
def __init__(
    self,
    message: str,
    *,
    width: SizingLike = None,
    height: SizingLike = None,
    style: TooltipStyle | None = None,
    padding: PaddingLike = 0,
    key: str | None = None,
) -> None:
    """Initialize Tooltip.

    Args:
        message: Short plain-text tooltip message.
        width: Optional width sizing.
        height: Optional height sizing.
        style: Optional style token set. Defaults to TooltipStyle.standard().
        padding: Insets from the allocated rect to the tooltip.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, padding=padding, key=key)
    self.message = str(message)
    self._user_style = style

style property

style: TooltipStyle

Return tooltip style resolved from user style or current theme.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:TooltipStyle's presets.

Source code in src/nuiitivet/material/tooltip_widgets.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`TooltipStyle`'s presets."""
    return {"style": TooltipStyle}

RichTooltip

RichTooltip(supporting_text: str, *, subhead: str | None = None, action_label: str | None = None, on_action_click: Callable[[], None] | None = None, action_label_2: str | None = None, on_action_click_2: Callable[[], None] | None = None, width: SizingLike = None, height: SizingLike = None, style: RichTooltipStyle | None = None, padding: PaddingLike = 0, key: str | None = None)

Bases: ComposableWidget

Material Design 3 rich tooltip widget.

Initialize RichTooltip.

Parameters:

Name Type Description Default
supporting_text str

Main explanatory text.

required
subhead str | None

Optional short title line.

None
action_label str | None

Optional primary text button label.

None
on_action_click Callable[[], None] | None

Optional callback for primary action.

None
action_label_2 str | None

Optional secondary text button label.

None
on_action_click_2 Callable[[], None] | None

Optional callback for secondary action.

None
width SizingLike

Optional width sizing.

None
height SizingLike

Optional height sizing.

None
style RichTooltipStyle | None

Optional style token set. Defaults to RichTooltipStyle.standard().

None
padding PaddingLike

Insets from the allocated rect to the tooltip.

0
key str | None

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

None
Source code in src/nuiitivet/material/tooltip_widgets.py
def __init__(
    self,
    supporting_text: str,
    *,
    subhead: str | None = None,
    action_label: str | None = None,
    on_action_click: Callable[[], None] | None = None,
    action_label_2: str | None = None,
    on_action_click_2: Callable[[], None] | None = None,
    width: SizingLike = None,
    height: SizingLike = None,
    style: RichTooltipStyle | None = None,
    padding: PaddingLike = 0,
    key: str | None = None,
) -> None:
    """Initialize RichTooltip.

    Args:
        supporting_text: Main explanatory text.
        subhead: Optional short title line.
        action_label: Optional primary text button label.
        on_action_click: Optional callback for primary action.
        action_label_2: Optional secondary text button label.
        on_action_click_2: Optional callback for secondary action.
        width: Optional width sizing.
        height: Optional height sizing.
        style: Optional style token set. Defaults to RichTooltipStyle.standard().
        padding: Insets from the allocated rect to the tooltip.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, padding=padding, key=key)
    self.supporting_text = str(supporting_text)
    self.subhead = subhead
    self.action_label = action_label
    self.on_action_click = on_action_click
    self.action_label_2 = action_label_2
    self.on_action_click_2 = on_action_click_2
    self._user_style = style

style property

style: RichTooltipStyle

Return rich tooltip style resolved from user style or current theme.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:RichTooltipStyle's presets.

Source code in src/nuiitivet/material/tooltip_widgets.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`RichTooltipStyle`'s presets."""
    return {"style": RichTooltipStyle}

preferred_size

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

Return preferred size clamped to [min_width, max_width] when auto-sized.

Source code in src/nuiitivet/material/tooltip_widgets.py
def preferred_size(
    self,
    max_width: Optional[int] = None,
    max_height: Optional[int] = None,
) -> Tuple[int, int]:
    """Return preferred size clamped to [min_width, max_width] when auto-sized."""
    if self.width_sizing.kind != "fixed":
        style = self.style
        effective_max = style.max_width
        if max_width is not None:
            effective_max = min(effective_max, max_width)
        w, h = super().preferred_size(max_width=effective_max, max_height=max_height)
        return max(w, style.min_width), h
    return super().preferred_size(max_width=max_width, max_height=max_height)

SideSheetStyle dataclass

SideSheetStyle(width: SizingLike = 400, height: SizingLike = 'wt', corner_radius: float = 16.0, background_color: ColorSpec = SURFACE_CONTAINER_LOW)

Immutable container style for a modal side sheet.

The framework wraps caller-supplied content in a container sized by this style. height defaults to "wt" so the sheet spans the full screen height; any other weight does the same (see the module docstring). corner_radius is applied to the inner (away-from-edge) corners only. background_color defaults to ColorRole.SURFACE_CONTAINER_LOW per M3 spec.

copy_with

copy_with(**changes) -> 'SideSheetStyle'

Return a copy with the given fields replaced.

Source code in src/nuiitivet/material/styles/sheet_style.py
def copy_with(self, **changes) -> "SideSheetStyle":
    """Return a copy with the given fields replaced."""
    return replace(self, **changes)

BottomSheetStyle dataclass

BottomSheetStyle(width: SizingLike = 'wt', height: SizingLike = None, corner_radius: float = 28.0, background_color: ColorSpec = SURFACE_CONTAINER_LOW)

Immutable container style for a modal bottom sheet.

The framework wraps caller-supplied content in a container sized by this style. width defaults to "wt" so the sheet spans the full screen width; any other weight does the same (see the module docstring). height=None means the container sizes to its content; a fixed number (height=400) is the way to ask for a partial-height sheet. corner_radius is applied to the top corners only. background_color defaults to ColorRole.SURFACE_CONTAINER_LOW per M3 spec.

copy_with

copy_with(**changes) -> 'BottomSheetStyle'

Return a copy with the given fields replaced.

Source code in src/nuiitivet/material/styles/sheet_style.py
def copy_with(self, **changes) -> "BottomSheetStyle":
    """Return a copy with the given fields replaced."""
    return replace(self, **changes)

StandardSideSheetStyle dataclass

StandardSideSheetStyle(width: SizingLike = 256, height: SizingLike = 'wt', corner_radius: float = 0.0, background_color: ColorSpec = SURFACE, show_divider: bool = True)

Immutable container style for a standard (docked) side sheet.

A standard side sheet is part of the layout and sits beside main content. It does not use an overlay or scrim.

width defaults to 256 per M3 token md.comp.sheet.side.docked.container.width. height defaults to "wt" so the sheet spans the full content area height; any other weight does the same (see the module docstring). corner_radius defaults to 0.0 per M3 token md.comp.sheet.side.docked.container.shape (corner.none). background_color defaults to ColorRole.SURFACE per M3 token md.comp.sheet.side.docked.standard.container.color (elevation level 0). show_divider defaults to True. When True, a vertical Divider is rendered on the edge facing the main content area. The divider color is governed by the theme's outlineVariant role per M3 token md.comp.sheet.side.docked.divider.color.

copy_with

copy_with(**changes) -> 'StandardSideSheetStyle'

Return a copy with the given fields replaced.

Source code in src/nuiitivet/material/styles/sheet_style.py
def copy_with(self, **changes) -> "StandardSideSheetStyle":
    """Return a copy with the given fields replaced."""
    return replace(self, **changes)

SideSheet

SideSheet(content: Widget, *, headline: Union[str, ObservableBase[str]], on_back: Optional[Callable[[], None]] = None, show_back_button: Union[bool, ObservableBase[bool]] = False, style: Optional[SideSheetStyle] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget, OverlayAware[None]

Modal side sheet container widget.

Renders an M3-compliant header (optional Back button, Headline, Close button) above content. Pass this widget to MaterialOverlay.side_sheet().

The header layout is fixed by M3 spec::

[ Back (optional) ]  [ Headline ]  [ Close ]
Note

The Back button is visible only when show_back_button is truthy and on_back is not None. Providing show_back_button=True alone without on_back will silently suppress the button.

The Close button always dismisses the sheet through the overlay's unified dismissal pipeline. To intercept the close (for unsaved changes, etc.), attach a will_pop modifier::

overlay.side_sheet(
    SideSheet(content, headline="Settings")
    .modifier(will_pop(on_will_pop=lambda: not has_unsaved_changes))
)

The slide-in edge and corner rounding are owned by MaterialOverlay.side_sheet(sheet, side=...), not by this widget.

Initialize SideSheet.

Parameters:

Name Type Description Default
content Widget

Widget to display below the header.

required
headline Union[str, ObservableBase[str]]

Header title text (str or Observable[str]).

required
on_back Optional[Callable[[], None]]

Callback invoked when the Back icon button is pressed.

None
show_back_button Union[bool, ObservableBase[bool]]

Whether to show the Back icon button; an Observable[bool] toggles it, e.g. from in-sheet navigation state. Rendered only when truthy and on_back is not None.

False
style Optional[SideSheetStyle]

Container style. Defaults to :class:SideSheetStyle.

None
padding PaddingLike

Insets from the allocated rect to the sheet.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/sheet.py
def __init__(
    self,
    content: Widget,
    *,
    headline: Union[str, ObservableBase[str]],
    on_back: Optional[Callable[[], None]] = None,
    show_back_button: Union[bool, ObservableBase[bool]] = False,
    style: Optional[SideSheetStyle] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize SideSheet.

    Args:
        content: Widget to display below the header.
        headline: Header title text (str or Observable[str]).
        on_back: Callback invoked when the Back icon button is pressed.
        show_back_button: Whether to show the Back icon button; an
            ``Observable[bool]`` toggles it, e.g. from in-sheet navigation
            state. Rendered only when truthy **and** *on_back* is not
            ``None``.
        style: Container style. Defaults to :class:`SideSheetStyle`.
        padding: Insets from the allocated rect to the sheet.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    _style = style if style is not None else SideSheetStyle()
    pl, pt, pr, pb = parse_padding(padding if padding is not None else 0)
    super().__init__(
        width=_plus(_style.width, pl + pr),
        height=_plus(_style.height, pt + pb),
        padding=padding,
        key=key,
    )
    self._content = content
    self._headline = headline
    self._on_back = on_back
    self._show_back_button = show_back_button
    self._user_style = style

style property

Return resolved sheet style.

on_mount

on_mount() -> None

Mount and subscribe to show_back_button observable if provided.

Source code in src/nuiitivet/material/sheet.py
def on_mount(self) -> None:
    """Mount and subscribe to show_back_button observable if provided."""
    super().on_mount()
    if isinstance(self._show_back_button, ObservableBase):
        sub = self._show_back_button.subscribe(lambda _: self.rebuild())
        self.bind(sub)

build

build() -> Widget

Build the sheet: outer Box with header Row and content Column.

Source code in src/nuiitivet/material/sheet.py
def build(self) -> Widget:
    """Build the sheet: outer Box with header Row and content Column."""
    resolved_style = self.style

    # Header row: [Back slot] [Headline (weight)] [Close]
    # The back-button slot is always reserved (same width as IconButton default)
    # so the headline stays at a consistent horizontal position regardless of
    # whether the back button is visible.
    _BACK_SIZE = 40  # matches IconButton default size

    if self._resolve_show_back() and self._on_back is not None:
        back_slot: Widget = IconButton("arrow_back", on_click=self._on_back)
    else:
        back_slot = Box(width=_BACK_SIZE, height=_BACK_SIZE)

    header = Row(
        [
            back_slot,
            Box(
                Text(
                    self._headline,
                    style=TextStyle(color=ColorRole.ON_SURFACE_VARIANT),
                    type_scale=TypeScaleToken.from_size(22),
                ),
                width="wt",
                padding=(8, 0, 8, 0),
            ),
            IconButton("close", on_click=self._on_close_click if self._overlay_handle is not None else None),
        ],
        width="wt",
        height=72,
        padding=(4, 0, 4, 0),
        cross_alignment="center",
    )

    # Corner rounding is applied by ``MaterialOverlay.side_sheet()`` via the
    # ``corner_radius`` modifier (it depends on the slide-in edge, which this
    # widget does not know).  The container itself is square.
    return Box(
        Column(
            [header, self._content],
            width="wt",
        ),
        width=resolved_style.width,
        height=resolved_style.height,
        background_color=resolved_style.background_color,
        alignment="top-left",
    )

BottomSheet

BottomSheet(content: Widget, *, headline: Union[str, ObservableBase[str]], style: Optional[BottomSheetStyle] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget, OverlayAware[None]

Modal bottom sheet container widget.

Renders an M3-compliant header (Headline, Close button) above content. Pass this widget to MaterialOverlay.bottom_sheet().

The header layout is fixed by M3 spec::

[ Headline ]  [ Close ]

The Close button always dismisses the sheet through the overlay's unified dismissal pipeline. To intercept the close (for unsaved changes, etc.), attach a will_pop modifier.

Initialize BottomSheet.

Parameters:

Name Type Description Default
content Widget

Widget to display below the header.

required
headline Union[str, ObservableBase[str]]

Header title text (str or Observable[str]).

required
style Optional[BottomSheetStyle]

Container size, background and shape. Defaults to :class:BottomSheetStyle.

None
padding PaddingLike

Insets from the allocated rect to the sheet.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/sheet.py
def __init__(
    self,
    content: Widget,
    *,
    headline: Union[str, ObservableBase[str]],
    style: Optional[BottomSheetStyle] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize BottomSheet.

    Args:
        content: Widget to display below the header.
        headline: Header title text (str or Observable[str]).
        style: Container size, background and shape. Defaults to
            :class:`BottomSheetStyle`.
        padding: Insets from the allocated rect to the sheet.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    _style = style if style is not None else BottomSheetStyle()
    pl, pt, pr, pb = parse_padding(padding if padding is not None else 0)
    super().__init__(
        width=_plus(_style.width, pl + pr),
        height=_plus(_style.height, pt + pb),
        padding=padding,
        key=key,
    )
    self._content = content
    self._headline = headline
    self._user_style = style

style property

Return resolved sheet style.

build

build() -> Widget

Build the sheet: outer Box with header Row and content Column.

Source code in src/nuiitivet/material/sheet.py
def build(self) -> Widget:
    """Build the sheet: outer Box with header Row and content Column."""
    resolved_style = self.style

    header = Row(
        [
            Box(
                Text(
                    self._headline,
                    style=TextStyle(color=ColorRole.ON_SURFACE_VARIANT),
                    type_scale=TypeScaleToken.from_size(22),
                ),
                width="wt",
                padding=(8, 0, 8, 0),
            ),
            IconButton("close", on_click=self._on_close_click if self._overlay_handle is not None else None),
        ],
        width="wt",
        height=72,
        padding=(4, 0, 4, 0),
        cross_alignment="center",
    )

    # Round only the top corners.
    cr = float(resolved_style.corner_radius)
    corner_radius = (cr, cr, 0.0, 0.0)  # tl, tr, br, bl

    return Box(
        Column(
            [header, self._content],
            width="wt",
        ),
        width=resolved_style.width,
        height=resolved_style.height,
        corner_radius=corner_radius,
        background_color=resolved_style.background_color,
    )

StandardSideSheet

StandardSideSheet(content: Widget, *, opened: Union[bool, MutableObservableBase[bool]] = True, on_close_click: Optional[Callable[[], None]] = None, headline: Optional[Union[str, ObservableBase[str]]] = None, side: Literal['right', 'left'] = 'right', style: Optional[StandardSideSheetStyle] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget

Material Design 3 standard (docked) side sheet.

A standard side sheet is a permanent part of the layout, sitting beside the main content. It owns its open/close animation: the sheet stays mounted while its allocated width animates between the style width and zero::

opened: Observable[bool] = Observable(True)

Row([
    main_content,
    StandardSideSheet(panel_content, headline="Filters", opened=opened),
])

Toggling the sheet is a plain write to opened (opened.value = not opened.value). Conditionally rendering the sheet instead would unmount it and skip the animation.

The close icon button is rendered when the sheet can act on a press, i.e. when opened is a writable observable, when on_close_click is given, or both. With a literal bool opened and no callback there is nothing a press could do, so no button is shown.

Initialize StandardSideSheet.

Parameters:

Name Type Description Default
content Widget

Widget to display inside the sheet.

required
opened Union[bool, MutableObservableBase[bool]]

bool or writable Observable[bool] driving the expand/collapse animation.

True
on_close_click Optional[Callable[[], None]]

Callback invoked when the close icon button is pressed. Supplying it disables the default auto-close: the sheet does not write opened.value = False, and updating opened is the caller's job. This is the interception point for confirm-before-close flows.

None
headline Optional[Union[str, ObservableBase[str]]]

Header title (str or Observable[str]). When given, an M3 header row is rendered above content.

None
side Literal['right', 'left']

Edge the sheet is attached to ("right" or "left"); the sheet collapses toward it.

'right'
style Optional[StandardSideSheetStyle]

Container style. Defaults to :class:StandardSideSheetStyle.

None
padding PaddingLike

Insets from the allocated rect to the sheet.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/sheet.py
def __init__(
    self,
    content: Widget,
    *,
    opened: Union[bool, MutableObservableBase[bool]] = True,
    on_close_click: Optional[Callable[[], None]] = None,
    headline: Optional[Union[str, ObservableBase[str]]] = None,
    side: Literal["right", "left"] = "right",
    style: Optional[StandardSideSheetStyle] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize StandardSideSheet.

    Args:
        content: Widget to display inside the sheet.
        opened: ``bool`` or writable ``Observable[bool]`` driving the
            expand/collapse animation.
        on_close_click: Callback invoked when the close icon button is
            pressed.  **Supplying it disables the default auto-close**:
            the sheet does not write ``opened.value = False``, and
            updating *opened* is the caller's job.  This is the
            interception point for confirm-before-close flows.
        headline: Header title (str or Observable[str]).  When given, an
            M3 header row is rendered above *content*.
        side: Edge the sheet is attached to (``"right"`` or ``"left"``);
            the sheet collapses toward it.
        style: Container style.  Defaults to :class:`StandardSideSheetStyle`.
        padding: Insets from the allocated rect to the sheet.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    _style = style if style is not None else StandardSideSheetStyle()
    # Only the height is declared on this node: the parent resolves a
    # child's cross-axis size from ``height_sizing``, so a percentage
    # height must be visible there.  The width stays ``auto`` because the
    # open/close animation works by having ``Collapsible`` report an
    # animating preferred width to the parent.
    _l, pt, _r, pb = parse_padding(padding if padding is not None else 0)
    super().__init__(height=_plus(_style.height, pt + pb), padding=padding, key=key)
    self._content = content
    self._opened = opened
    self._on_close_click = on_close_click
    self._headline = headline
    self.side = side
    self._user_style = style

style property

Return the resolved sheet style.

on_mount

on_mount() -> None

Mount and subscribe to headline observable if provided.

Source code in src/nuiitivet/material/sheet.py
def on_mount(self) -> None:
    """Mount and subscribe to headline observable if provided."""
    super().on_mount()
    if isinstance(self._headline, ObservableBase):
        sub = self._headline.subscribe(lambda _: self.rebuild())
        self.bind(sub)

build

build() -> Widget

Build the sheet: a Collapsible wrapping the sheet container Box.

Source code in src/nuiitivet/material/sheet.py
def build(self) -> Widget:
    """Build the sheet: a Collapsible wrapping the sheet container Box."""
    resolved_style = self.style

    # Optionally build the header row (headline + close button).
    body_parts: list[Widget] = []
    show_close = self._show_close_button()
    if self._headline is not None or show_close:
        header_children: list[Widget] = []
        if self._headline is not None:
            header_children.append(
                Box(
                    Text(
                        self._headline,
                        style=TextStyle(color=ColorRole.ON_SURFACE_VARIANT),
                        type_scale=TypeScaleToken.from_size(22),
                    ),
                    width=Sizing.weight(1),
                    padding=(8, 0, 8, 0),
                )
            )
        if show_close:
            header_children.append(IconButton("close", on_click=self._handle_close_click))
        body_parts.append(
            Row(
                header_children,
                width="wt",
                height=72,
                padding=(4, 0, 4, 0),
                cross_alignment="center",
            )
        )
    body_parts.append(self._content)

    # The body fills the container so that content declaring a weight height
    # gets the space left over by the header.
    content_col = Column(body_parts, width=Sizing.weight(1), height=Sizing.weight(1))

    # Optionally add a vertical Divider on the edge facing the main content.
    if resolved_style.show_divider:
        divider = VerticalDivider()
        if self.side == "right":
            inner: Widget = Row([divider, content_col], width="wt", height="wt")
        else:
            inner = Row([content_col, divider], width="wt", height="wt")
    else:
        inner = content_col

    container = Box(
        inner,
        width=resolved_style.width,
        height=resolved_style.height,
        background_color=resolved_style.background_color,
        alignment="top-left",
    )

    # The collapse anchor is the edge the sheet is docked to: the child is
    # laid out at its natural width while the allocated rect shrinks, so
    # the docked edge must stay pinned.
    alignment = ("end", "start") if self.side == "right" else ("start", "start")
    return Collapsible(
        container,
        opened=self._opened,
        axis="horizontal",
        alignment=alignment,
        motion=EXPRESSIVE_DEFAULT_SPATIAL,
    )

GroupButton

GroupButton(label: 'str | ObservableBase[str] | None' = None, icon: 'Symbol | str | ObservableBase | None' = None, *, selected: 'bool | MutableObservableBase[bool]' = False, on_change: Optional[BoolCallback] = None, disabled: 'bool | MutableObservableBase[bool]' = False, width: SizingLike = None, style: 'Optional[ButtonGroupStyle]' = None, key: Optional[str] = None)

Bases: InteractiveWidget

A single interactive segment in a ButtonGroup (Standard or Connected).

Handles position-aware corner-radius shape morphing via EXPRESSIVE_FAST_SPATIAL motion on press / release. set_position() is called exclusively by the containing _ButtonGroupBase during on_mount; it is not part of the public user API.

Initialize GroupButton.

Parameters:

Name Type Description Default
label 'str | ObservableBase[str] | None'

Text label, or an observable string.

None
icon 'Symbol | str | ObservableBase | None'

Icon symbol, string name, or an observable of either.

None
selected 'bool | MutableObservableBase[bool]'

Initial selected state, or a writable observable bool bound to external state.

False
on_change Optional[BoolCallback]

Callback invoked with the new selected state after each toggle. In a ConnectedButtonGroup it runs together with the group's selection enforcement.

None
disabled 'bool | MutableObservableBase[bool]'

Whether the item ignores pointer events.

False
width SizingLike

Width sizing. A fixed value is the item's allocated width and sits out the pressed-width interaction. A content-fit item in a ConnectedButtonGroup becomes Sizing.weight(1) so such segments share the width equally.

None
style 'Optional[ButtonGroupStyle]'

Visual style override. When omitted the containing group's style is used; a group button standing on its own follows the theme's standard-group style.

None
key Optional[str]

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

None
Source code in src/nuiitivet/material/button_group.py
def __init__(
    self,
    label: "str | ObservableBase[str] | None" = None,
    icon: "Symbol | str | ObservableBase | None" = None,
    *,
    selected: "bool | MutableObservableBase[bool]" = False,
    on_change: Optional[BoolCallback] = None,
    disabled: "bool | MutableObservableBase[bool]" = False,
    width: SizingLike = None,
    style: "Optional[ButtonGroupStyle]" = None,
    key: Optional[str] = None,
) -> None:
    """Initialize GroupButton.

    Args:
        label: Text label, or an observable string.
        icon: Icon symbol, string name, or an observable of either.
        selected: Initial selected state, or a writable observable bool
            bound to external state.
        on_change: Callback invoked with the new selected state after
            each toggle. In a ``ConnectedButtonGroup`` it runs together
            with the group's selection enforcement.
        disabled: Whether the item ignores pointer events.
        width: Width sizing. A fixed value is the item's allocated width
            and sits out the pressed-width interaction. A content-fit item
            in a ``ConnectedButtonGroup`` becomes ``Sizing.weight(1)`` so
            such segments share the width equally.
        style: Visual style override. When omitted the containing
            group's style is used; a group button standing on its own
            follows the theme's standard-group style.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    from nuiitivet.material.styles.button_group_style import StandardButtonGroupStyle

    if label is None and icon is None:
        raise ValueError("GroupButton requires at least one of label or icon")

    self._has_user_style = style is not None
    #: Set by the containing group, which resolves one style for the whole
    #: row and pushes it down; an item that has one must not pull its own.
    self._group_styled = False
    # Held in a local for everything below ``super().__init__()``: reads that
    # run before the widget is attached must not go through an accessor that
    # could reach for the theme, which is not resolvable yet. The preset is
    # what ``StandardButtonGroupStyle.from_theme`` falls back to, so an
    # unthemed app sees no change.
    effective_style: "ButtonGroupStyle" = style or StandardButtonGroupStyle.default()
    self._style: "ButtonGroupStyle" = effective_style
    self._label = label
    self._icon = icon

    # on_change is interceptable by the containing group
    self._on_change: Optional[BoolCallback] = on_change

    # Selected state
    self._selected_external: "Optional[MutableObservableBase[bool]]" = None
    if hasattr(selected, "subscribe") and hasattr(selected, "value"):
        self._selected_external = cast("MutableObservableBase[bool]", selected)
        self._selected: bool = bool(self._selected_external.value)
    else:
        self._selected = bool(selected)

    # Corner animation state
    self._position: ButtonGroupPosition = "only"
    self._adjacent_animation: bool = True
    self._persistent_selected_pressed_shape: bool = False
    self._connected_inner_press_only: bool = False
    self._own_pressed: bool = False

    # Adjacent width-interaction state (Standard groups only).  Each item
    # exposes only a 0..1 "active" progress and its natural content-fit
    # width; the parent group layout (``_ButtonGroupRow``) reads these in a
    # single measure pass to grow the active item and compress its direct
    # neighbors, keeping the group width conserved (mirrors M3 Compose's
    # ButtonGroup, which avoids per-child layout jitter).  The width is NOT
    # animated per item here.
    self._base_width: float = float(effective_style.min_item_width)

    # Store child widget refs for colour updates
    self._text_widget: "Optional[Widget]" = None
    self._icon_widget_ref: "Optional[Widget]" = None

    # Compute initial effective colours
    bg, fg, bc, bw = self._effective_colors()

    # Build content child (stores text/icon refs)
    content = self._build_content(fg)

    # Initialize corner animation (no motion yet; motion is enabled in
    # set_position() so the initial position snap is immediate)
    initial_corners = self._compute_raw_idle_corners(
        effective_style.outer_corner_radius,
        effective_style.outer_corner_radius,  # "only" position: all outer
    )
    self._corner_anim: "Animatable[Tuple[float, float, float, float]]" = Animatable.vector(
        initial_value=initial_corners,
        converter=_CORNER_CONVERTER,
        motion=None,  # Enabled after first set_position()
    )

    # Active progress 0..1 (motion enabled in set_position()).  Drives the
    # parent-computed width interaction; ticks only request a re-layout.
    self._press_progress: "Animatable[float]" = Animatable(0.0, motion=None)

    super().__init__(
        child=content,
        on_click=self._handle_click,
        on_press=self._handle_press_down,
        on_release=self._handle_press_up,
        disabled=disabled,
        width=width,
        height=effective_style.container_height,
        # No box padding: the leading/trailing space is reserved via
        # ``preferred_size`` and rendered by *centering* the content (see
        # ``_side_space``).  This keeps the icon/label centred so the
        # pressed-width interaction compresses neighbours symmetrically.
        padding=0,
        alignment="center",
        background_color=bg,
        border_color=bc,
        border_width=bw,
        corner_radius=initial_corners,
        state_layer_color=effective_style.overlay_color or ColorRole.ON_SURFACE,
        key=key,
    )

    # Override state-layer opacities from style
    self._PRESS_OPACITY = effective_style.overlay_alpha
    self._HOVER_OPACITY = effective_style.overlay_alpha * 2 / 3

preferred_size

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

Return preferred size.

A fixed width is the item's allocated width: no side space is added on top of it. A content-fit item measures as content plus the reserved leading and trailing space.

Connected groups enforce a visual minimum width (M3: 48dp for XS/S segments). Standard groups are content-fit: their 48dp spec value is an accessible tap-target requirement, not a visual width floor, so it is intentionally not applied to the rendered width here.

Parameters:

Name Type Description Default
max_width Optional[int]

Available width constraint.

None
max_height Optional[int]

Available height constraint.

None

Returns:

Type Description
Tuple[int, int]

(width, height) in pixels.

Source code in src/nuiitivet/material/button_group.py
def preferred_size(self, max_width: Optional[int] = None, max_height: Optional[int] = None) -> Tuple[int, int]:
    """Return preferred size.

    A fixed ``width`` is the item's allocated width: no side space is added
    on top of it. A content-fit item measures as content plus the reserved
    leading and trailing space.

    Connected groups enforce a visual minimum width (M3: 48dp for XS/S
    segments).  Standard groups are content-fit: their 48dp spec value is an
    accessible **tap-target** requirement, not a visual width floor, so it
    is intentionally not applied to the rendered width here.

    Args:
        max_width: Available width constraint.
        max_height: Available height constraint.

    Returns:
        ``(width, height)`` in pixels.
    """
    self._sync_theme_style()
    w, _h = super().preferred_size(max_width=max_width, max_height=max_height)
    if self.width_sizing.kind != "fixed":
        # Content is centred with zero box padding, so ``super`` returns
        # the bare content width; add the reserved leading + trailing
        # space so the idle width equals content + 2 × side-space.
        w += 2 * self._side_space()
        if not self._adjacent_animation:  # Connected groups only
            w = max(w, self._style.min_item_width)
    return (int(w), self._style.container_height)

set_position

set_position(position: ButtonGroupPosition, adjacent_animation: bool = True) -> None

Configure this item's position within its group.

Called exclusively by _ButtonGroupBase.on_mount(). Snaps the corner radius to the idle value for the given position without animation, then arms the EXPRESSIVE_FAST_SPATIAL motion for subsequent press interactions.

Parameters:

Name Type Description Default
position ButtonGroupPosition

One of "start", "middle", "end", "only".

required
adjacent_animation bool

True for Standard groups (the active item's width grows and neighbors compress); False for Connected.

True
Source code in src/nuiitivet/material/button_group.py
def set_position(
    self,
    position: ButtonGroupPosition,
    adjacent_animation: bool = True,
) -> None:
    """Configure this item's position within its group.

    Called exclusively by ``_ButtonGroupBase.on_mount()``.  Snaps the
    corner radius to the idle value for the given position without
    animation, then arms the ``EXPRESSIVE_FAST_SPATIAL`` motion for
    subsequent press interactions.

    Args:
        position: One of ``"start"``, ``"middle"``, ``"end"``, ``"only"``.
        adjacent_animation: ``True`` for Standard groups (the active item's
            width grows and neighbors compress); ``False`` for Connected.
    """
    self._position = position
    self._adjacent_animation = adjacent_animation
    self._own_pressed = False

    # Capture the natural content-fit width (the parent layout grows/
    # compresses around this base).  Arm the MD3-spec spring on the active
    # progress so press/select transitions are smooth.
    self._base_width = float(self.preferred_size()[0])
    self._press_progress.snap_to(0.0)
    self._press_progress.set_motion(STANDARD_BUTTON_GROUP_WIDTH)

    idle = self._compute_target_corners(False)

    # Snap the animation to idle (no motion for position init)
    self._corner_anim.stop()
    # Directly set internal observable to avoid a spurious animation tick
    self._corner_anim._value.value = idle  # type: ignore[attr-defined]
    self._corner_anim._target = idle  # type: ignore[attr-defined]
    if self._corner_anim._state is not None:  # type: ignore[attr-defined]
        v = _CORNER_CONVERTER.to_vector(idle)
        state = self._corner_anim._state  # type: ignore[attr-defined]
        state.value = v.copy()
        state.start = v.copy()
        state.target = v.copy()

    # Enable expressive motion for future press interactions
    self._corner_anim._motion = EXPRESSIVE_FAST_SPATIAL  # type: ignore[attr-defined]
    v0 = _CORNER_CONVERTER.to_vector(idle)
    self._corner_anim._state = EXPRESSIVE_FAST_SPATIAL.create_state(v0, v0)  # type: ignore[attr-defined]

    # Apply immediately to Box's corner_radius (invalidates paint cache)
    self.corner_radius = idle

on_mount

on_mount() -> None

Subscribe to corner animation and external selected observable.

Source code in src/nuiitivet/material/button_group.py
def on_mount(self) -> None:
    """Subscribe to corner animation and external selected observable."""
    super().on_mount()

    # Subscribe to corner animation ticks
    self.bind(self._corner_anim.subscribe(self._on_corner_value_changed))

    # Subscribe to active-progress ticks: request a parent re-layout so the
    # group recomputes all widths in a single coordinated pass.
    self.bind(self._press_progress.subscribe(self._on_progress_changed))

    # Subscribe to external selected observable if provided
    if self._selected_external is not None:
        sub = self._selected_external.subscribe(lambda v: self._set_selected(bool(v)))
        self.bind(sub)

StandardButtonGroup

StandardButtonGroup(items: Sequence[GroupButton], *, style: 'Optional[StandardButtonGroupStyle]' = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: _ButtonGroupBase

A ButtonGroup that organises action or toggle segments horizontally.

Width fits the combined item widths. When a segment is activated (pressed) or selected, the MD3 adjacent interaction runs: the active segment animates its width, shape, and (via centered content) padding, while its direct neighbors shrink to compensate so the group's overall width stays stable. All transitions use M3 Expressive (EXPRESSIVE_FAST_SPATIAL) motion. Item selected states are independent — no group-level enforcement.

Initialize StandardButtonGroup.

Parameters:

Name Type Description Default
items Sequence[GroupButton]

Between 2 and 5 GroupButton instances.

required
style 'Optional[StandardButtonGroupStyle]'

Visual style: StandardButtonGroupStyle.filled(), .tonal() or .outlined(), each taking an optional size (filled("m")). Defaults to the theme's standard button group style, which falls back to filled() at size "s".

None
padding PaddingLike

Insets from the allocated rect to the group.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/button_group.py
def __init__(
    self,
    items: Sequence[GroupButton],
    *,
    style: "Optional[StandardButtonGroupStyle]" = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize StandardButtonGroup.

    Args:
        items: Between 2 and 5 ``GroupButton`` instances.
        style: Visual style: ``StandardButtonGroupStyle.filled()``,
            ``.tonal()`` or ``.outlined()``, each taking an optional size
            (``filled("m")``). Defaults to the theme's standard button
            group style, which falls back to ``filled()`` at size ``"s"``.
        padding: Insets from the allocated rect to the group.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    from nuiitivet.material.styles.button_group_style import (
        StandardButtonGroupStyle as _Std,
    )

    self._has_user_style = style is not None
    eff_style = style if style is not None else _Std.default()
    super().__init__(
        items,
        adjacent_animation=True,
        persistent_selected_pressed_shape=True,
        connected_inner_press_only=False,
        group_width=None,  # Fits content
        style=eff_style,
        padding=padding,
        key=key,
    )

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:StandardButtonGroupStyle's presets.

Source code in src/nuiitivet/material/button_group.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`StandardButtonGroupStyle`'s presets."""
    from nuiitivet.material.styles.button_group_style import StandardButtonGroupStyle

    return {"style": StandardButtonGroupStyle}

ConnectedButtonGroup

ConnectedButtonGroup(items: Sequence[GroupButton], *, select_mode: Literal['single', 'multi'] = 'single', style: 'Optional[ConnectedButtonGroupStyle]' = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: _ButtonGroupBase

A ButtonGroup that functions as an option selector / view switcher.

Width expands to fill the containing widget (width="wt"). Items share space equally (Sizing.weight(1)). Only corner shapes animate on press — adjacent segment corners are unaffected. Selection is always enforced by the group.

Initialize ConnectedButtonGroup.

Parameters:

Name Type Description Default
items Sequence[GroupButton]

Between 2 and 5 GroupButton instances.

required
select_mode Literal['single', 'multi']

"single" keeps at most one item selected; "multi" allows any combination.

'single'
style 'Optional[ConnectedButtonGroupStyle]'

Visual style: ConnectedButtonGroupStyle.filled(), .tonal() or .outlined(), each taking an optional size (filled("m")). Defaults to the theme's connected button group style, which falls back to filled() at size "s".

None
padding PaddingLike

Insets from the allocated rect to the group.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/button_group.py
def __init__(
    self,
    items: Sequence[GroupButton],
    *,
    select_mode: Literal["single", "multi"] = "single",
    style: "Optional[ConnectedButtonGroupStyle]" = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize ConnectedButtonGroup.

    Args:
        items: Between 2 and 5 ``GroupButton`` instances.
        select_mode: ``"single"`` keeps at most one item selected;
            ``"multi"`` allows any combination.
        style: Visual style: ``ConnectedButtonGroupStyle.filled()``,
            ``.tonal()`` or ``.outlined()``, each taking an optional size
            (``filled("m")``). Defaults to the theme's connected button
            group style, which falls back to ``filled()`` at size ``"s"``.
        padding: Insets from the allocated rect to the group.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    from nuiitivet.material.styles.button_group_style import (
        ConnectedButtonGroupStyle as _Con,
    )

    self._has_user_style = style is not None
    eff_style = style if style is not None else _Con.default()
    self._select_mode = select_mode

    super().__init__(
        items,
        adjacent_animation=False,
        persistent_selected_pressed_shape=False,
        connected_inner_press_only=True,
        group_width="wt",
        style=eff_style,
        padding=padding,
        key=key,
    )

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:ConnectedButtonGroupStyle's presets.

Source code in src/nuiitivet/material/button_group.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`ConnectedButtonGroupStyle`'s presets."""
    from nuiitivet.material.styles.button_group_style import ConnectedButtonGroupStyle

    return {"style": ConnectedButtonGroupStyle}

on_mount

on_mount() -> None

Assign positions, set weight widths, and wire group selection logic.

Source code in src/nuiitivet/material/button_group.py
def on_mount(self) -> None:
    """Assign positions, set weight widths, and wire group selection logic."""
    super().on_mount()  # Calls _ButtonGroupBase.on_mount → set_position()

    # Equal-width distribution for connected layout; a fixed width is the
    # caller's choice and keeps its value.
    for item in self._items:
        if _is_fixed_width(item):
            continue
        item.width_sizing = Sizing.weight(1)
        item.mark_needs_layout()

    # Intercept each item's on_change to apply group selection logic
    for i, item in enumerate(self._items):
        original_on_change = item._on_change

        def _make_wrapper(
            item_idx: int,
            orig_cb: Optional[BoolCallback],
        ) -> BoolCallback:
            def _wrapper(selected: bool) -> None:
                # 1. Item-level callback fires first
                if orig_cb is not None:
                    invoke_event_handler(
                        orig_cb,
                        selected,
                        error_key="group_button_item_on_change",
                        error_msg="GroupButton item on_change raised",
                        owner_name=type(item).__name__,
                    )
                # 2. Group selection logic
                self._handle_group_selection_change(item_idx, selected)

            return _wrapper

        item._on_change = _make_wrapper(i, original_on_change)

StandardButtonGroupStyle dataclass

StandardButtonGroupStyle(background: Optional[ColorSpec] = None, foreground: Optional[ColorSpec] = None, border_color: Optional[ColorSpec] = None, border_width: float = 0.0, selected_background: Optional[ColorSpec] = None, selected_foreground: Optional[ColorSpec] = None, container_height: int = 40, item_gap: int = 12, min_item_width: int = 48, inner_padding: int = 16, icon_size: int = 20, label_size: int = 14, icon_label_space: int = 8, outer_corner_radius: float = 20.0, pressed_outer_corner_radius: float = 12.0, pressed_inner_corner_radius: float = 12.0, pressed_width_multiplier: float = 0.15, overlay_color: Optional[ColorSpec] = None, overlay_alpha: float = 0.12)

Immutable style for StandardButtonGroup (M3-compliant).

All segments are independent fully-rounded pills. There is no junction-corner concept; inner_corner_radius always equals outer_corner_radius (exposed as a read-only property).

Use filled(), tonal(), or outlined() to create a preset, optionally passing a ButtonSize.

inner_corner_radius property

inner_corner_radius: float

Inner corner radius equals outer (fully-rounded pill).

selected_inner_corner_radius property

selected_inner_corner_radius: float

Not applicable; returns 0.0.

selected_border_color property

selected_border_color: Optional[ColorSpec]

No distinct selected border; falls back to border_color.

copy_with

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

Return a copy with the specified fields replaced.

Source code in src/nuiitivet/material/styles/button_group_style.py
def copy_with(self, **changes: Any) -> "StandardButtonGroupStyle":
    """Return a copy with the specified fields replaced."""
    return replace(self, **changes)

filled classmethod

filled(size: ButtonSize = 's') -> 'StandardButtonGroupStyle'

Create a filled-variant style.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'
Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def filled(cls, size: ButtonSize = "s") -> "StandardButtonGroupStyle":
    """Create a filled-variant style.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).
    """
    t = _STANDARD_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.SURFACE_CONTAINER_HIGHEST,
        foreground=ColorRole.ON_SURFACE,
        border_width=0.0,
        overlay_color=ColorRole.ON_SURFACE,
        overlay_alpha=0.08,
        selected_background=ColorRole.PRIMARY,
        selected_foreground=ColorRole.ON_PRIMARY,
        container_height=int(t["container_height"]),
        item_gap=int(t["item_gap"]),
        icon_size=int(t["icon_size"]),
        label_size=int(t["label_size"]),
        icon_label_space=int(t["icon_label_space"]),
        inner_padding=int(t["inner_padding"]),
        outer_corner_radius=float(t["outer_corner_radius"]),
        pressed_outer_corner_radius=float(t["pressed_corner_radius"]),
        pressed_inner_corner_radius=float(t["pressed_corner_radius"]),
    )

tonal classmethod

tonal(size: ButtonSize = 's') -> 'StandardButtonGroupStyle'

Create a tonal-variant style.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'
Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def tonal(cls, size: ButtonSize = "s") -> "StandardButtonGroupStyle":
    """Create a tonal-variant style.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).
    """
    t = _STANDARD_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.SECONDARY_CONTAINER,
        foreground=ColorRole.ON_SECONDARY_CONTAINER,
        border_width=0.0,
        overlay_color=ColorRole.ON_SURFACE,
        overlay_alpha=0.08,
        selected_background=ColorRole.SECONDARY,
        selected_foreground=ColorRole.ON_SECONDARY,
        container_height=int(t["container_height"]),
        item_gap=int(t["item_gap"]),
        icon_size=int(t["icon_size"]),
        label_size=int(t["label_size"]),
        icon_label_space=int(t["icon_label_space"]),
        inner_padding=int(t["inner_padding"]),
        outer_corner_radius=float(t["outer_corner_radius"]),
        pressed_outer_corner_radius=float(t["pressed_corner_radius"]),
        pressed_inner_corner_radius=float(t["pressed_corner_radius"]),
    )

outlined classmethod

outlined(size: ButtonSize = 's') -> 'StandardButtonGroupStyle'

Create an outlined-variant style.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'
Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def outlined(cls, size: ButtonSize = "s") -> "StandardButtonGroupStyle":
    """Create an outlined-variant style.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).
    """
    t = _STANDARD_SIZE_TOKENS[size]
    return cls(
        background=None,
        foreground=ColorRole.ON_SURFACE,
        border_color=ColorRole.OUTLINE,
        border_width=1.0,
        overlay_color=ColorRole.PRIMARY,
        overlay_alpha=0.08,
        selected_background=ColorRole.INVERSE_SURFACE,
        selected_foreground=ColorRole.INVERSE_ON_SURFACE,
        container_height=int(t["container_height"]),
        item_gap=int(t["item_gap"]),
        icon_size=int(t["icon_size"]),
        label_size=int(t["label_size"]),
        icon_label_space=int(t["icon_label_space"]),
        inner_padding=int(t["inner_padding"]),
        outer_corner_radius=float(t["outer_corner_radius"]),
        pressed_outer_corner_radius=float(t["pressed_corner_radius"]),
        pressed_inner_corner_radius=float(t["pressed_corner_radius"]),
    )

default classmethod

default() -> 'StandardButtonGroupStyle'

Return the framework default, ignoring any theme.

This is what a standard button group renders with before it is mounted, and what :meth:from_theme falls back to when no Material theme is installed.

Returns:

Type Description
'StandardButtonGroupStyle'

The filled standard-group style at size "s".

Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def default(cls) -> "StandardButtonGroupStyle":
    """Return the framework default, ignoring any theme.

    This is what a standard button group renders with before it is
    mounted, and what :meth:`from_theme` falls back to when no Material
    theme is installed.

    Returns:
        The filled standard-group style at size ``"s"``.
    """
    return cls.filled("s")

from_theme classmethod

from_theme(theme: 'Theme | None') -> 'StandardButtonGroupStyle'

Resolve the standard button group style from theme.

Parameters:

Name Type Description Default
theme 'Theme | None'

The active theme, or None when there is none.

required

Returns:

Type Description
'StandardButtonGroupStyle'

Resolved standard-group style.

Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def from_theme(cls, theme: "Theme | None") -> "StandardButtonGroupStyle":
    """Resolve the standard button group style from ``theme``.

    Args:
        theme: The active theme, or ``None`` when there is none.

    Returns:
        Resolved standard-group style.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    if theme is not None:
        theme_data = theme.extension(MaterialThemeData)
        if theme_data is not None:
            return theme_data.standard_button_group_style
    return cls.default()

ConnectedButtonGroupStyle dataclass

ConnectedButtonGroupStyle(background: Optional[ColorSpec] = None, foreground: Optional[ColorSpec] = None, border_color: Optional[ColorSpec] = None, border_width: float = 0.0, selected_background: Optional[ColorSpec] = None, selected_foreground: Optional[ColorSpec] = None, selected_border_color: Optional[ColorSpec] = None, container_height: int = 40, item_gap: int = 2, min_item_width: int = 48, icon_size: int = 20, label_size: int = 14, icon_label_space: int = 8, outer_corner_radius: float = 20.0, inner_corner_radius: float = 8.0, pressed_outer_corner_radius: float = 8.0, pressed_inner_corner_radius: float = 4.0, selected_inner_corner_radius: float = 0.0, overlay_color: Optional[ColorSpec] = None, overlay_alpha: float = 0.12)

Immutable style for ConnectedButtonGroup (M3-compliant).

Segments are tightly connected with distinct junction corners. Supports selection-state colours and a separate selected_inner_corner_radius.

Use filled(), tonal(), or outlined() to create a preset, optionally passing a ButtonSize.

copy_with

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

Return a copy with the specified fields replaced.

Source code in src/nuiitivet/material/styles/button_group_style.py
def copy_with(self, **changes: Any) -> "ConnectedButtonGroupStyle":
    """Return a copy with the specified fields replaced."""
    return replace(self, **changes)

filled classmethod

filled(size: ButtonSize = 's') -> 'ConnectedButtonGroupStyle'

Create a filled-variant style.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'
Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def filled(cls, size: ButtonSize = "s") -> "ConnectedButtonGroupStyle":
    """Create a filled-variant style.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).
    """
    t = _CONNECTED_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.SURFACE_CONTAINER_HIGHEST,
        foreground=ColorRole.ON_SURFACE,
        border_width=0.0,
        overlay_color=ColorRole.ON_SURFACE,
        overlay_alpha=0.08,
        selected_background=ColorRole.PRIMARY,
        selected_foreground=ColorRole.ON_PRIMARY,
        container_height=int(t["container_height"]),
        item_gap=int(t["item_gap"]),
        icon_size=int(t["icon_size"]),
        label_size=int(t["label_size"]),
        icon_label_space=int(t["icon_label_space"]),
        outer_corner_radius=float(t["outer_corner_radius"]),
        inner_corner_radius=float(t["inner_corner_radius"]),
        pressed_inner_corner_radius=float(t["pressed_inner_corner_radius"]),
    )

tonal classmethod

tonal(size: ButtonSize = 's') -> 'ConnectedButtonGroupStyle'

Create a tonal-variant style.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'
Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def tonal(cls, size: ButtonSize = "s") -> "ConnectedButtonGroupStyle":
    """Create a tonal-variant style.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).
    """
    t = _CONNECTED_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.SECONDARY_CONTAINER,
        foreground=ColorRole.ON_SECONDARY_CONTAINER,
        border_width=0.0,
        overlay_color=ColorRole.ON_SURFACE,
        overlay_alpha=0.08,
        selected_background=ColorRole.SECONDARY,
        selected_foreground=ColorRole.ON_SECONDARY,
        container_height=int(t["container_height"]),
        item_gap=int(t["item_gap"]),
        icon_size=int(t["icon_size"]),
        label_size=int(t["label_size"]),
        icon_label_space=int(t["icon_label_space"]),
        outer_corner_radius=float(t["outer_corner_radius"]),
        inner_corner_radius=float(t["inner_corner_radius"]),
        pressed_inner_corner_radius=float(t["pressed_inner_corner_radius"]),
    )

outlined classmethod

outlined(size: ButtonSize = 's') -> 'ConnectedButtonGroupStyle'

Create an outlined-variant style.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'
Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def outlined(cls, size: ButtonSize = "s") -> "ConnectedButtonGroupStyle":
    """Create an outlined-variant style.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).
    """
    t = _CONNECTED_SIZE_TOKENS[size]
    return cls(
        background=None,
        foreground=ColorRole.ON_SURFACE,
        border_color=ColorRole.OUTLINE,
        border_width=1.0,
        overlay_color=ColorRole.PRIMARY,
        overlay_alpha=0.08,
        selected_background=ColorRole.INVERSE_SURFACE,
        selected_foreground=ColorRole.INVERSE_ON_SURFACE,
        selected_border_color=ColorRole.OUTLINE,
        container_height=int(t["container_height"]),
        item_gap=int(t["item_gap"]),
        icon_size=int(t["icon_size"]),
        label_size=int(t["label_size"]),
        icon_label_space=int(t["icon_label_space"]),
        outer_corner_radius=float(t["outer_corner_radius"]),
        inner_corner_radius=float(t["inner_corner_radius"]),
        pressed_inner_corner_radius=float(t["pressed_inner_corner_radius"]),
    )

default classmethod

default() -> 'ConnectedButtonGroupStyle'

Return the framework default, ignoring any theme.

This is what a connected button group renders with before it is mounted, and what :meth:from_theme falls back to when no Material theme is installed.

Returns:

Type Description
'ConnectedButtonGroupStyle'

The filled connected-group style at size "s".

Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def default(cls) -> "ConnectedButtonGroupStyle":
    """Return the framework default, ignoring any theme.

    This is what a connected button group renders with before it is
    mounted, and what :meth:`from_theme` falls back to when no Material
    theme is installed.

    Returns:
        The filled connected-group style at size ``"s"``.
    """
    return cls.filled("s")

from_theme classmethod

from_theme(theme: 'Theme | None') -> 'ConnectedButtonGroupStyle'

Resolve the connected button group style from theme.

Parameters:

Name Type Description Default
theme 'Theme | None'

The active theme, or None when there is none.

required

Returns:

Type Description
'ConnectedButtonGroupStyle'

Resolved connected-group style.

Source code in src/nuiitivet/material/styles/button_group_style.py
@classmethod
def from_theme(cls, theme: "Theme | None") -> "ConnectedButtonGroupStyle":
    """Resolve the connected button group style from ``theme``.

    Args:
        theme: The active theme, or ``None`` when there is none.

    Returns:
        Resolved connected-group style.
    """
    from nuiitivet.material.theme.theme_data import MaterialThemeData

    if theme is not None:
        theme_data = theme.extension(MaterialThemeData)
        if theme_data is not None:
            return theme_data.connected_button_group_style
    return cls.default()

SplitButton

SplitButton(label: 'str | Any | None' = None, icon: 'Symbol | str | Any | None' = None, *, on_click: Optional[VoidCallback] = None, on_menu_toggle: Optional[BoolCallback] = None, menu_open: 'bool | MutableObservableBase[bool]' = False, disabled: 'bool | MutableObservableBase[bool]' = False, width: SizingLike = None, style: 'Optional[SplitButtonStyle]' = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: Box

Material Design 3 Expressive Split Button.

Combines a leading button (main action) with a trailing button (menu trigger). The two halves share an animated inner corner junction that morphs on hover and press. The trailing button's icon rotates 180° when the menu is opened.

Spec: https://m3.material.io/components/split-button/specs

Example::

SplitButton(
    "Start",
    icon="play_arrow",
    on_click=lambda: start_action(),
    on_menu_toggle=lambda open: handle_menu(open),
    style=SplitButtonStyle.default(),
)

Initialize SplitButton.

Parameters:

Name Type Description Default
label 'str | Any | None'

Text label for the leading button. Either label or icon (or both) must be provided.

None
icon 'Symbol | str | Any | None'

Leading icon for the leading button. Accepts a :class:Symbol, a symbol name string, or a :class:ObservableBase.

None
on_click Optional[VoidCallback]

Callback invoked when the leading button is clicked.

None
on_menu_toggle Optional[BoolCallback]

Callback invoked with the new bool menu open state when the trailing button is clicked.

None
menu_open 'bool | MutableObservableBase[bool]'

Initial menu open (selected) state of the trailing button. Pass an :class:MutableObservableBase to bind externally.

False
disabled 'bool | MutableObservableBase[bool]'

Disables both button halves when True.

False
width SizingLike

Width sizing. A fixed value or "wt" widens the button: the trailing segment keeps its icon width and the leading segment takes the rest, its content centred. None or "auto" fits both segments to their content.

None
style 'Optional[SplitButtonStyle]'

Visual style. Defaults to SplitButtonStyle.filled("s").

None
padding PaddingLike

Insets from the allocated rect to the button pair.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/split_button.py
def __init__(
    self,
    label: "str | Any | None" = None,
    icon: "Symbol | str | Any | None" = None,
    *,
    on_click: Optional[VoidCallback] = None,
    on_menu_toggle: Optional[BoolCallback] = None,
    menu_open: "bool | MutableObservableBase[bool]" = False,
    disabled: "bool | MutableObservableBase[bool]" = False,
    width: SizingLike = None,
    style: "Optional[SplitButtonStyle]" = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize SplitButton.

    Args:
        label: Text label for the leading button.  Either ``label`` or
            ``icon`` (or both) must be provided.
        icon: Leading icon for the leading button.  Accepts a
            :class:`Symbol`, a symbol name string, or a
            :class:`ObservableBase`.
        on_click: Callback invoked when the leading button is clicked.
        on_menu_toggle: Callback invoked with the new ``bool`` menu open
            state when the trailing button is clicked.
        menu_open: Initial menu open (selected) state of the trailing
            button.  Pass an :class:`MutableObservableBase` to bind
            externally.
        disabled: Disables both button halves when ``True``.
        width: Width sizing. A fixed value or ``"wt"`` widens the button:
            the trailing segment keeps its icon width and the leading
            segment takes the rest, its content centred. ``None`` or
            ``"auto"`` fits both segments to their content.
        style: Visual style.  Defaults to ``SplitButtonStyle.filled("s")``.
        padding: Insets from the allocated rect to the button pair.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    if label is None and icon is None:
        raise ValueError("SplitButton requires at least one of label or icon")

    from nuiitivet.material.styles.split_button_style import SplitButtonStyle as _Style

    resolved_style: "SplitButtonStyle" = style or _Style.filled("s")

    leading_child = self._build_leading_content(label, icon, resolved_style)

    # A given width belongs to the button, not to margins around it: the
    # row fills the box and the leading segment absorbs the leftover.
    fills = parse_sizing(width).kind != "auto"
    inner_width: SizingLike = "wt" if fills else None

    self._leading_btn = _SplitLeadingButton(
        child=leading_child,
        style=resolved_style,
        on_click=on_click,
        disabled=disabled,
        width=inner_width,
    )
    self._trailing_btn = _SplitTrailingButton(
        style=resolved_style,
        on_menu_toggle=on_menu_toggle,
        menu_open=menu_open,
        disabled=disabled,
    )

    from nuiitivet.layout.row import Row

    row = Row(
        [self._leading_btn, self._trailing_btn],
        gap=resolved_style.between_space,
        cross_alignment="center",
        width=inner_width,
    )

    super().__init__(child=row, width=width, padding=padding, key=key)

menu_open property

menu_open: bool

Whether the menu is currently open (trailing button selected).

Returns:

Type Description
bool

True when the menu is open.

editable_parameters classmethod

editable_parameters() -> dict[str, type]

style is picked from :class:SplitButtonStyle's presets.

Source code in src/nuiitivet/material/split_button.py
@classmethod
def editable_parameters(cls) -> dict[str, type]:
    """``style`` is picked from :class:`SplitButtonStyle`'s presets."""
    from nuiitivet.material.styles.split_button_style import SplitButtonStyle

    return {"style": SplitButtonStyle}

SplitButtonStyle dataclass

SplitButtonStyle(background: Optional[ColorSpec] = None, foreground: Optional[ColorSpec] = None, border_color: Optional[ColorSpec] = None, border_width: float = 0.0, elevation: int = 0, overlay_color: Optional[ColorSpec] = None, overlay_alpha: float = 0.12, container_height: int = 40, between_space: int = 2, outer_corner_radius: float = 20.0, inner_corner_radius: float = 4.0, inner_corner_hovered_radius: float = 12.0, inner_corner_pressed_radius: float = 12.0, leading_leading_space: int = 16, leading_trailing_space: int = 12, trailing_icon_size: int = 22, trailing_leading_space: int = 13, trailing_trailing_space: int = 13, menu_icon_offset: int = -1, label_font_size: int = 14, icon_size: int = 20)

Immutable style for :class:SplitButton (M3 Expressive-compliant).

Use the filled, elevated, tonal, or outlined factory classmethods rather than constructing directly where possible.

All size-related tokens are driven by :data:SPLIT_BUTTON_SIZE_TOKENS.

copy_with

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

Return a new style with the specified fields replaced.

Parameters:

Name Type Description Default
**changes Any

Fields to override.

{}

Returns:

Type Description
'SplitButtonStyle'

A new :class:SplitButtonStyle instance.

Source code in src/nuiitivet/material/styles/split_button_style.py
def copy_with(self, **changes: Any) -> "SplitButtonStyle":
    """Return a new style with the specified fields replaced.

    Args:
        **changes: Fields to override.

    Returns:
        A new :class:`SplitButtonStyle` instance.
    """
    return replace(self, **changes)

default classmethod

default() -> 'SplitButtonStyle'

Return the framework default, ignoring any theme.

This is what a split button renders with before it is mounted, and what a split button without a theme falls back to.

Returns:

Type Description
'SplitButtonStyle'

The filled split-button style at size "s".

Source code in src/nuiitivet/material/styles/split_button_style.py
@classmethod
def default(cls) -> "SplitButtonStyle":
    """Return the framework default, ignoring any theme.

    This is what a split button renders with before it is mounted, and
    what a split button without a theme falls back to.

    Returns:
        The filled split-button style at size ``"s"``.
    """
    return cls.filled("s")

filled classmethod

filled(size: ButtonSize = 's') -> 'SplitButtonStyle'

Create a filled-variant style.

Uses Primary as the container colour.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'

Returns:

Type Description
'SplitButtonStyle'

A new :class:SplitButtonStyle instance.

Source code in src/nuiitivet/material/styles/split_button_style.py
@classmethod
def filled(cls, size: ButtonSize = "s") -> "SplitButtonStyle":
    """Create a filled-variant style.

    Uses ``Primary`` as the container colour.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).

    Returns:
        A new :class:`SplitButtonStyle` instance.
    """
    t = SPLIT_BUTTON_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.PRIMARY,
        foreground=ColorRole.ON_PRIMARY,
        border_width=0.0,
        overlay_color=ColorRole.ON_PRIMARY,
        overlay_alpha=0.12,
        **t,
    )

elevated classmethod

elevated(size: ButtonSize = 's') -> 'SplitButtonStyle'

Create an elevated-variant style.

Uses Surface as the container colour with elevation level 1.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'

Returns:

Type Description
'SplitButtonStyle'

A new :class:SplitButtonStyle instance.

Source code in src/nuiitivet/material/styles/split_button_style.py
@classmethod
def elevated(cls, size: ButtonSize = "s") -> "SplitButtonStyle":
    """Create an elevated-variant style.

    Uses ``Surface`` as the container colour with elevation level 1.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).

    Returns:
        A new :class:`SplitButtonStyle` instance.
    """
    t = SPLIT_BUTTON_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.SURFACE_CONTAINER_LOW,
        foreground=ColorRole.PRIMARY,
        border_width=0.0,
        elevation=1,
        overlay_color=ColorRole.PRIMARY,
        overlay_alpha=0.08,
        **t,
    )

tonal classmethod

tonal(size: ButtonSize = 's') -> 'SplitButtonStyle'

Create a tonal-variant style.

Uses SecondaryContainer as the container colour.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'

Returns:

Type Description
'SplitButtonStyle'

A new :class:SplitButtonStyle instance.

Source code in src/nuiitivet/material/styles/split_button_style.py
@classmethod
def tonal(cls, size: ButtonSize = "s") -> "SplitButtonStyle":
    """Create a tonal-variant style.

    Uses ``SecondaryContainer`` as the container colour.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).

    Returns:
        A new :class:`SplitButtonStyle` instance.
    """
    t = SPLIT_BUTTON_SIZE_TOKENS[size]
    return cls(
        background=ColorRole.SECONDARY_CONTAINER,
        foreground=ColorRole.ON_SECONDARY_CONTAINER,
        border_width=0.0,
        overlay_color=ColorRole.ON_SECONDARY_CONTAINER,
        overlay_alpha=0.12,
        **t,
    )

outlined classmethod

outlined(size: ButtonSize = 's') -> 'SplitButtonStyle'

Create an outlined-variant style.

Uses a transparent background with an Outline-coloured border.

Parameters:

Name Type Description Default
size ButtonSize

M3 size token preset ("xs"–"xl").

's'

Returns:

Type Description
'SplitButtonStyle'

A new :class:SplitButtonStyle instance.

Source code in src/nuiitivet/material/styles/split_button_style.py
@classmethod
def outlined(cls, size: ButtonSize = "s") -> "SplitButtonStyle":
    """Create an outlined-variant style.

    Uses a transparent background with an ``Outline``-coloured border.

    Args:
        size: M3 size token preset (``"xs"``–``"xl"``).

    Returns:
        A new :class:`SplitButtonStyle` instance.
    """
    t = SPLIT_BUTTON_SIZE_TOKENS[size]
    return cls(
        background=None,
        foreground=ColorRole.ON_SURFACE,
        border_color=ColorRole.OUTLINE,
        border_width=1.0,
        overlay_color=ColorRole.PRIMARY,
        overlay_alpha=0.08,
        **t,
    )

MaterialTransitionSpec dataclass

MaterialTransitionSpec(enter: TransitionDefinition, exit_: TransitionDefinition, barrier_mode: Literal['none', 'fade'] = 'none', enter_back: TransitionDefinition | None = None, exit_back: TransitionDefinition | None = None)

Material transition token for overlay/page lifecycle.

Carries enter / exit_ TransitionDefinitions plus a barrier_mode that controls scrim opacity behavior:

  • "none" : no scrim (page, snackbar)
  • "fade" : scrim fades in/out following progress (dialog, sheets)

enter_back / exit_back are the backward-direction (pop) variants. Directional transitions such as Shared Axis (Z) reverse their motion when navigating back, so a pop must not merely replay the forward enter / exit_. When either is None the resolver falls back to the forward definition, which keeps symmetric transitions (dialog, sheets, snackbar) unchanged.

DatePicker

DatePicker(value: ObservableProtocol[Optional[date]], *, on_change: Optional[Callable[[Optional[date]], None]] = None, on_confirm: Optional[Callable[[Optional[date]], None]] = None, on_cancel: Optional[Callable[[], None]] = None, min_date: Optional[date] = None, max_date: Optional[date] = None, labels: CalendarLabels = DEFAULT_CALENDAR_LABELS, style: Optional['DatePickerStyle'] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget

Material Design 3 inline calendar date picker.

An inline calendar widget that updates a shared observable value when the user selects a date. The picker always stays visible (not a dialog), which makes it composable with other widgets — :class:DockedDatePicker embeds one as its dropdown content.

MD3 container: 360×456dp, Large corner rounding (16dp).

Selecting a day updates value immediately; the MD3 action row confirms or abandons that selection. Standalone there is nothing to confirm to, so OK is inert and Cancel clears the selection. An embedder that owns a dismissal — :class:DockedDatePicker closing its dropdown — passes on_confirm and on_cancel to take over both buttons.

Initialize DatePicker.

Parameters:

Name Type Description Default
value ObservableProtocol[Optional[date]]

Observable holding the selected date (or None); the picker reads it and writes each selection into it.

required
on_change Optional[Callable[[Optional[date]], None]]

Callback invoked after the user selects a date.

None
on_confirm Optional[Callable[[Optional[date]], None]]

Callback invoked with the selected date when OK is pressed. When omitted, OK does nothing.

None
on_cancel Optional[Callable[[], None]]

Callback invoked when Cancel is pressed. When omitted, Cancel clears value.

None
min_date Optional[date]

Earliest selectable date.

None
max_date Optional[date]

Latest selectable date.

None
labels CalendarLabels

Month names, weekday headers and first day of week. The default is English and Sunday-first on every platform; it never reads the process locale.

DEFAULT_CALENDAR_LABELS
style Optional['DatePickerStyle']

Visual style. Defaults to :class:DatePickerStyle.

None
padding PaddingLike

Insets from the allocated rect to the picker.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/date_picker.py
def __init__(
    self,
    value: ObservableProtocol[Optional[_Date]],
    *,
    on_change: Optional[Callable[[Optional[_Date]], None]] = None,
    on_confirm: Optional[Callable[[Optional[_Date]], None]] = None,
    on_cancel: Optional[Callable[[], None]] = None,
    min_date: Optional[_Date] = None,
    max_date: Optional[_Date] = None,
    labels: CalendarLabels = DEFAULT_CALENDAR_LABELS,
    style: Optional["DatePickerStyle"] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize DatePicker.

    Args:
        value: Observable holding the selected date (or None); the
            picker reads it and writes each selection into it.
        on_change: Callback invoked after the user selects a date.
        on_confirm: Callback invoked with the selected date when OK is
            pressed. When omitted, OK does nothing.
        on_cancel: Callback invoked when Cancel is pressed. When omitted,
            Cancel clears *value*.
        min_date: Earliest selectable date.
        max_date: Latest selectable date.
        labels: Month names, weekday headers and first day of week. The
            default is English and Sunday-first on every platform; it
            never reads the process locale.
        style: Visual style. Defaults to :class:`DatePickerStyle`.
        padding: Insets from the allocated rect to the picker.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)
    self._value_obs = value
    self._on_change = on_change
    self._on_confirm = on_confirm
    self._on_cancel_cb = on_cancel
    self._min_date = min_date
    self._max_date = max_date
    self._labels = labels
    self._user_style = style

    # Initialise view to the currently selected month, or the current month.
    today = _Date.today()
    initial = getattr(value, "value", None)
    ref = initial if isinstance(initial, _Date) else today
    self._view_year = ref.year
    self._view_month = ref.month

    # View mode: "calendar" | "month" | "year"
    self._view_mode: Literal["calendar", "month", "year"] = "calendar"
    self._year_page_start: int = today.year - 3

    # Dropdown-arrow rotation: 0° (pointing down / menu closed) → 180°
    # (pointing up / menu open). Held on the persistent picker so the
    # animation survives the rebuild() triggered when toggling views.
    self._month_rotation: Animatable[float] = Animatable(0.0, motion=EXPRESSIVE_DEFAULT_SPATIAL)
    self._year_rotation: Animatable[float] = Animatable(0.0, motion=EXPRESSIVE_DEFAULT_SPATIAL)

    # Value last reflected in the built subtree. Used to suppress the
    # redundant rebuild that ``observe`` would otherwise trigger by applying
    # the current value immediately on (re)mount.
    self._synced_value: Any = initial

style property

style: 'DatePickerStyle'

Return the resolved date picker style.

on_mount

on_mount() -> None

Subscribe to external value changes to keep the display in sync.

Source code in src/nuiitivet/material/date_picker.py
def on_mount(self) -> None:
    """Subscribe to external value changes to keep the display in sync."""
    super().on_mount()
    self._synced_value = getattr(self._value_obs, "value", None)
    self.observe(self._value_obs, self._on_value_changed)

show_month

show_month(year: int, month: int) -> None

Scroll the calendar to year/month without changing the value.

Returns the picker to the calendar view if a month or year list is open.

Parameters:

Name Type Description Default
year int

Calendar year to display.

required
month int

Calendar month to display (1–12).

required
Source code in src/nuiitivet/material/date_picker.py
def show_month(self, year: int, month: int) -> None:
    """Scroll the calendar to ``year``/``month`` without changing the value.

    Returns the picker to the calendar view if a month or year list is open.

    Args:
        year: Calendar year to display.
        month: Calendar month to display (1–12).
    """
    changed = (
        self._view_year != year or self._view_month != month or self._view_mode != "calendar"
    )
    self._view_year = year
    self._view_month = month
    self._view_mode = "calendar"
    self._sync_rotation()
    if changed:
        self.rebuild()

build

build() -> Widget

Build the inline calendar container with navigation header and calendar.

Source code in src/nuiitivet/material/date_picker.py
def build(self) -> Widget:
    """Build the inline calendar container with navigation header and calendar."""
    style = self.style
    selected = getattr(self._value_obs, "value", None)
    shadows = elevation_shadows(style.elevation)

    nav_header = _MonthYearHeader(
        self._view_year,
        self._view_month,
        on_prev=self._go_prev_month,
        on_next=self._go_next_month,
        on_prev_year=self._go_prev_year,
        on_next_year=self._go_next_year,
        on_month_tap=self._toggle_month_view,
        on_year_tap=self._toggle_year_view,
        active_view=self._view_mode if self._view_mode in ("month", "year") else None,  # type: ignore[arg-type]
        month_rotation=self._month_rotation,
        year_rotation=self._year_rotation,
        variant="inline",
        labels=self._labels,
        style=style,
    )

    # Vertical layout per MD3 docked measurement (padding order = left, top,
    # right, bottom). Sections stack with no inter-section gap; the gaps come
    # from each section's own padding (component-box to component-box), and
    # sum to exactly the 460dp container:
    #   nav_header   : pad (4, 20, 4, 15)  -> 75dp  (20 + menu-button 40 + 15)
    #   weekday row  : pad (12, 15, 12, 8) -> 37dp  (15 + text 14 + 8)
    #   calendar grid: pad (12, 8, 12, 4)  -> 292dp (8 + 6*40+5*8 + 4)
    #   action row   : pad (12, 4, 12, 12) -> 56dp  (4 + button 40 + 12)
    # Resulting box gaps: top 20, below-header 30, below-weekday 16,
    # below-grid 8, below-button 12.
    # The docked nav header block is 75dp tall (20dp top + 40dp menu-button
    # + 15dp bottom). In a list view there is no action row (it is hidden,
    # matching MD3), so the list fills the remaining container height.
    _HEADER_BLOCK_H = 75
    list_height = int(style.container_height) - _HEADER_BLOCK_H

    if self._view_mode == "month":
        # Full container width so the scrollbar sits flush against the right
        # container edge with no dead margin beside it.
        body: Widget = _MonthList(
            self._view_month,
            on_select=self._select_month,
            list_height=list_height,
            item_width=style.container_width,
            labels=self._labels,
            style=style,
        )
    elif self._view_mode == "year":
        body = _YearList(
            self._view_year,
            on_select=self._select_year,
            list_height=list_height,
            item_width=style.container_width,
            style=style,
        )
    else:
        body = _CalendarGrid(
            self._view_year,
            self._view_month,
            selected_date=selected if isinstance(selected, _Date) else None,
            min_date=self._min_date,
            max_date=self._max_date,
            on_day_tap=self._on_day_tap,
            labels=self._labels,
            style=style,
        )

    # MD3 docked action buttons are 40dp tall; ButtonStyle.text() enforces a
    # 48dp min touch-target by default, so override min_height to keep the
    # visible button box at 40dp (12dp gap below it = MD3 action bottom).
    # The action row is only shown in the calendar view; the month/year list
    # menus replace the calendar area and hide the Cancel/OK buttons per MD3.
    column_children: list[Widget] = [nav_header, body]
    if self._view_mode == "calendar":
        action_btn_style = ButtonStyle.text().copy_with(container_height=40, min_height=40)
        action_row = Row(
            [
                Button("Cancel", on_click=self._on_cancel, style=action_btn_style),
                Button("OK", on_click=self._on_ok, style=action_btn_style),
            ],
            gap=16,
            main_alignment="end",
            padding=(12, 4, 12, 12),
            width=int(style.container_width),
        )
        column_children.append(action_row)

    return Box(
        background_color=style.background,
        corner_radius=style.corner_radius,
        shadows=shadows,
        width=style.container_width,
        height=style.container_height,
        child=Column(
            column_children,
            gap=0,
            height=int(style.container_height),
        ),
    )

DockedDatePicker

DockedDatePicker(*, value: ReadOnlyObservableProtocol[str], on_change: Optional[Callable[[str], None]] = None, on_submit: Optional[Callable[[str], None]] = None, on_focus_change: Optional[FocusChangeCallback] = None, date_format: DateFormat = DEFAULT_DATE_FORMAT, min_date: Optional[date] = None, max_date: Optional[date] = None, labels: CalendarLabels = DEFAULT_CALENDAR_LABELS, label: str = 'Date', supporting_text: str | ReadOnlyObservableProtocol[str | None] | None = None, is_error: bool | ReadOnlyObservableProtocol[bool] = False, style: Optional['DockedDatePickerStyle'] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget

Material Design 3 Docked Date Picker.

A text field with a trailing calendar icon button that opens a :class:DatePicker in a dropdown anchored below the field. The date can be entered either by typing it or by picking it from the calendar.

value is the field's text, not a date. The typed date is derived from it by the application::

self.date_text = nv.Observable("")
self.date = self.date_text.filter(nv.is_date, initial="").map(nv.parse_date)

nv.DockedDatePicker(value=self.date_text, label="Arrival")

The widget reports no errors of its own: half-typed text such as "06/1" is a normal state, and only the application knows when it is an error. Derive supporting_text and is_error from the same text.

Per MD3 the dropdown carries a Cancel/OK action row, so picking a day is a selection rather than a commit. The calendar edits an internal draft; only OK writes it into value. Cancel -- and any other dismissal, such as tapping outside the dropdown -- drops the draft, so an abandoned selection is never observable from value.

A read-only observable makes the field display-only, as it does for :class:TextField; the calendar's OK then has nowhere to write and does nothing.

MD3 reference: md.comp.date-picker.docked.*

Initialize DockedDatePicker.

Parameters:

Name Type Description Default
value ReadOnlyObservableProtocol[str]

Observable holding the field's text. Typing writes into it, and the calendar's OK writes the picked date into it, formatted by date_format.

required
on_change Optional[Callable[[str], None]]

Callback invoked with the text as it changes, by typing or by the calendar alike. The observable bound to value carries the same signal.

None
on_submit Optional[Callable[[str], None]]

Callback invoked with the text when the user presses Enter -- a request to act, not a value settling.

None
on_focus_change Optional[FocusChangeCallback]

Callback invoked as focus arrives and leaves. Blur-triggered work belongs here, such as reformatting a half-typed date once the user has left the field.

None
date_format DateFormat

How text is read as a date and how a picked date is written back. Reading only decides which month the calendar opens on and which day it highlights, never validates -- unparseable text leaves the calendar where it was. Pass the one the application derives its date with, and they agree.

DEFAULT_DATE_FORMAT
min_date Optional[date]

Earliest date selectable in the calendar.

None
max_date Optional[date]

Latest date selectable in the calendar. Typing can still produce a date outside the bounds; enforcing a range on typed text is the application's, via is_error.

None
labels CalendarLabels

Month names, weekday headers and first day of week of the dropdown calendar. The default is English and Sunday-first on every platform; it never reads the process locale.

DEFAULT_CALENDAR_LABELS
label str

Floating label for the text field.

'Date'
supporting_text str | ReadOnlyObservableProtocol[str | None] | None

Text shown below the field; the slot for the application's error message. For a format hint, pass str(date_format).

None
is_error bool | ReadOnlyObservableProtocol[bool]

Whether to show the field in its error state. It recolors the whole field and is independent of supporting_text.

False
style Optional['DockedDatePickerStyle']

Visual style. Defaults to :class:DockedDatePickerStyle.

None
padding PaddingLike

Insets from the allocated rect to the field.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/date_picker.py
def __init__(
    self,
    *,
    value: ReadOnlyObservableProtocol[str],
    on_change: Optional[Callable[[str], None]] = None,
    on_submit: Optional[Callable[[str], None]] = None,
    on_focus_change: Optional[FocusChangeCallback] = None,
    date_format: DateFormat = DEFAULT_DATE_FORMAT,
    min_date: Optional[_Date] = None,
    max_date: Optional[_Date] = None,
    labels: CalendarLabels = DEFAULT_CALENDAR_LABELS,
    label: str = "Date",
    supporting_text: str | ReadOnlyObservableProtocol[str | None] | None = None,
    is_error: bool | ReadOnlyObservableProtocol[bool] = False,
    style: Optional["DockedDatePickerStyle"] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize DockedDatePicker.

    Args:
        value: Observable holding the field's text. Typing writes into
            it, and the calendar's OK writes the picked date into it,
            formatted by *date_format*.
        on_change: Callback invoked with the text as it changes, by typing
            or by the calendar alike. The observable bound to *value*
            carries the same signal.
        on_submit: Callback invoked with the text when the user presses
            Enter -- a request to act, not a value settling.
        on_focus_change: Callback invoked as focus arrives and leaves.
            Blur-triggered work belongs here, such as reformatting a
            half-typed date once the user has left the field.
        date_format: How text is read as a date and how a picked date is
            written back. Reading only decides which month the calendar
            opens on and which day it highlights, never validates --
            unparseable text leaves the calendar where it was. Pass the
            one the application derives its date with, and they agree.
        min_date: Earliest date selectable in the calendar.
        max_date: Latest date selectable in the calendar. Typing can
            still produce a date outside the bounds; enforcing a range
            on typed text is the application's, via *is_error*.
        labels: Month names, weekday headers and first day of week of the
            dropdown calendar. The default is English and Sunday-first
            on every platform; it never reads the process locale.
        label: Floating label for the text field.
        supporting_text: Text shown below the field; the slot for the
            application's error message. For a format hint, pass
            ``str(date_format)``.
        is_error: Whether to show the field in its error state. It
            recolors the whole field and is independent of
            *supporting_text*.
        style: Visual style. Defaults to :class:`DockedDatePickerStyle`.
        padding: Insets from the allocated rect to the field.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)
    self._value_obs = value
    # The one writable cell this widget touches, and only when the calendar
    # commits. A read-only source displays only -- the same rule TextField
    # applies to typing, extended to the calendar.
    self._writable: Optional[ObservableProtocol[str]] = (
        cast("ObservableProtocol[str]", value) if isinstance(value, ObservableProtocol) else None
    )
    self._date_format = date_format
    self._user_style = style
    self._is_open: Observable[bool] = Observable(False)

    # The dropdown calendar writes here, not to ``value``. Opening seeds it
    # from the text; OK copies it back. Cancelling just drops it, so an
    # abandoned selection never reaches ``value``.
    self._draft_obs: Observable[Optional[_Date]] = Observable(date_format.parse(self._text()))

    style_ = self.style

    # Both the field and the calendar are built once and reused across
    # rebuild cycles: the field to preserve focus and cursor position, the
    # calendar to preserve the month being viewed while the dropdown reopens.
    from nuiitivet.material.text_fields import TextField
    from nuiitivet.material.styles.text_field_style import TextFieldStyle

    self._text_field: TextField = TextField(
        value,
        on_change=on_change,
        on_submit=on_submit,
        on_focus_change=on_focus_change,
        label=label,
        supporting_text=supporting_text,
        is_error=is_error,
        trailing_icon="calendar_today",
        on_tap_trailing_icon=self._toggle_dropdown,
        style=TextFieldStyle.outlined(),
        width=style_.field_width,
    )
    self._calendar = DatePicker(
        self._draft_obs,
        on_confirm=self._on_calendar_confirm,
        on_cancel=self._on_calendar_cancel,
        min_date=min_date,
        max_date=max_date,
        labels=labels,
        style=style_.calendar,
    )

style property

style: 'DockedDatePickerStyle'

Return the resolved docked date picker style.

on_mount

on_mount() -> None

Track the text so the calendar follows it, and the dropdown state.

Both observers only ever write to the internal draft, so neither can echo back into value -- there is nothing here to guard against.

Source code in src/nuiitivet/material/date_picker.py
def on_mount(self) -> None:
    """Track the text so the calendar follows it, and the dropdown state.

    Both observers only ever write to the internal draft, so neither can
    echo back into ``value`` -- there is nothing here to guard against.
    """
    super().on_mount()
    self.observe(self._value_obs, self._place_calendar)
    self.observe(self._is_open, self._on_open_changed)

build

build() -> Widget

Build the text field with its anchored calendar dropdown.

Source code in src/nuiitivet/material/date_picker.py
def build(self) -> Widget:
    """Build the text field with its anchored calendar dropdown."""
    return self._text_field.modifier(
        popup(
            self._calendar,
            is_open=self._is_open,
            target_anchor="bottom-left",
            content_anchor="top-left",
            offset=(0.0, self.style.dropdown_gap),
            # Stay below the field even when the window is too short for the
            # calendar: it overflows rather than opening upwards. Opening
            # above is the other reasonable answer -- turn ``flip`` back on
            # if that is wanted.
            flip=False,
        )
    )

ModalDatePicker

ModalDatePicker(*, init_value: Optional[date] = None, supporting_text: str = 'Select date', min_date: Optional[date] = None, max_date: Optional[date] = None, style: Optional['ModalDatePickerStyle'] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget, OverlayAware[Optional[date]]

Material Design 3 Modal Date Picker (single date selection).

When shown via overlay.dialog(ModalDatePicker(...)), the returned :class:OverlayHandle resolves to the selected date on confirmation or None on cancellation::

result = await overlay.dialog(ModalDatePicker())
if result.value is not None:
    selected_date: datetime.date = result.value

MD3 container: 360×524dp, Extra large corner rounding (28dp).

.. note:: Experimental implementation. This class does not yet fully comply with the MD3 Expressive specification. Known limitation: the icon button that toggles between :class:ModalDatePicker and :class:ModalDateInput is not implemented. Deferred: Nuiitivet prioritizes the Docked variant as a desktop-oriented framework.

Initialize ModalDatePicker.

Parameters:

Name Type Description Default
init_value Optional[date]

Date selected when the picker opens.

None
supporting_text str

Small label shown at the top of the header (14pt).

'Select date'
min_date Optional[date]

Earliest selectable date.

None
max_date Optional[date]

Latest selectable date.

None
style Optional['ModalDatePickerStyle']

Visual style. Defaults to :class:ModalDatePickerStyle.

None
padding PaddingLike

Insets from the allocated rect to the picker.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/date_picker.py
def __init__(
    self,
    *,
    init_value: Optional[_Date] = None,
    supporting_text: str = "Select date",
    min_date: Optional[_Date] = None,
    max_date: Optional[_Date] = None,
    style: Optional["ModalDatePickerStyle"] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize ModalDatePicker.

    Args:
        init_value: Date selected when the picker opens.
        supporting_text: Small label shown at the top of the header (14pt).
        min_date: Earliest selectable date.
        max_date: Latest selectable date.
        style: Visual style. Defaults to :class:`ModalDatePickerStyle`.
        padding: Insets from the allocated rect to the picker.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)
    self._supporting_text = supporting_text
    self._min_date = min_date
    self._max_date = max_date
    self._user_style = style

    today = _Date.today()
    self._selected_date: Optional[_Date] = init_value

    if init_value is not None:
        self._view_year = init_value.year
        self._view_month = init_value.month
    else:
        self._view_year = today.year
        self._view_month = today.month

    self._showing_year_picker: bool = False

style property

style: 'ModalDatePickerStyle'

Return the resolved date picker style.

build

build() -> Widget

Build the modal picker with header, calendar (or year grid), and action buttons.

Source code in src/nuiitivet/material/date_picker.py
def build(self) -> Widget:
    """Build the modal picker with header, calendar (or year grid), and action buttons."""
    style = self.style
    shadows = elevation_shadows(style.elevation)

    nav_header = _MonthYearHeader(
        self._view_year,
        self._view_month,
        on_prev=self._go_prev_month,
        on_next=self._go_next_month,
        on_toggle_year_picker=self._toggle_year_picker,
        year_picker_active=self._showing_year_picker,
        variant="modal",
        # Same nav padding in both views so the dialog height is unchanged
        # when toggling the year picker (paired with growing the year grid
        # by the hidden action-row height). The 28dp left inset aligns the
        # "Month Year" label's left edge with the first weekday letter (the
        # weekday letters are centred in 48dp columns starting at 12dp, so
        # the first letter sits ~17dp in; 12 + 17 ≈ 29). The measurement does
        # not give a number, so this is a best-fit alignment.
        nav_padding=(28, 6, 12, 2),
        style=style,
    )

    # The dialog sizes to its content (Flex) so the 6-week calendar grid gets
    # its full height with the MD3 section paddings intact (a fixed token
    # height was sized for 5 weeks and clipped the action row).
    column_children: list[Widget] = [
        self._build_header(style, year_view=self._showing_year_picker),
        HorizontalDivider(),
        nav_header,
    ]

    if self._showing_year_picker:
        # Actions are hidden during year selection (no date is confirmable
        # in this view). The year grid is grown by the action-row height so
        # the dialog height stays constant when toggling the year picker.
        column_children.append(
            _YearChipGrid(
                self._view_year,
                on_select=self._select_year,
                list_height=_modal_calendar_body_height(style) + _MODAL_ACTION_ROW_HEIGHT,
                style=style,
            )
        )
    else:
        column_children.append(
            _CalendarGrid(
                self._view_year,
                self._view_month,
                selected_date=self._selected_date,
                min_date=self._min_date,
                max_date=self._max_date,
                on_day_tap=self._on_day_tap,
                style=style,
            )
        )
        column_children.append(
            Row(
                [
                    Button("Cancel", on_click=self._on_cancel, style=ButtonStyle.text()),
                    Button("OK", on_click=self._on_confirm, style=ButtonStyle.text()),
                ],
                gap=8,
                main_alignment="end",
                padding=(12, 4, 12, 12),
                width=int(style.container_width),
            )
        )

    return Box(
        background_color=style.background,
        corner_radius=style.corner_radius,
        shadows=shadows,
        width=style.container_width,
        child=Column(column_children, gap=0),
    )

ModalDateRangePicker

ModalDateRangePicker(*, init_value: Optional[Tuple[date, date]] = None, supporting_text: str = 'Select range', min_date: Optional[date] = None, max_date: Optional[date] = None, style: Optional['ModalDateRangePickerStyle'] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget, OverlayAware[Optional[Tuple[date, date]]]

Material Design 3 Modal Date Range Picker.

Allows the user to select a start and end date via two sequential taps. When shown via overlay.dialog(ModalDateRangePicker(...)), the returned :class:OverlayHandle resolves to (start, end) on confirmation or None on cancellation::

result = await overlay.dialog(ModalDateRangePicker())
if result.value is not None:
    start, end = result.value

MD3 container: 360×524dp, Extra large corner rounding (28dp).

Range selection flow
  • First tap sets the start date.
  • Second tap sets the end date (must be ≥ start; tapping before the start resets and begins a new selection from that date).

.. note:: Experimental implementation. This class does not yet fully comply with the MD3 Expressive specification. Known limitations: the icon button that toggles between :class:ModalDateRangePicker and a range-input variant (ModalDateRangeInput) is not implemented, and ModalDateRangeInput does not yet exist. Deferred: Nuiitivet prioritizes the Docked variant as a desktop-oriented framework.

Initialize ModalDateRangePicker.

Parameters:

Name Type Description Default
init_value Optional[Tuple[date, date]]

Range selected when the picker opens, as (start, end).

None
supporting_text str

Small label shown at the top of the header (14pt).

'Select range'
min_date Optional[date]

Earliest selectable date.

None
max_date Optional[date]

Latest selectable date.

None
style Optional['ModalDateRangePickerStyle']

Visual style. Defaults to :class:ModalDateRangePickerStyle.

None
padding PaddingLike

Insets from the allocated rect to the picker.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/date_picker.py
def __init__(
    self,
    *,
    init_value: Optional[Tuple[_Date, _Date]] = None,
    supporting_text: str = "Select range",
    min_date: Optional[_Date] = None,
    max_date: Optional[_Date] = None,
    style: Optional["ModalDateRangePickerStyle"] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize ModalDateRangePicker.

    Args:
        init_value: Range selected when the picker opens, as
            ``(start, end)``.
        supporting_text: Small label shown at the top of the header (14pt).
        min_date: Earliest selectable date.
        max_date: Latest selectable date.
        style: Visual style. Defaults to :class:`ModalDateRangePickerStyle`.
        padding: Insets from the allocated rect to the picker.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)
    self._supporting_text = supporting_text
    self._min_date = min_date
    self._max_date = max_date
    self._user_style = style

    today = _Date.today()
    self._range_start: Optional[_Date] = None
    self._range_end: Optional[_Date] = None
    # "first": waiting for start; "second": waiting for end.
    self._range_state: Literal["first", "second"] = "first"

    if init_value is not None and len(init_value) == 2:
        self._range_start, self._range_end = init_value
        self._view_year = self._range_start.year
        self._view_month = self._range_start.month
    else:
        self._view_year = today.year
        self._view_month = today.month

    self._showing_year_picker: bool = False

style property

style: 'ModalDateRangePickerStyle'

Return the resolved date picker style.

build

build() -> Widget

Build the modal range picker with header, calendar (or year grid), and action buttons.

Source code in src/nuiitivet/material/date_picker.py
def build(self) -> Widget:
    """Build the modal range picker with header, calendar (or year grid), and action buttons."""
    style = self.style
    shadows = elevation_shadows(style.elevation)

    ok_disabled = self._range_start is None or self._range_end is None

    nav_header = _MonthYearHeader(
        self._view_year,
        self._view_month,
        on_prev=self._go_prev_month,
        on_next=self._go_next_month,
        on_toggle_year_picker=self._toggle_year_picker,
        year_picker_active=self._showing_year_picker,
        variant="modal",
        # Same nav padding in both views so the dialog height is unchanged
        # when toggling the year picker (paired with growing the year grid
        # by the hidden action-row height). The 28dp left inset aligns the
        # "Month Year" label's left edge with the first weekday letter (the
        # weekday letters are centred in 48dp columns starting at 12dp, so
        # the first letter sits ~17dp in; 12 + 17 ≈ 29). The measurement does
        # not give a number, so this is a best-fit alignment.
        nav_padding=(28, 6, 12, 2),
        style=style,
    )

    # Content-sized (Flex) so the 6-week grid keeps its full height and MD3
    # section paddings (the fixed token height was sized for 5 weeks).
    column_children: list[Widget] = [
        self._build_header(style, year_view=self._showing_year_picker),
        HorizontalDivider(),
        nav_header,
    ]

    if self._showing_year_picker:
        # Actions hidden during year selection; grow the grid by the action
        # row height so the dialog height is unchanged when toggling.
        column_children.append(
            _YearChipGrid(
                self._view_year,
                on_select=self._select_year,
                list_height=_modal_calendar_body_height(style) + _MODAL_ACTION_ROW_HEIGHT,
                style=style,
            )
        )
    else:
        column_children.append(
            _CalendarGrid(
                self._view_year,
                self._view_month,
                range_start=self._range_start,
                range_end=self._range_end,
                min_date=self._min_date,
                max_date=self._max_date,
                on_day_tap=self._on_day_tap,
                style=style,
            )
        )
        column_children.append(
            Row(
                [
                    Button("Cancel", on_click=self._on_cancel, style=ButtonStyle.text()),
                    Button("OK", on_click=self._on_confirm, style=ButtonStyle.text(), disabled=ok_disabled),
                ],
                gap=8,
                main_alignment="end",
                padding=(12, 4, 12, 12),
                width=int(style.container_width),
            )
        )

    return Box(
        background_color=style.background,
        corner_radius=style.corner_radius,
        shadows=shadows,
        width=style.container_width,
        child=Column(column_children, gap=0),
    )

ModalDateInput

ModalDateInput(*, init_value: Optional[date] = None, supporting_text: str = 'Enter date', input_label: str = 'Date', date_format: DateFormat = DEFAULT_DATE_FORMAT, min_date: Optional[date] = None, max_date: Optional[date] = None, style: Optional['ModalDateInputStyle'] = None, padding: PaddingLike = 0, key: Optional[str] = None)

Bases: ComposableWidget, OverlayAware[Optional[date]]

Material Design 3 Modal Date Input.

Allows the user to type a date directly into a text field. When shown via overlay.dialog(ModalDateInput(...)), the returned :class:OverlayHandle resolves to the entered date on confirmation or None on cancellation::

result = await overlay.dialog(ModalDateInput())
if result.value is not None:
    entered: datetime.date = result.value

MD3 container: 328×512dp, Extra large corner rounding (28dp).

.. note:: Experimental implementation. This class does not yet fully comply with the MD3 Expressive specification. Known limitations: the icon button that toggles between :class:ModalDateInput and :class:ModalDatePicker is not implemented, and the range-input variant (ModalDateRangeInput) does not yet exist. Deferred: Nuiitivet prioritizes the Docked variant as a desktop-oriented framework.

Initialize ModalDateInput.

Parameters:

Name Type Description Default
init_value Optional[date]

Date the text field opens with.

None
supporting_text str

Small label shown at the top of the header (14pt).

'Enter date'
input_label str

Text field label.

'Date'
date_format DateFormat

How the typed date is read and rendered. Its pattern is also the hint shown below the field.

DEFAULT_DATE_FORMAT
min_date Optional[date]

Earliest acceptable date.

None
max_date Optional[date]

Latest acceptable date.

None
style Optional['ModalDateInputStyle']

Visual style. Defaults to :class:ModalDateInputStyle.

None
padding PaddingLike

Insets from the allocated rect to the picker.

0
key Optional[str]

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

None
Source code in src/nuiitivet/material/date_picker.py
def __init__(
    self,
    *,
    init_value: Optional[_Date] = None,
    supporting_text: str = "Enter date",
    input_label: str = "Date",
    date_format: DateFormat = DEFAULT_DATE_FORMAT,
    min_date: Optional[_Date] = None,
    max_date: Optional[_Date] = None,
    style: Optional["ModalDateInputStyle"] = None,
    padding: PaddingLike = 0,
    key: Optional[str] = None,
) -> None:
    """Initialize ModalDateInput.

    Args:
        init_value: Date the text field opens with.
        supporting_text: Small label shown at the top of the header (14pt).
        input_label: Text field label.
        date_format: How the typed date is read and rendered. Its pattern
            is also the hint shown below the field.
        min_date: Earliest acceptable date.
        max_date: Latest acceptable date.
        style: Visual style. Defaults to :class:`ModalDateInputStyle`.
        padding: Insets from the allocated rect to the picker.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(padding=padding, key=key)
    self._init_value = init_value
    self._supporting_text = supporting_text
    self._input_label = input_label
    self._date_format = date_format
    self._min_date = min_date
    self._max_date = max_date
    self._user_style = style

    # Internal observables for the text field
    self._text_obs: Observable[str] = Observable(date_format.format(init_value))
    self._supporting_text_obs: Observable[Optional[str]] = Observable(date_format.pattern)

    # Build the TextField once and reuse across rebuild cycles to preserve
    # focus state and cursor position.
    from nuiitivet.material.text_fields import TextField
    from nuiitivet.material.styles.text_field_style import TextFieldStyle

    self._text_field: TextField = TextField(
        self._text_obs,
        label=input_label,
        supporting_text=self._supporting_text_obs,
        style=TextFieldStyle.outlined(),
        width=self._resolved_field_width,
    )

style property

style: 'ModalDateInputStyle'

Return the resolved date picker style.

on_mount

on_mount() -> None

Subscribe to text changes to keep the header date display in sync.

Source code in src/nuiitivet/material/date_picker.py
def on_mount(self) -> None:
    """Subscribe to text changes to keep the header date display in sync."""
    super().on_mount()
    self.observe(self._text_obs, lambda _: self.rebuild())

build

build() -> Widget

Build the modal date input with header, text field, and action buttons.

Source code in src/nuiitivet/material/date_picker.py
def build(self) -> Widget:
    """Build the modal date input with header, text field, and action buttons."""
    style = self.style
    shadows = elevation_shadows(style.elevation)

    parsed_date = self._date_format.parse(self._text_obs.value)
    date_str = parsed_date.strftime("%b %d, %Y") if parsed_date is not None else "—"
    # Header layout (measurement image):
    # Supporting text: padding=(16, 24, 18, 24)
    # Headline row: headline + calendar_today icon (24dp, right-aligned)
    #   padding=(18, 24, 5, 24)
    # The header sizes to its content (supporting text + headline row); the
    # measurement reference does not box it to a fixed height, and forcing
    # ``header_height`` (120dp) here would leave ~11dp of dead slack that
    # shows up as an oversized gap above the divider.
    header = Box(
        width=style.container_width,
        child=Column(
            [
                Box(
                    padding=(24, 16, 24, 18),
                    child=Text(
                        self._supporting_text,
                        style=TextStyle(
                            color=style.header_supporting_text_color,
                        ),
                        type_scale=TypeScaleToken.from_size(int(style.header_supporting_text_font_size)),
                    ),
                ),
                Row(
                    [
                        # Pin the headline to its MD3 line-height box (32pt /
                        # 40dp). Centring the icon against this fixed line box
                        # (rather than the text's ink bounds, which grow with
                        # descenders like the "y" in "May") keeps the icon on
                        # the cap-height centre regardless of the month name.
                        Box(
                            height=40,
                            alignment="center-left",
                            child=Text(
                                date_str,
                                style=TextStyle(
                                    color=style.header_headline_color,
                                ),
                                type_scale=TypeScaleToken.from_size(int(style.header_headline_font_size)),
                            ),
                        ),
                        Icon("calendar_today", size=24),
                    ],
                    gap=8,
                    cross_alignment="center",
                    main_alignment="space-between",
                    padding=(24, 18, 24, 0),
                    width=int(style.container_width),
                ),
            ],
            gap=0,
        ),
    )

    action_row = Row(
        [
            Button("Cancel", on_click=self._on_cancel, style=ButtonStyle.text()),
            Button("OK", on_click=self._on_confirm, style=ButtonStyle.text()),
        ],
        gap=16,
        main_alignment="end",
        padding=(24, 4, 24, 12),
        width=int(style.container_width),
    )

    # Unlike the calendar/range modals, the date-input dialog has no fixed
    # body to fill: it sizes to its content (header + text field + actions).
    # The MD3 ``512dp`` container token would leave a large empty band, so
    # the height is intentionally left to wrap the content (the measurement
    # reference likewise sets no height on the outer box).
    return Box(
        background_color=style.background,
        corner_radius=style.corner_radius,
        shadows=shadows,
        width=style.container_width,
        child=Column(
            [
                header,
                # The divider owns its 10dp margins (top to the headline line
                # box, bottom toward the field). The outlined field's floating
                # label floats ~7dp above its outline, so an extra 7dp top
                # inset on the field keeps the *label text top* 10dp below the
                # divider line (not just the outline).
                HorizontalDivider(padding=(0, 10, 0, 10)),
                Box(
                    padding=(24, 6, 24, 4),
                    child=self._text_field,
                ),
                action_row,
            ],
            gap=0,
            width=int(style.container_width),
        ),
    )

DateFormat

DateFormat(pattern: str = 'mm/dd/yyyy', *, also_accepts: Sequence[str] = ())

How a date is written as text, and read back.

Initialize DateFormat.

Parameters:

Name Type Description Default
pattern str

The format dates are rendered in, and the first one accepted when parsing. Also what :meth:__str__ returns, so it can be shown to the user as a hint.

'mm/dd/yyyy'
also_accepts Sequence[str]

Further patterns accepted when parsing, tried in order after pattern. Typing is worth being lenient about -- someone will enter 2026-06-10 into a mm/dd/yyyy field -- while output stays in one format.

()

Raises:

Type Description
ValueError

If any pattern is malformed. See :func:_compile.

Source code in src/nuiitivet/material/date_format.py
def __init__(self, pattern: str = "mm/dd/yyyy", *, also_accepts: Sequence[str] = ()) -> None:
    """Initialize DateFormat.

    Args:
        pattern: The format dates are rendered in, and the first one accepted
            when parsing. Also what :meth:`__str__` returns, so it can be shown
            to the user as a hint.
        also_accepts: Further patterns accepted when parsing, tried in order
            after *pattern*. Typing is worth being lenient about -- someone will
            enter ``2026-06-10`` into a ``mm/dd/yyyy`` field -- while output
            stays in one format.

    Raises:
        ValueError: If any pattern is malformed.  See :func:`_compile`.
    """
    self._pattern = pattern
    self._also_accepts: Tuple[str, ...] = tuple(also_accepts)
    self._formats: Tuple[str, ...] = tuple(_compile(p) for p in (pattern, *self._also_accepts))

pattern property

pattern: str

The output pattern, and the first one tried when parsing.

also_accepts property

also_accepts: Tuple[str, ...]

Further patterns accepted when parsing.

parse

parse(text: str) -> Optional[date]

Read text as a date.

A bound method, so it can be handed straight to an operator::

self.arrival = self.arrival_text.map(fmt.parse)

Parameters:

Name Type Description Default
text str

Raw user input. Surrounding whitespace is ignored.

required

Returns:

Type Description
Optional[date]

The date, or None when text matches no accepted pattern --

Optional[date]

which includes empty and half-typed text.

Source code in src/nuiitivet/material/date_format.py
def parse(self, text: str) -> Optional[_Date]:
    """Read ``text`` as a date.

    A bound method, so it can be handed straight to an operator::

        self.arrival = self.arrival_text.map(fmt.parse)

    Args:
        text: Raw user input.  Surrounding whitespace is ignored.

    Returns:
        The date, or ``None`` when ``text`` matches no accepted pattern --
        which includes empty and half-typed text.
    """
    stripped = text.strip()
    for fmt in self._formats:
        try:
            return _DateTime.strptime(stripped, fmt).date()
        except ValueError:
            continue
    return None

format

format(value: Optional[date]) -> str

Render value in :attr:pattern, or "" when unset.

Parameters:

Name Type Description Default
value Optional[date]

The date to render, or None.

required

Returns:

Type Description
str

Text that :meth:parse reads back as value.

Source code in src/nuiitivet/material/date_format.py
def format(self, value: Optional[_Date]) -> str:
    """Render ``value`` in :attr:`pattern`, or ``""`` when unset.

    Args:
        value: The date to render, or ``None``.

    Returns:
        Text that :meth:`parse` reads back as ``value``.
    """
    if value is None:
        return ""
    return value.strftime(self._formats[0])

matches

matches(text: str) -> bool

Whether :meth:parse can read text.

The predicate form, for the filter() step of a derived date::

self.arrival = self.arrival_text.filter(fmt.matches, initial="").map(fmt.parse)

Parameters:

Name Type Description Default
text str

Raw user input.

required

Returns:

Type Description
bool

True when text parses, False otherwise.

Source code in src/nuiitivet/material/date_format.py
def matches(self, text: str) -> bool:
    """Whether :meth:`parse` can read ``text``.

    The predicate form, for the ``filter()`` step of a derived date::

        self.arrival = self.arrival_text.filter(fmt.matches, initial="").map(fmt.parse)

    Args:
        text: Raw user input.

    Returns:
        ``True`` when ``text`` parses, ``False`` otherwise.
    """
    return self.parse(text) is not None

CalendarLabels dataclass

CalendarLabels(month_names: Tuple[str, ...] = ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'), weekday_labels: Tuple[str, ...] = ('M', 'T', 'W', 'T', 'F', 'S', 'S'), first_day_of_week: int = SUNDAY)

Names and week convention a calendar renders dates with.

Attributes:

Name Type Description
month_names Tuple[str, ...]

Twelve month names, January first, indexed by month - 1.

weekday_labels Tuple[str, ...]

Seven weekday column headers, Monday first -- indexed by :meth:datetime.date.weekday regardless of first_day_of_week.

first_day_of_week int

The weekday the grid's first column shows, numbered as :meth:datetime.date.weekday does (calendar.MONDAY is 0, calendar.SUNDAY is 6). Defaults to Sunday, matching the MD3 spec; a calendar that starts on another day is a deliberate deviation from MD3, chosen by the application.

Raises:

Type Description
ValueError

If month_names does not hold 12 entries, weekday_labels does not hold 7, or first_day_of_week is outside 0-6.

weekday_columns

weekday_columns() -> Tuple[str, ...]

The weekday headers in grid column order.

Returns:

Type Description
Tuple[str, ...]

The seven labels starting at :attr:first_day_of_week.

Source code in src/nuiitivet/material/calendar_labels.py
def weekday_columns(self) -> Tuple[str, ...]:
    """The weekday headers in grid column order.

    Returns:
        The seven labels starting at :attr:`first_day_of_week`.
    """
    first = self.first_day_of_week
    return tuple(self.weekday_labels[(first + i) % 7] for i in range(7))

DatePickerStyle dataclass

DatePickerStyle(background: ColorSpec = SURFACE_CONTAINER_HIGH, elevation: int = 3, corner_radius: float = 16.0, container_width: float = 360.0, container_height: float = 460.0, date_cell_size: int = 40, date_cell_radius: float = 20.0, state_layer_size: int = 40, date_font_size: int = 16, date_selected_background: ColorSpec = PRIMARY, date_selected_text: ColorSpec = ON_PRIMARY, date_today_outline_color: ColorSpec = PRIMARY, date_today_text: ColorSpec = PRIMARY, date_unselected_text: ColorSpec = ON_SURFACE, date_outside_month_opacity: float = 0.38, weekday_text: ColorSpec = ON_SURFACE, range_active_indicator_background: ColorSpec = SECONDARY_CONTAINER, range_date_in_range_text: ColorSpec = ON_SECONDARY_CONTAINER, header_height: float = 64.0, header_headline_color: ColorSpec = ON_SURFACE_VARIANT, header_supporting_text_color: ColorSpec = ON_SURFACE_VARIANT, hover_state_layer_opacity: float = 0.08, focus_state_layer_opacity: float = 0.1, pressed_state_layer_opacity: float = 0.1, header_supporting_text_font_size: float = 14.0, header_headline_font_size: float = 32.0, menu_button_height: float = 40.0, menu_button_font_size: int = 14, menu_button_icon_size: int = 18, menu_button_text: ColorSpec = ON_SURFACE_VARIANT, menu_list_item_height: float = 48.0, menu_list_item_selected_background: ColorSpec = SECONDARY_CONTAINER, menu_list_item_text: ColorSpec = ON_SURFACE, menu_list_item_selected_text: ColorSpec = ON_SECONDARY_CONTAINER)

Bases: CalendarStyle

Style for :class:DatePicker (inline calendar).

MD3 calendar: 360×460dp container, Large corner rounding (16dp). Adds the month/year inline list-menu tokens to the shared calendar base.

DockedDatePickerStyle dataclass

DockedDatePickerStyle(calendar: DatePickerStyle = DatePickerStyle(), field_width: float = 360.0, dropdown_gap: float = 4.0)

Style for :class:DockedDatePicker (text field + anchored calendar).

Composes — rather than inherits — a :class:DatePickerStyle for the dropdown calendar, keeping the calendar tokens separate from the text-field and dropdown tokens.

copy_with

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

Return a new style with the given fields overridden.

Parameters:

Name Type Description Default
**changes Any

Fields to override.

{}

Returns:

Type Description
'DockedDatePickerStyle'

New DockedDatePickerStyle instance with applied changes.

Source code in src/nuiitivet/material/styles/date_picker_style.py
def copy_with(self, **changes: Any) -> "DockedDatePickerStyle":
    """Return a new style with the given fields overridden.

    Args:
        **changes: Fields to override.

    Returns:
        New ``DockedDatePickerStyle`` instance with applied changes.
    """
    return replace(self, **changes)

ModalDatePickerStyle dataclass

ModalDatePickerStyle(background: ColorSpec = SURFACE_CONTAINER_HIGH, elevation: int = 3, corner_radius: float = 28.0, container_width: float = 360.0, container_height: float = 524.0, date_cell_size: int = 40, date_cell_radius: float = 20.0, state_layer_size: int = 40, date_font_size: int = 16, date_selected_background: ColorSpec = PRIMARY, date_selected_text: ColorSpec = ON_PRIMARY, date_today_outline_color: ColorSpec = PRIMARY, date_today_text: ColorSpec = PRIMARY, date_unselected_text: ColorSpec = ON_SURFACE, date_outside_month_opacity: float = 0.38, weekday_text: ColorSpec = ON_SURFACE, range_active_indicator_background: ColorSpec = SECONDARY_CONTAINER, range_date_in_range_text: ColorSpec = ON_SECONDARY_CONTAINER, header_height: float = 120.0, header_headline_color: ColorSpec = ON_SURFACE_VARIANT, header_supporting_text_color: ColorSpec = ON_SURFACE_VARIANT, hover_state_layer_opacity: float = 0.08, focus_state_layer_opacity: float = 0.1, pressed_state_layer_opacity: float = 0.1, header_supporting_text_font_size: float = 14.0, header_headline_font_size: float = 32.0, menu_button_height: float = 40.0, menu_button_font_size: int = 14, menu_button_icon_size: int = 18, menu_button_text: ColorSpec = ON_SURFACE_VARIANT, year_chip_width: float = 72.0, year_chip_height: float = 36.0, year_chip_radius: float = 18.0, year_chip_gap: int = 30, year_chip_selected_background: ColorSpec = PRIMARY, year_chip_selected_text: ColorSpec = ON_PRIMARY, year_chip_unselected_text: ColorSpec = ON_SURFACE_VARIANT)

Bases: CalendarStyle

Style for :class:ModalDatePicker (single-date dialog).

MD3 modal picker: 360×524dp container, Extra-large corner rounding (28dp). Adds the year-chip selection tokens to the shared calendar base.

ModalDateRangePickerStyle dataclass

ModalDateRangePickerStyle(background: ColorSpec = SURFACE_CONTAINER_HIGH, elevation: int = 3, corner_radius: float = 28.0, container_width: float = 360.0, container_height: float = 524.0, date_cell_size: int = 40, date_cell_radius: float = 20.0, state_layer_size: int = 40, date_font_size: int = 16, date_selected_background: ColorSpec = PRIMARY, date_selected_text: ColorSpec = ON_PRIMARY, date_today_outline_color: ColorSpec = PRIMARY, date_today_text: ColorSpec = PRIMARY, date_unselected_text: ColorSpec = ON_SURFACE, date_outside_month_opacity: float = 0.38, weekday_text: ColorSpec = ON_SURFACE, range_active_indicator_background: ColorSpec = SECONDARY_CONTAINER, range_date_in_range_text: ColorSpec = ON_SECONDARY_CONTAINER, header_height: float = 120.0, header_headline_color: ColorSpec = ON_SURFACE_VARIANT, header_supporting_text_color: ColorSpec = ON_SURFACE_VARIANT, hover_state_layer_opacity: float = 0.08, focus_state_layer_opacity: float = 0.1, pressed_state_layer_opacity: float = 0.1, header_supporting_text_font_size: float = 14.0, header_headline_font_size: float = 32.0, menu_button_height: float = 40.0, menu_button_font_size: int = 14, menu_button_icon_size: int = 18, menu_button_text: ColorSpec = ON_SURFACE_VARIANT, year_chip_width: float = 72.0, year_chip_height: float = 36.0, year_chip_radius: float = 18.0, year_chip_gap: int = 30, year_chip_selected_background: ColorSpec = PRIMARY, year_chip_selected_text: ColorSpec = ON_PRIMARY, year_chip_unselected_text: ColorSpec = ON_SURFACE_VARIANT, range_header_height: float = 128.0, range_headline_font_size: float = 22.0)

Bases: ModalDatePickerStyle

Style for :class:ModalDateRangePicker (date-range dialog).

Extends :class:ModalDatePickerStyle (same calendar, year chips and container) with the taller range-selection header tokens.

ModalDateInputStyle dataclass

ModalDateInputStyle(background: ColorSpec = SURFACE_CONTAINER_HIGH, elevation: int = 3, corner_radius: float = 28.0, container_width: float = 328.0, container_height: float = 512.0, header_headline_color: ColorSpec = ON_SURFACE_VARIANT, header_supporting_text_color: ColorSpec = ON_SURFACE_VARIANT, header_supporting_text_font_size: float = 14.0, header_headline_font_size: float = 32.0)

Style for :class:ModalDateInput (text-field date entry dialog).

Independent of the calendar pickers: the date-input dialog is a text-field form, so it shares none of the calendar/selection tokens — only the dialog container and header typography.

MD3 modal input: 328×512dp container, Extra-large corner rounding (28dp).

copy_with

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

Return a new style with the given fields overridden.

Parameters:

Name Type Description Default
**changes Any

Fields to override.

{}

Returns:

Type Description
'ModalDateInputStyle'

New ModalDateInputStyle instance with applied changes.

Source code in src/nuiitivet/material/styles/date_picker_style.py
def copy_with(self, **changes: Any) -> "ModalDateInputStyle":
    """Return a new style with the given fields overridden.

    Args:
        **changes: Fields to override.

    Returns:
        New ``ModalDateInputStyle`` instance with applied changes.
    """
    return replace(self, **changes)

Image

Image(source: bytes | None | ObservableBase[bytes | None], *, fit: Fit = 'contain', width: SizingLike = None, height: SizingLike = None, padding: int | tuple[int, int] | tuple[int, int, int, int] = 0, alignment: AlignmentLike = 'center', key: str | None = None)

Bases: Widget

Display a raster image from in-memory bytes.

Initialize an Image widget.

Parameters:

Name Type Description Default
source bytes | None | ObservableBase[bytes | None]

Encoded image bytes, None, or an Observable that provides them.

required
fit Fit

Content fit mode. One of "contain", "cover", "fill", "none".

'contain'
width SizingLike

Width sizing.

None
height SizingLike

Height sizing.

None
padding int | tuple[int, int] | tuple[int, int, int, int]

Insets from the allocated rect to the image.

0
alignment AlignmentLike

Content alignment in the allocated content rect.

'center'
key str | None

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

None
Source code in src/nuiitivet/widgets/image.py
def __init__(
    self,
    source: bytes | None | ObservableBase[bytes | None],
    *,
    fit: Fit = "contain",
    width: SizingLike = None,
    height: SizingLike = None,
    padding: int | tuple[int, int] | tuple[int, int, int, int] = 0,
    alignment: AlignmentLike = "center",
    key: str | None = None,
) -> None:
    """Initialize an Image widget.

    Args:
        source: Encoded image bytes, ``None``, or an Observable that provides them.
        fit: Content fit mode. One of ``"contain"``, ``"cover"``, ``"fill"``, ``"none"``.
        width: Width sizing.
        height: Height sizing.
        padding: Insets from the allocated rect to the image.
        alignment: Content alignment in the allocated content rect.
        key: Stable widget identity for dev-bridge targeting and hot reload.
    """
    super().__init__(width=width, height=height, padding=padding, key=key)
    self._fit: Fit = self._normalize_fit(fit)
    self._align_raw: AlignmentLike = alignment
    self._alignment: tuple[str, str] = normalize_alignment(alignment, default=("center", "center"))

    self._source: bytes | None | ObservableBase[bytes | None] = source
    self._resolved_source: bytes | None = None
    self._decoded_image: Any | None = None
    self._decoded_token: tuple[int, int] | None = None

    if isinstance(source, ObservableBase):
        self.observe(source, self._on_source_change)
    else:
        self._on_source_change(source)

preferred_size

preferred_size(max_width: int | None = None, max_height: int | None = None) -> tuple[int, int]

Return preferred size based on intrinsic image size and explicit sizing.

Source code in src/nuiitivet/widgets/image.py
def preferred_size(self, max_width: int | None = None, max_height: int | None = None) -> tuple[int, int]:
    """Return preferred size based on intrinsic image size and explicit sizing."""
    w_dim = self.width_sizing
    h_dim = self.height_sizing

    if w_dim.kind == "fixed" and h_dim.kind == "fixed":
        l, t, r, b = self.padding
        return (int(w_dim.value) + l + r, int(h_dim.value) + t + b)

    image = self._decoded_image
    intrinsic_w, intrinsic_h = self._image_size(image)

    width = int(w_dim.value) if w_dim.kind == "fixed" else intrinsic_w
    height = int(h_dim.value) if h_dim.kind == "fixed" else intrinsic_h

    l, t, r, b = self.padding
    total_w = int(width) + int(l) + int(r)
    total_h = int(height) + int(t) + int(b)

    if max_width is not None:
        total_w = min(total_w, int(max_width))
    if max_height is not None:
        total_h = min(total_h, int(max_height))

    return (max(0, total_w), max(0, total_h))

paint

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

Paint the image into the given rect according to fit and alignment.

Source code in src/nuiitivet/widgets/image.py
@replay_safe
def paint(self, canvas, x: int, y: int, width: int, height: int) -> None:
    """Paint the image into the given rect according to fit and alignment."""
    self.set_last_rect(x, y, width, height)

    if canvas is None:
        return

    image = self._decoded_image
    if image is None:
        return

    img_w, img_h = self._image_size(image)
    if img_w <= 0 or img_h <= 0:
        return

    cx, cy, cw, ch = self.content_rect(x, y, width, height)
    if cw <= 0 or ch <= 0:
        return

    fit = self._fit
    align_x, align_y = self._alignment
    fx = self._align_factor(align_x)
    fy = self._align_factor(align_y)

    if fit == "fill":
        self._draw_image_rect(
            canvas,
            image,
            (0.0, 0.0, float(img_w), float(img_h)),
            (float(cx), float(cy), float(cw), float(ch)),
        )
        return

    if fit == "cover":
        src = self._compute_cover_source(img_w, img_h, cw, ch, fx, fy)
        self._draw_image_rect(canvas, image, src, (float(cx), float(cy), float(cw), float(ch)))
        return

    if fit == "contain":
        scale = min(float(cw) / float(img_w), float(ch) / float(img_h))
        draw_w = max(0.0, float(img_w) * scale)
        draw_h = max(0.0, float(img_h) * scale)
    else:  # "none"
        draw_w = float(img_w)
        draw_h = float(img_h)

    if fit == "none":
        # Allow negative offsets so alignment works even when the image is
        # larger than the container (image is centered/aligned and clipped).
        dx = float(cx) + (float(cw) - draw_w) * fx
        dy = float(cy) + (float(ch) - draw_h) * fy
    else:
        dx = float(cx) + max(0.0, float(cw) - draw_w) * fx
        dy = float(cy) + max(0.0, float(ch) - draw_h) * fy

    if fit == "none" and (draw_w > float(cw) or draw_h > float(ch)):
        clip_r = make_rect(cx, cy, cw, ch)
        save_fn = getattr(canvas, "save", None)
        restore_fn = getattr(canvas, "restore", None)
        if callable(save_fn) and callable(restore_fn) and clip_r is not None:
            save_fn()
            clip_rect(canvas, clip_r)
            self._draw_image_rect(canvas, image, (0.0, 0.0, float(img_w), float(img_h)), (dx, dy, draw_w, draw_h))
            restore_fn()
            return

    self._draw_image_rect(canvas, image, (0.0, 0.0, float(img_w), float(img_h)), (dx, dy, draw_w, draw_h))

elevation_shadows

elevation_shadows(level: int) -> Shadows

Return the shadows for the given MD3 elevation level.

The result feeds the shadows() modifier directly::

widget.modifier(nv.shadows(nv.elevation_shadows(2)))

Parameters:

Name Type Description Default
level int

MD3 elevation level, clamped to the range 0-5.

required

Returns:

Type Description
Shadows

A tuple of Shadow layers ordered back to front (ambient, then

Shadows

key). Level 0 returns an empty tuple, which draws no shadow.

Source code in src/nuiitivet/material/theme/elevation.py
def elevation_shadows(level: int) -> Shadows:
    """Return the shadows for the given MD3 elevation level.

    The result feeds the ``shadows()`` modifier directly::

        widget.modifier(nv.shadows(nv.elevation_shadows(2)))

    Args:
        level: MD3 elevation level, clamped to the range 0-5.

    Returns:
        A tuple of ``Shadow`` layers ordered back to front (ambient, then
        key). Level 0 returns an empty tuple, which draws no shadow.
    """
    return _MD3_SHADOWS[max(0, min(5, level))]

parse_date

parse_date(text: str) -> Optional[date]

Read text as a date using :data:DEFAULT_DATE_FORMAT.

Parameters:

Name Type Description Default
text str

Raw user input.

required

Returns:

Type Description
Optional[date]

The date, or None when the text matches no accepted pattern.

Source code in src/nuiitivet/material/date_format.py
def parse_date(text: str) -> Optional[_Date]:
    """Read ``text`` as a date using :data:`DEFAULT_DATE_FORMAT`.

    Args:
        text: Raw user input.

    Returns:
        The date, or ``None`` when the text matches no accepted pattern.
    """
    return DEFAULT_DATE_FORMAT.parse(text)

format_date

format_date(value: Optional[date]) -> str

Render value as mm/dd/yyyy, or "" when unset.

Parameters:

Name Type Description Default
value Optional[date]

The date to render, or None.

required

Returns:

Type Description
str

Text that :func:parse_date reads back as value.

Source code in src/nuiitivet/material/date_format.py
def format_date(value: Optional[_Date]) -> str:
    """Render ``value`` as ``mm/dd/yyyy``, or ``""`` when unset.

    Args:
        value: The date to render, or ``None``.

    Returns:
        Text that :func:`parse_date` reads back as ``value``.
    """
    return DEFAULT_DATE_FORMAT.format(value)

is_date

is_date(text: str) -> bool

Whether :func:parse_date can read text as a date.

Written for the filter() step of a derived date, where the application wants the last valid date held while the user types an incomplete one::

self.date_text = nv.Observable("")
self.date = self.date_text.filter(nv.is_date, initial="").map(nv.parse_date)

The seed is filtered text, not a date, so it goes through map as well: initial="" reads back as None until the first valid date arrives.

Parameters:

Name Type Description Default
text str

Raw user input.

required

Returns:

Type Description
bool

True when text parses, False otherwise.

Source code in src/nuiitivet/material/date_format.py
def is_date(text: str) -> bool:
    """Whether :func:`parse_date` can read ``text`` as a date.

    Written for the ``filter()`` step of a derived date, where the application
    wants the last valid date held while the user types an incomplete one::

        self.date_text = nv.Observable("")
        self.date = self.date_text.filter(nv.is_date, initial="").map(nv.parse_date)

    The seed is filtered text, not a date, so it goes through ``map`` as well:
    ``initial=""`` reads back as ``None`` until the first valid date arrives.

    Args:
        text: Raw user input.

    Returns:
        ``True`` when ``text`` parses, ``False`` otherwise.
    """
    return DEFAULT_DATE_FORMAT.matches(text)