Skip to content

xnano.utils.focus

xnano.utils.focus

xnano.utils.focus


Discover, move, and synchronize focus across grid fields.

Functions:

is_component

is_component(value: Any) -> bool

Return whether a value follows the component contract.

Source code in xnano/types.py
def is_component(value: Any) -> bool:
    """Return whether a value follows the component contract."""
    return bool(getattr(type(value), "_xnano_component_base", False))

is_focusable_component

is_focusable_component(value: Any) -> bool

Return whether a component accepts field focus.

Source code in xnano/types.py
def is_focusable_component(value: Any) -> bool:
    """Return whether a component accepts field focus."""
    return is_component(value) and bool(getattr(value, "focusable", False))

uses_default_component_size

uses_default_component_size(value: Any) -> bool

Return whether a component uses the default layout size.

Source code in xnano/types.py
def uses_default_component_size(value: Any) -> bool:
    """Return whether a component uses the default layout size."""
    return is_component(value) and not bool(
        getattr(value, "fit_content", False)
    )

get_focusable_component

get_focusable_component(
    grid: Any, field_name: str
) -> Any | None

Return a focusable component stored in one grid field.

Source code in xnano/utils/focus.py
def get_focusable_component(grid: Any, field_name: str) -> Any | None:
    """Return a focusable component stored in one grid field."""
    value = getattr(grid, field_name, None)
    return value if is_focusable_component(value) else None

field_group

field_group(grid: Any, field_name: str) -> str | None

Return the configured group for a grid field.

Source code in xnano/utils/focus.py
def field_group(grid: Any, field_name: str) -> str | None:
    """Return the configured group for a grid field."""
    info = getattr(grid, "_grid_field_info", lambda name: None)(field_name)
    return None if info is None else info.group

collect_focusable_fields

collect_focusable_fields(terminal: Any) -> list[FieldFocus]

Collect focusable fields from the runtime's root grid.

Source code in xnano/utils/focus.py
def collect_focusable_fields(terminal: Any) -> list[FieldFocus]:
    """Collect focusable fields from the runtime's root grid."""
    root = getattr(terminal, "_root", None)
    targets: list[FieldFocus] = []
    for grid in _walk_grids(root):
        for name, info in grid._grid_fields.items():
            component = get_focusable_component(grid, name)
            if component is not None or info.autofocus or info.group:
                targets.append(
                    FieldFocus(
                        grid=grid,
                        field_name=name,
                        group=info.group,
                    )
                )
    return targets

collect_group_targets

collect_group_targets(
    terminal: Any,
) -> dict[str, FieldFocus]

Map focus group names to their first matching field.

Source code in xnano/utils/focus.py
def collect_group_targets(terminal: Any) -> dict[str, FieldFocus]:
    """Map focus group names to their first matching field."""
    return {
        target.group: target
        for target in collect_focusable_fields(terminal)
        if target.group is not None
    }

resolve_group_target

resolve_group_target(
    terminal: Any, group: str
) -> FieldFocus | None

Return the field targeted by a focus group.

Source code in xnano/utils/focus.py
def resolve_group_target(terminal: Any, group: str) -> FieldFocus | None:
    """Return the field targeted by a focus group."""
    return collect_group_targets(terminal).get(group)

set_field_focus

set_field_focus(
    terminal: Any,
    target: FieldFocus | None,
    *,
    fire_hooks: bool = True
) -> bool

Set the focused field and synchronize component flags.

Source code in xnano/utils/focus.py
def set_field_focus(
    terminal: Any,
    target: FieldFocus | None,
    *,
    fire_hooks: bool = True,
) -> bool:
    """Set the focused field and synchronize component flags."""
    current = getattr(terminal, "_field_focus", None)
    if current == target:
        return False
    if current is not None and fire_hooks:
        _dispatch_field_focus(terminal, current, "field_lost")
    setattr(terminal, "_field_focus", target)
    sync_input_focus_flags(terminal)
    if target is not None:
        setattr(terminal, "_focused_group", target.group)
        if fire_hooks:
            _dispatch_field_focus(terminal, target, "field_gained")
    else:
        setattr(terminal, "_focused_group", None)
    return True

clear_field_focus

clear_field_focus(
    terminal: Any, *, fire_hooks: bool = True
) -> bool

Clear the active field focus.

Source code in xnano/utils/focus.py
def clear_field_focus(terminal: Any, *, fire_hooks: bool = True) -> bool:
    """Clear the active field focus."""
    return set_field_focus(terminal, None, fire_hooks=fire_hooks)

focus_group

focus_group(terminal: Any, group: str) -> bool

Focus the field registered for a group.

Source code in xnano/utils/focus.py
def focus_group(terminal: Any, group: str) -> bool:
    """Focus the field registered for a group."""
    target = resolve_group_target(terminal, group)
    return False if target is None else set_field_focus(terminal, target)

focused_group_name

focused_group_name(terminal: Any) -> str | None

Return the focused field's group name.

Source code in xnano/utils/focus.py
def focused_group_name(terminal: Any) -> str | None:
    """Return the focused field's group name."""
    target = getattr(terminal, "_field_focus", None)
    return None if target is None else target.group

is_group_focused

is_group_focused(terminal: Any, group: str) -> bool

Return whether a focus group is active.

Source code in xnano/utils/focus.py
def is_group_focused(terminal: Any, group: str) -> bool:
    """Return whether a focus group is active."""
    return focused_group_name(terminal) == group

focused_component

focused_component(terminal: Any) -> Any | None

Return the component inside the focused field.

Source code in xnano/utils/focus.py
def focused_component(terminal: Any) -> Any | None:
    """Return the component inside the focused field."""
    target = getattr(terminal, "_field_focus", None)
    if target is None:
        return None
    return get_focusable_component(target.grid, target.field_name)

sync_input_focus_flags

sync_input_focus_flags(terminal: Any) -> None

Synchronize component and field-state focus flags.

Source code in xnano/utils/focus.py
def sync_input_focus_flags(terminal: Any) -> None:
    """Synchronize component and field-state focus flags."""
    active = getattr(terminal, "_field_focus", None)
    for target in collect_focusable_fields(terminal):
        focused = target == active
        component = getattr(target.grid, target.field_name, None)
        if is_component(component):
            setattr(component, "_input_focused", focused)
        state = target.grid.grid_get_field_state(target.field_name)
        if state is not None:
            state.focused = focused

cycle_field_focus

cycle_field_focus(terminal: Any, step: int = 1) -> bool

Move focus forward or backward through focusable fields.

Source code in xnano/utils/focus.py
def cycle_field_focus(terminal: Any, step: int = 1) -> bool:
    """Move focus forward or backward through focusable fields."""
    targets = collect_focusable_fields(terminal)
    if not targets:
        return False
    current = getattr(terminal, "_field_focus", None)
    index = targets.index(current) if current in targets else -1
    return set_field_focus(terminal, targets[(index + step) % len(targets)])

ensure_default_field_focus

ensure_default_field_focus(terminal: Any) -> None

Apply autofocus or select the first focusable field.

Source code in xnano/utils/focus.py
def ensure_default_field_focus(terminal: Any) -> None:
    """Apply autofocus or select the first focusable field."""
    if getattr(terminal, "_field_focus", None) is not None:
        return
    targets = collect_focusable_fields(terminal)
    target = next(
        (
            item
            for item in targets
            if getattr(
                item.grid._grid_field_info(item.field_name),
                "autofocus",
                False,
            )
        ),
        targets[0] if targets else None,
    )
    if target is not None:
        set_field_focus(terminal, target, fire_hooks=False)

spatial_focus_enabled

spatial_focus_enabled(terminal: Any) -> bool

Return whether any focusable field declares autofocus.

Arrow-key and click focus movement is opt-in: it activates only when the app uses Field(autofocus=True) somewhere, so apps that bind arrow keys themselves keep their behavior.

Source code in xnano/utils/focus.py
def spatial_focus_enabled(terminal: Any) -> bool:
    """Return whether any focusable field declares ``autofocus``.

    Arrow-key and click focus movement is opt-in: it activates only when the
    app uses ``Field(autofocus=True)`` somewhere, so apps that bind arrow keys
    themselves keep their behavior.
    """
    for target in collect_focusable_fields(terminal):
        info = target.grid._grid_field_info(target.field_name)
        if getattr(info, "autofocus", False):
            return True
    return False

move_field_focus

move_field_focus(
    terminal: Any,
    direction: Literal["up", "down", "left", "right"],
) -> bool

Move focus to the nearest focusable field in direction.

Uses the live painted geometry of each focusable field; falls back to tab-order cycling when geometry is unavailable.

Source code in xnano/utils/focus.py
def move_field_focus(
    terminal: Any,
    direction: Literal["up", "down", "left", "right"],
) -> bool:
    """Move focus to the nearest focusable field in ``direction``.

    Uses the live painted geometry of each focusable field; falls back to
    tab-order cycling when geometry is unavailable.
    """
    targets = collect_focusable_fields(terminal)
    if len(targets) < 2:
        return False
    forward = direction in ("down", "right")
    current = getattr(terminal, "_field_focus", None)
    origin = None if current is None else _target_area(current)
    if origin is None:
        return cycle_field_focus(terminal, 1 if forward else -1)
    origin_x = origin.x + origin.width / 2
    origin_y = origin.y + origin.height / 2
    best: FieldFocus | None = None
    best_score: float | None = None
    for target in targets:
        if target == current:
            continue
        area = _target_area(target)
        if area is None:
            continue
        delta_x = (area.x + area.width / 2) - origin_x
        delta_y = (area.y + area.height / 2) - origin_y
        if direction == "up" and delta_y >= 0:
            continue
        if direction == "down" and delta_y <= 0:
            continue
        if direction == "left" and delta_x >= 0:
            continue
        if direction == "right" and delta_x <= 0:
            continue
        if direction in ("up", "down"):
            score = abs(delta_y) + abs(delta_x) * 2
        else:
            score = abs(delta_x) + abs(delta_y) * 2
        if best_score is None or score < best_score:
            best_score = score
            best = target
    if best is None:
        return False
    return set_field_focus(terminal, best)

apply_text_keyboard

apply_text_keyboard(text: Any, keyboard: Any) -> bool

Send keyboard input to a focused text component.

Source code in xnano/utils/focus.py
def apply_text_keyboard(text: Any, keyboard: Any) -> bool:
    """Send keyboard input to a focused text component."""
    handler = getattr(text, "handle_keyboard", None)
    return bool(handler(keyboard)) if callable(handler) else False

place_cursor_for_focus

place_cursor_for_focus(terminal: Any) -> None

Show and place the terminal caret at the focused input, else hide it.

A focused text input reports an absolute cursor_position during paint; the caret follows it and is shown. With nothing editable focused the caret is hidden so a browsing UI shows no stray cursor.

Source code in xnano/utils/focus.py
def place_cursor_for_focus(terminal: Any) -> None:
    """Show and place the terminal caret at the focused input, else hide it.

    A focused text input reports an absolute ``cursor_position`` during
    paint; the caret follows it and is shown. With nothing editable focused
    the caret is hidden so a browsing UI shows no stray cursor.
    """
    component = focused_component(terminal)
    position = getattr(component, "cursor_position", None)
    if position is not None:
        terminal.cursor.position = position
        terminal.cursor.visible = True
    else:
        terminal.cursor.visible = False

scroll_handle_for_group

scroll_handle_for_group(
    terminal: Any, group: str
) -> ScrollHandle | None

Return mutable scroll state for a grouped scroll field.

Source code in xnano/utils/focus.py
def scroll_handle_for_group(
    terminal: Any,
    group: str,
) -> ScrollHandle | None:
    """Return mutable scroll state for a grouped scroll field."""
    target = resolve_group_target(terminal, group)
    if target is None:
        return None
    info = target.grid._grid_field_info(target.field_name)
    axis = "x" if info.scroll == "horizontal" else "y"
    return target.grid._grid_scroll_handle(target.field_name, axis)