"""Built-in element factories.
Each factory function (``Text``, ``Button``, ) is a fully-typed thin
wrapper that builds an [`Element`][pythonnative.Element] through the
shared :func:`_make_element` helper, so style resolution, ``ref``
attachment, ``None``-default dropping, and forced overrides (e.g.
``Column``'s fixed ``flex_direction``) live in exactly one place. The
factory signatures themselves are the canonical prop schemas: editors
and type checkers validate calls directly against them.
Example:
```python
import pythonnative as pn
pn.Column(
pn.Text("Hello", style=pn.style(font_size=18)),
pn.Button("Tap", on_press=lambda: print("tapped")),
style=pn.style(spacing=12, padding=16),
)
```
"""
import bisect
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
from .element import Element
from .hooks import (
Ref,
component,
use_imperative_handle,
use_keyboard_height,
use_ref,
use_safe_area_insets,
use_state,
)
from .style import (
AccessibilityState,
AutoCapitalize,
Color,
KeyboardType,
ReturnKeyType,
ScaleType,
StyleProp,
resolve_style,
validate_style_keys,
)
# ======================================================================
# Canonical element builder
# ======================================================================
def _make_element(
name: str,
*children: Element,
style: StyleProp = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
_defaults: Optional[Dict[str, Any]] = None,
_forced: Optional[Dict[str, Any]] = None,
**props: Any,
) -> Element:
"""Build an [`Element`][pythonnative.Element] of type ``name``.
This is the single helper every built-in factory routes through, so
the cross-cutting concerns that used to be duplicated per component
live in one place:
1. ``style`` is flattened via
[`resolve_style`][pythonnative.style.resolve_style] (list-of-dicts
and ``None`` both handled).
2. ``_defaults`` are filled in for keys not already present (used for
things like ``View``'s default ``flex_direction: "column"`` that
a user style may legitimately override).
3. ``**props`` are merged on top, with ``None`` values *dropped* so
optional kwargs don't pollute the prop dict.
4. ``ref`` is attached under the reserved ``"ref"`` key.
5. ``_forced`` overrides everything (used by ``Column`` / ``Row`` to
lock their flex direction regardless of user style).
Args:
name: Element type name (e.g. ``"Text"``).
*children: Child elements.
style: Style dict, list of dicts, or ``None``.
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``; the
reconciler populates ``ref.current`` with the underlying
native view.
key: Stable identity for keyed reconciliation.
_defaults: Internal: fill-only-if-missing prop defaults.
_forced: Internal: prop overrides applied last.
**props: Per-component props. ``None`` values are dropped.
Returns:
A fresh [`Element`][pythonnative.Element].
"""
out: Dict[str, Any] = dict(resolve_style(style))
if out:
validate_style_keys(out, owner=name)
if _defaults:
for k, v in _defaults.items():
out.setdefault(k, v)
for k, v in props.items():
if v is not None:
out[k] = v
if ref is not None:
out["ref"] = ref
if _forced:
out.update(_forced)
return Element(name, out, list(children), key=key)
# ======================================================================
# Leaf factories
# ======================================================================
# Style keys that can vary per span inside a rich-text run. Layout
# keys are excluded: spans are inline fragments of one paragraph and
# have no boxes of their own.
_SPAN_STYLE_KEYS = (
"color",
"background_color",
"font_size",
"font_family",
"font_weight",
"bold",
"italic",
"text_decoration",
"letter_spacing",
)
def _flatten_text_spans(
parts: Tuple[Any, ...],
inherited: Dict[str, Any],
) -> List[Dict[str, Any]]:
"""Flatten nested ``Text`` parts into a flat list of styled spans.
Strings become spans carrying only the inherited style overrides;
nested ``Text`` elements contribute their own span-style keys
(merged over what they inherit) and recurse into their parts.
"""
spans: List[Dict[str, Any]] = []
for part in parts:
if part is None:
continue
if isinstance(part, Element):
props = part.props or {}
child_style = dict(inherited)
for k in _SPAN_STYLE_KEYS:
if props.get(k) is not None:
child_style[k] = props[k]
nested = props.get("spans")
if nested:
for span in nested:
merged = dict(child_style)
for k in _SPAN_STYLE_KEYS:
if span.get(k) is not None:
merged[k] = span[k]
spans.append({"text": span.get("text", ""), **merged})
else:
spans.append({"text": str(props.get("text", "")), **child_style})
else:
spans.append({"text": str(part), **inherited})
return spans
def Text(
*parts: Any,
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessibility_hint: Optional[str] = None,
accessibility_role: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Display a string of text, optionally with styled nested spans.
Style properties: ``font_size``, ``color``, ``bold``,
``font_weight``, ``font_family``, ``italic``, ``text_align``,
``background_color``, ``max_lines``, ``letter_spacing``,
``line_height``, ``text_decoration`` (``"underline"`` /
``"line_through"``), ``border_radius``, ``border_width``,
``border_color``, ``shadow_*``, ``opacity``, ``transform``, plus
the common layout props.
**Rich text**: pass multiple parts, mixing plain strings and
nested ``Text`` elements, to render one paragraph with per-span
styling (a single ``TextView`` / ``UILabel`` natively, so line
wrapping flows across spans):
```python
pn.Text(
"Hello, ",
pn.Text("world", style=pn.style(bold=True, color="#0A84FF")),
"!",
style=pn.style(font_size=18),
)
```
Nested spans inherit the outer element's text styling and may
override ``color``, ``background_color``, ``font_size``,
``font_family``, ``font_weight``, ``bold``, ``italic``,
``text_decoration``, and ``letter_spacing``.
Args:
*parts: Text content: a single string, or any mix of strings
and nested ``Text`` elements for rich text.
style: Style dict (or list of dicts).
accessibility_label: Spoken description for screen readers.
accessibility_hint: Spoken extra detail (iOS only).
accessibility_role: Semantic role for assistive tech.
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Text"``.
"""
rich = any(isinstance(p, Element) for p in parts) or len(parts) > 1
if rich:
spans = _flatten_text_spans(parts, {})
text = "".join(s["text"] for s in spans)
else:
spans = None
text = str(parts[0]) if parts else ""
return _make_element(
"Text",
style=style,
ref=ref,
key=key,
text=text,
spans=spans,
accessibility_label=accessibility_label,
accessibility_hint=accessibility_hint,
accessibility_role=accessibility_role,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
)
def Button(
title: str = "",
*,
on_press: Optional[Callable[[], None]] = None,
enabled: bool = True,
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessibility_hint: Optional[str] = None,
accessibility_role: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Display a tappable button.
Style properties: ``color``, ``background_color``, ``font_size``,
``border_radius``, ``border_width``, ``border_color``, ``shadow_*``,
``opacity``, ``transform``, plus the common layout props.
Buttons get ``accessibility_role="button"`` by default.
Args:
title: Button label.
on_press: Callback invoked when the user taps the button.
enabled: When ``False``, the button is disabled and cannot be
tapped.
style: Style dict (or list of dicts).
accessibility_label: Spoken description for screen readers.
accessibility_hint: Spoken extra detail (iOS only).
accessibility_role: Override the default ``"button"`` role.
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Button"``.
"""
return _make_element(
"Button",
style=style,
ref=ref,
key=key,
title=title,
on_press=on_press,
enabled=enabled,
accessibility_label=accessibility_label,
accessibility_hint=accessibility_hint,
accessibility_role=accessibility_role,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
_defaults={"accessibility_role": "button"},
)
def TextInput(
*,
value: str = "",
placeholder: Optional[str] = None,
on_change: Optional[Callable[[str], None]] = None,
on_submit: Optional[Callable[[str], None]] = None,
secure: bool = False,
multiline: bool = False,
keyboard_type: Optional[KeyboardType] = None,
auto_capitalize: Optional[AutoCapitalize] = None,
auto_correct: Optional[bool] = None,
auto_focus: bool = False,
return_key_type: Optional[ReturnKeyType] = None,
max_length: Optional[int] = None,
placeholder_color: Optional[Color] = None,
editable: bool = True,
clear_button: bool = False,
on_focus: Optional[Callable[[], None]] = None,
on_blur: Optional[Callable[[], None]] = None,
selection_color: Optional[Color] = None,
text_content_type: Optional[str] = None,
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessibility_hint: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Display a text-entry field (single-line by default, or ``multiline``).
Style properties: ``font_size``, ``color``, ``background_color``,
``border_*``, plus the common layout props.
Args:
value: Current text content (controlled-input pattern).
placeholder: Hint shown when ``value`` is empty.
on_change: Callback invoked with the new string each keystroke.
on_submit: Callback invoked when the user submits (Return /
Done / etc.). Receives the final text.
secure: When ``True``, characters are masked (use for passwords).
multiline: When ``True``, allows multiple lines of input.
keyboard_type: One of ``"default"``, ``"email_address"``,
``"number_pad"``, ``"decimal_pad"``, ``"phone_pad"``, ``"url"``.
auto_capitalize: One of ``"none"``, ``"sentences"``, ``"words"``,
``"characters"``.
auto_correct: Enable/disable autocorrection.
auto_focus: Request focus on mount.
return_key_type: One of ``"default"``, ``"done"``, ``"go"``,
``"next"``, ``"send"``, ``"search"``.
max_length: Maximum number of characters allowed.
placeholder_color: Color used for the placeholder string.
editable: When ``False``, the field is read-only (still
selectable).
clear_button: When ``True``, shows a clear ("x") button while
editing (iOS ``clearButtonMode``; an inline button on
Android).
on_focus: Callback invoked when the field gains focus.
on_blur: Callback invoked when the field loses focus.
selection_color: Cursor / selection highlight color.
text_content_type: Semantic content hint for autofill (e.g.
``"username"``, ``"password"``, ``"one_time_code"``).
style: Style dict (or list of dicts).
accessibility_label: Spoken description for screen readers.
accessibility_hint: Spoken extra detail (iOS only).
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"TextInput"``.
"""
return _make_element(
"TextInput",
style=style,
ref=ref,
key=key,
value=value,
placeholder=placeholder,
on_change=on_change,
on_submit=on_submit,
secure=secure or None,
multiline=multiline or None,
keyboard_type=keyboard_type,
auto_capitalize=auto_capitalize,
auto_correct=auto_correct,
auto_focus=auto_focus or None,
return_key_type=return_key_type,
max_length=max_length,
placeholder_color=placeholder_color,
editable=False if editable is False else None,
clear_button=clear_button or None,
on_focus=on_focus,
on_blur=on_blur,
selection_color=selection_color,
text_content_type=text_content_type,
accessibility_label=accessibility_label,
accessibility_hint=accessibility_hint,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
)
def Image(
source: str = "",
*,
scale_type: Optional[ScaleType] = None,
tint_color: Optional[Color] = None,
placeholder_color: Optional[Color] = None,
on_load: Optional[Callable[[], None]] = None,
on_error: Optional[Callable[[str], None]] = None,
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessibility_role: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Display an image from a resource path or URL.
Style properties: ``background_color``, ``border_*``, ``opacity``,
``transform``, plus the common layout props.
Network images (``http://`` / ``https://``) go through the shared
image pipeline (`pythonnative.images`): downloads happen on a
background thread, bytes are cached in memory and on disk keyed by
URL, concurrent requests for the same URL share one download, and
large bitmaps are downsampled to the view size when decoded.
Args:
source: Image resource name or URL.
scale_type: Fit mode: ``"cover"``, ``"contain"``, ``"stretch"``,
``"center"``.
tint_color: Color overlay applied to template images
(monochrome icons).
placeholder_color: Background color shown while a remote image
is loading (and left in place if it fails).
on_load: Callback invoked once the image has been decoded and
displayed.
on_error: Callback invoked with an error message when a remote
image fails to download or decode.
style: Style dict (or list of dicts).
accessibility_label: Spoken description for screen readers.
accessibility_role: Override the default ``"image"`` role.
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Image"``.
"""
return _make_element(
"Image",
style=style,
ref=ref,
key=key,
source=source or None,
scale_type=scale_type,
tint_color=tint_color,
placeholder_color=placeholder_color,
on_load=on_load,
on_error=on_error,
accessibility_label=accessibility_label,
accessibility_role=accessibility_role,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
_defaults={"accessibility_role": "image"},
)
def Switch(
*,
value: bool = False,
on_change: Optional[Callable[[bool], None]] = None,
accessibility_label: Optional[str] = None,
style: StyleProp = None,
key: Optional[str] = None,
) -> Element:
"""Display a toggle switch.
Args:
value: Current on/off state.
on_change: Callback invoked with the new boolean state.
accessibility_label: Label exposed to assistive technology (and
UI test drivers) for the switch.
style: Style dict (or list of dicts).
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Switch"``.
"""
return _make_element(
"Switch",
style=style,
key=key,
value=value,
on_change=on_change,
accessibility_label=accessibility_label,
)
def ProgressBar(
*,
value: float = 0.0,
color: Optional[Color] = None,
track_color: Optional[Color] = None,
indeterminate: bool = False,
style: StyleProp = None,
key: Optional[str] = None,
) -> Element:
"""Show determinate progress as a value between ``0.0`` and ``1.0``.
For a spinner instead of a bar, use
[`ActivityIndicator`][pythonnative.ActivityIndicator]; for an
indeterminate *bar* pass ``indeterminate=True``.
Args:
value: Fraction complete (clamped to ``[0.0, 1.0]`` by the
platform handler).
color: Color of the filled portion of the bar.
track_color: Color of the unfilled track behind the fill.
indeterminate: When ``True``, the bar animates continuously and
``value`` is ignored.
style: Style dict (or list of dicts).
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"ProgressBar"``.
"""
return _make_element(
"ProgressBar",
style=style,
key=key,
value=value,
color=color,
track_color=track_color,
indeterminate=indeterminate or None,
)
def ActivityIndicator(
*,
animating: bool = True,
color: Optional[Color] = None,
size: Literal["small", "large"] = "small",
style: StyleProp = None,
key: Optional[str] = None,
) -> Element:
"""Show an indeterminate loading spinner.
Args:
animating: When ``False``, the spinner is hidden.
color: Spinner color.
size: ``"small"`` (default) or ``"large"``.
style: Style dict (or list of dicts).
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type
``"ActivityIndicator"``.
"""
return _make_element(
"ActivityIndicator",
style=style,
key=key,
animating=animating,
color=color,
size=size,
)
def WebView(
*,
url: str = "",
html: Optional[str] = None,
on_load: Optional[Callable[[str], None]] = None,
on_message: Optional[Callable[[str], None]] = None,
on_navigation_state_change: Optional[Callable[[str], None]] = None,
inject_javascript: Optional[str] = None,
scroll_enabled: bool = True,
style: StyleProp = None,
key: Optional[str] = None,
) -> Element:
"""Embed web content from a URL or an inline HTML string.
Args:
url: HTTP(S) URL to load. Ignored when ``html`` is given.
html: Inline HTML markup to render instead of loading a URL.
on_load: Callback invoked with the final URL once a page
finishes loading.
on_message: Callback invoked with the string payload whenever
page JavaScript calls
``window.pythonnative.postMessage(...)``.
on_navigation_state_change: Callback invoked with the URL each
time the top-level document begins navigating.
inject_javascript: JavaScript evaluated after each page load
(useful for installing the ``postMessage`` bridge or
tweaking the DOM).
scroll_enabled: When ``False``, disables scrolling inside the
web content.
style: Style dict (or list of dicts).
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"WebView"``.
"""
return _make_element(
"WebView",
style=style,
key=key,
url=url or None,
html=html,
on_load=on_load,
on_message=on_message,
on_navigation_state_change=on_navigation_state_change,
inject_javascript=inject_javascript,
scroll_enabled=False if scroll_enabled is False else None,
)
def Spacer(
*,
size: Optional[float] = None,
flex: Optional[float] = None,
key: Optional[str] = None,
) -> Element:
"""Insert empty space inside a flex container.
Pass ``size`` for a fixed gap, or ``flex`` to expand and absorb
remaining space.
Args:
size: Fixed gap in dp/pt along the parent's main axis. Mirrored
on both axes: whichever axis the parent's
``flex_direction`` chooses as main becomes the actual gap.
flex: Flex-grow weight; useful for pushing siblings to the
opposite end of a [`Row`][pythonnative.Row] or
[`Column`][pythonnative.Column].
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Spacer"``.
"""
width = size if size is not None else None
height = size if size is not None else None
return _make_element(
"Spacer",
key=key,
size=size,
width=width,
height=height,
flex=flex,
)
def Slider(
*,
value: float = 0.0,
min_value: float = 0.0,
max_value: float = 1.0,
on_change: Optional[Callable[[float], None]] = None,
accessibility_label: Optional[str] = None,
style: StyleProp = None,
key: Optional[str] = None,
) -> Element:
"""Continuous-value slider between ``min_value`` and ``max_value``.
Args:
value: Current slider value.
min_value: Lower bound.
max_value: Upper bound.
on_change: Callback invoked with the new value as the user
drags.
accessibility_label: Label exposed to assistive technology (and
UI test drivers) for the slider.
style: Style dict (or list of dicts).
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Slider"``.
"""
return _make_element(
"Slider",
style=style,
key=key,
value=value,
min_value=min_value,
max_value=max_value,
on_change=on_change,
accessibility_label=accessibility_label,
)
# ======================================================================
# Container factories
# ======================================================================
def View(
*children: Element,
style: StyleProp = None,
gestures: Optional[List[Any]] = None,
hit_slop: Optional[Union[float, Dict[str, float]]] = None,
on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
accessibility_label: Optional[str] = None,
accessibility_hint: Optional[str] = None,
accessibility_role: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Universal flex container (like React Native's ``View``).
Defaults to ``flex_direction: "column"`` (override via ``style``).
Flex container properties (passed via ``style``):
- ``flex_direction``: ``"column"`` (default), ``"row"``,
``"column_reverse"``, ``"row_reverse"``.
- ``flex_wrap``: ``"nowrap"`` (default), ``"wrap"``,
``"wrap_reverse"``, with ``align_content`` controlling how
wrapped lines share leftover cross-axis space.
- ``justify_content``: main-axis distribution. Accepts
``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
``"space_between"``, ``"space_around"``, ``"space_evenly"``.
- ``align_items``: cross-axis alignment. Accepts ``"stretch"``
(default), ``"flex_start"``, ``"center"``, ``"flex_end"``.
- ``direction``: ``"ltr"`` (default) or ``"rtl"``. Flips rows and
resolves ``margin_start`` / ``padding_end`` / absolute ``start``
/ ``end`` insets.
- ``overflow``: ``"visible"`` (default) or ``"hidden"``.
- ``spacing`` (alias ``gap``; per-axis ``row_gap`` /
``column_gap``), ``padding``, ``background_color``,
``border_radius``, ``border_width``, ``border_color``,
``shadow_color``, ``shadow_offset``, ``shadow_opacity``,
``shadow_radius``, ``elevation``, ``opacity``, ``transform``.
Args:
*children: Child elements rendered inside the container.
style: Style dict (or list of dicts).
gestures: Optional list of gesture descriptors from
`pythonnative.gestures` (e.g. ``[gestures.Pan(on_change=)]``)
recognized natively on this view.
hit_slop: Extend the touch target beyond the view's bounds
without changing layout: a uniform number of points, or a
dict with any of ``top`` / ``left`` / ``bottom`` /
``right``.
on_layout: Callback invoked with
``{"x", "y", "width", "height"}`` after this view is laid
out, and again whenever its frame changes.
accessibility_label: Spoken description for screen readers.
accessibility_hint: Spoken extra detail (iOS only).
accessibility_role: Semantic role for assistive tech.
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"View"``.
"""
return _make_element(
"View",
*children,
style=style,
ref=ref,
key=key,
gestures=gestures,
hit_slop=hit_slop,
on_layout=on_layout,
accessibility_label=accessibility_label,
accessibility_hint=accessibility_hint,
accessibility_role=accessibility_role,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
_defaults={"flex_direction": "column"},
)
def Column(
*children: Element,
style: StyleProp = None,
on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Arrange children vertically.
Convenience wrapper around [`View`][pythonnative.View] with
``flex_direction`` locked to ``"column"``. Use ``View`` directly if
you need to switch between row and column at runtime.
Args:
*children: Child elements stacked top to bottom.
style: Style dict (or list of dicts).
on_layout: Callback invoked with
``{"x", "y", "width", "height"}`` after layout and on
frame changes.
ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Column"``.
"""
return _make_element(
"Column",
*children,
style=style,
on_layout=on_layout,
ref=ref,
key=key,
_forced={"flex_direction": "column"},
)
def Row(
*children: Element,
style: StyleProp = None,
on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Arrange children horizontally.
Convenience wrapper around [`View`][pythonnative.View] with
``flex_direction`` locked to ``"row"``. Use ``View`` directly if you
need to switch between row and column at runtime.
Args:
*children: Child elements arranged left to right.
style: Style dict (or list of dicts).
on_layout: Callback invoked with
``{"x", "y", "width", "height"}`` after layout and on
frame changes.
ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Row"``.
"""
return _make_element(
"Row",
*children,
style=style,
on_layout=on_layout,
ref=ref,
key=key,
_forced={"flex_direction": "row"},
)
def ScrollView(
*children: Element,
refresh_control: Optional[Dict[str, Any]] = None,
scroll_axis: Optional[Literal["vertical", "horizontal"]] = None,
on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
shows_scroll_indicator: bool = True,
paging_enabled: bool = False,
bounces: bool = True,
content_container_style: StyleProp = None,
keyboard_dismiss_mode: Optional[Literal["none", "on_drag", "interactive"]] = None,
style: StyleProp = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Wrap children in a scrollable container.
``ScrollView`` typically takes a single child (a ``Column`` or
``Row`` aggregating the scrollable content). It accepts ``*children``
for ergonomic call sites; the underlying native scroll view stacks
them on its content axis.
Args:
*children: Child elements to scroll.
refresh_control: Optional pull-to-refresh spec, typically
constructed via
[`RefreshControl`][pythonnative.RefreshControl]. The dict
must have ``refreshing`` (bool) and ``on_refresh``
(callable).
scroll_axis: ``"vertical"`` (default) or ``"horizontal"``.
on_scroll: Callback invoked with ``{"x": , "y": }`` content
offsets as the user scrolls.
shows_scroll_indicator: When ``False``, hides the scroll bar.
paging_enabled: When ``True``, the scroll view snaps to
multiples of its own size (carousel behavior).
bounces: When ``False``, disables the iOS rubber-band overscroll.
content_container_style: Style applied to the inner content
wrapper (padding, alignment, spacing of the scrollable
content), distinct from ``style`` (the scroll view frame).
keyboard_dismiss_mode: ``"none"`` (default), ``"on_drag"``, or
``"interactive"``. Controls whether scrolling dismisses
the keyboard.
style: Style dict (or list of dicts).
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"ScrollView"``.
"""
return _make_element(
"ScrollView",
*children,
style=style,
ref=ref,
key=key,
refresh_control=refresh_control,
scroll_axis=scroll_axis,
on_scroll=on_scroll,
shows_scroll_indicator=False if shows_scroll_indicator is False else None,
paging_enabled=paging_enabled or None,
bounces=False if bounces is False else None,
content_container_style=resolve_style(content_container_style) or None,
keyboard_dismiss_mode=keyboard_dismiss_mode,
)
_SAFE_AREA_EDGES: Tuple[str, ...] = ("top", "left", "bottom", "right")
def _numeric_edge_padding(style: Dict[str, Any], edge: str) -> float:
"""Return the numeric padding already declared for ``edge`` in ``style``.
Only numeric values participate; percentage strings and dict
shorthands are left alone (the inset simply overrides them for
that edge). Resolution order matches the layout engine:
``padding_{edge}`` beats the axis shorthand, which beats
``padding``.
"""
axis_key = "padding_vertical" if edge in ("top", "bottom") else "padding_horizontal"
for key in (f"padding_{edge}", axis_key, "padding"):
value = style.get(key)
if isinstance(value, (int, float)) and not isinstance(value, bool):
return float(value)
return 0.0
@component
def _SafeAreaContainer(**p: Any) -> Element:
"""Hook-driven body of `SafeAreaView`.
Reads the live insets via
[`use_safe_area_insets`][pythonnative.use_safe_area_insets] (so the
subtree re-renders when the platform publishes new values, e.g. on
rotation) and adds each selected edge's inset on top of any padding
the user declared for that edge.
"""
insets = use_safe_area_insets()
style: Dict[str, Any] = dict(p.get("style") or {})
edges = p.get("edges") or _SAFE_AREA_EDGES
for edge in _SAFE_AREA_EDGES:
if edge not in edges:
continue
inset = float(insets.get(edge, 0.0) or 0.0)
if inset > 0:
style[f"padding_{edge}"] = _numeric_edge_padding(style, edge) + inset
children = p.get("children") or []
return Element("SafeAreaView", style, list(children))
def SafeAreaView(
*children: Element,
edges: Optional[Tuple[Literal["top", "left", "bottom", "right"], ...]] = None,
style: StyleProp = None,
key: Optional[str] = None,
) -> Element:
"""Container that respects safe-area insets (notch, status bar, home indicator).
Applies the platform-reported insets as extra padding on the
selected edges and re-renders automatically when the insets change
(rotation, split view). User padding on an inset edge is added to
the inset, matching ``react-native-safe-area-context``.
Args:
*children: Child elements that should avoid system UI overlays.
edges: Which edges to pad; defaults to all four.
style: Style dict (or list of dicts).
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] that renders a
``"SafeAreaView"`` container.
"""
return _SafeAreaContainer(
children=list(children),
edges=tuple(edges) if edges else None,
style=resolve_style(style),
key=key,
)
def Modal(
*children: Element,
visible: bool = False,
on_dismiss: Optional[Callable[[], None]] = None,
on_show: Optional[Callable[[], None]] = None,
title: Optional[str] = None,
animation_type: Literal["slide", "fade", "none"] = "slide",
transparent: bool = False,
presentation_style: Literal["page_sheet", "form_sheet", "full_screen", "overlay"] = "page_sheet",
dismiss_on_backdrop: bool = True,
style: StyleProp = None,
key: Optional[str] = None,
) -> Element:
"""Overlay modal dialog backed by a real native presentation.
The modal is shown when ``visible=True`` and hidden when ``False``.
Drive ``visible`` from a hook so the parent component can dismiss
the modal in response to user actions. On iOS this presents a
``UIViewController``; on Android it shows an ``android.app.Dialog``.
Children are mounted as the modal's content view, not into the
on-tree placeholder, so they appear above all other native content
and don't influence the underlying layout.
Args:
*children: Modal content.
visible: Controls whether the modal is presented.
on_dismiss: Callback invoked when the user dismisses the modal
via system gesture.
on_show: Callback invoked once the modal has finished
presenting.
title: Optional title-bar text.
animation_type: ``"slide"`` (default), ``"fade"``, or ``"none"``.
transparent: When ``True``, the underlying view is dimmed
instead of fully covered.
presentation_style: iOS presentation style,
``"page_sheet"`` (default), ``"form_sheet"``,
``"full_screen"``, or ``"overlay"`` (custom dimmed
overlay). On Android, ``"overlay"`` keeps the dialog
non-fullscreen.
dismiss_on_backdrop: When ``True`` (default) and
``transparent`` / ``"overlay"``, tapping the dimmed
backdrop dismisses the modal.
style: Style dict (or list of dicts).
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Modal"``.
"""
return _make_element(
"Modal",
*children,
style=style,
key=key,
visible=visible,
animation_type=animation_type,
transparent=transparent,
presentation_style=presentation_style,
dismiss_on_backdrop=False if dismiss_on_backdrop is False else None,
on_dismiss=on_dismiss,
on_show=on_show,
title=title,
)
@component
def _StatefulPressable(**p: Any) -> Element:
"""Hook-driven Pressable used when ``style`` is a callable.
Tracks the pressed state with ``use_state`` and re-invokes the
user's style function with ``{"pressed": bool}`` on every press
transition, mirroring React Native's function-style ``style`` prop.
"""
pressed, set_pressed = use_state(False)
style_fn = p["style_fn"]
user_press_in = p.get("on_press_in")
user_press_out = p.get("on_press_out")
def _press_in() -> None:
set_pressed(True)
if user_press_in is not None:
user_press_in()
def _press_out() -> None:
set_pressed(False)
if user_press_out is not None:
user_press_out()
forwarded = {k: v for k, v in p.items() if k not in ("children", "style_fn", "on_press_in", "on_press_out")}
return _make_element(
"Pressable",
*(p.get("children") or []),
style=resolve_style(style_fn({"pressed": pressed})),
on_press_in=_press_in,
on_press_out=_press_out,
_defaults={"accessibility_role": "button"},
**forwarded,
)
def Pressable(
*children: Element,
on_press: Optional[Callable[[], None]] = None,
on_long_press: Optional[Callable[[], None]] = None,
on_press_in: Optional[Callable[[], None]] = None,
on_press_out: Optional[Callable[[], None]] = None,
pressed_opacity: float = 0.6,
gestures: Optional[List[Any]] = None,
hit_slop: Optional[Union[float, Dict[str, float]]] = None,
on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
style: Union[StyleProp, Callable[[Dict[str, bool]], StyleProp]] = None,
accessibility_label: Optional[str] = None,
accessibility_hint: Optional[str] = None,
accessibility_role: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Wrap children with tap / long-press / gesture handlers.
Useful for making non-button elements (text, images, custom views)
respond to user taps. The wrapper view fades to ``pressed_opacity``
on touch-down and back to full opacity on touch-up.
Pressable gets ``accessibility_role="button"`` by default.
Args:
*children: Elements to make pressable.
on_press: Callback invoked on a normal tap.
on_long_press: Callback invoked on a sustained press.
on_press_in: Callback invoked the moment the press starts.
on_press_out: Callback invoked when the press lifts or cancels.
pressed_opacity: Opacity (01) applied while the user's finger
is down. Set to ``1.0`` for no visual feedback.
gestures: Optional list of gesture descriptors from
`pythonnative.gestures` recognized natively on this view
(pan / swipe / pinch / rotation / multi-tap).
hit_slop: Extend the touch target beyond the view's bounds
without changing layout: a uniform number of points, or a
dict with any of ``top`` / ``left`` / ``bottom`` /
``right``. Essential for small touch targets (icons,
chips) that should honor the 44-point guideline.
on_layout: Callback invoked with
``{"x", "y", "width", "height"}`` after layout and on
frame changes.
style: Style dict applied to the wrapper, or a callable
receiving the interaction state (``{"pressed": bool}``)
and returning a style, re-evaluated on every press
transition:
```python
pn.Pressable(
pn.Text("Tap"),
style=lambda s: pn.style(
background_color="#0051A8" if s["pressed"] else "#007AFF",
),
)
```
accessibility_label: Spoken description for screen readers.
accessibility_hint: Spoken extra detail (iOS only).
accessibility_role: Override the default ``"button"`` role.
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Pressable"``
(wrapped in a stateful composite when ``style`` is callable).
"""
if callable(style):
return _StatefulPressable(
children=list(children),
style_fn=style,
ref=ref,
key=key,
on_press=on_press,
on_long_press=on_long_press,
on_press_in=on_press_in,
on_press_out=on_press_out,
pressed_opacity=pressed_opacity,
gestures=gestures,
hit_slop=hit_slop,
on_layout=on_layout,
accessibility_label=accessibility_label,
accessibility_hint=accessibility_hint,
accessibility_role=accessibility_role,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
)
return _make_element(
"Pressable",
*children,
style=style,
ref=ref,
key=key,
on_press=on_press,
on_long_press=on_long_press,
on_press_in=on_press_in,
on_press_out=on_press_out,
pressed_opacity=pressed_opacity,
gestures=gestures,
hit_slop=hit_slop,
on_layout=on_layout,
accessibility_label=accessibility_label,
accessibility_hint=accessibility_hint,
accessibility_role=accessibility_role,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
_defaults={"accessibility_role": "button"},
)
# ======================================================================
# Touchables & controls
# ======================================================================
def TouchableOpacity(
*children: Element,
on_press: Optional[Callable[[], None]] = None,
on_long_press: Optional[Callable[[], None]] = None,
active_opacity: float = 0.2,
disabled: bool = False,
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessibility_hint: Optional[str] = None,
accessibility_role: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
key: Optional[str] = None,
) -> Element:
"""Wrap children so they fade to ``active_opacity`` while pressed.
A thin ergonomic alias over [`Pressable`][pythonnative.Pressable]
that mirrors React Native's ``TouchableOpacity``: the only visual
feedback is an opacity dip on touch-down. When ``disabled`` is set,
the press callbacks are dropped so the wrapper is inert.
Args:
*children: Elements to make tappable.
on_press: Callback invoked on a normal tap.
on_long_press: Callback invoked on a sustained press.
active_opacity: Opacity (01) applied while the finger is down.
disabled: When ``True``, ignores presses and renders at reduced
opacity.
style: Style dict applied to the wrapper.
accessibility_label: Spoken description for screen readers.
accessibility_hint: Spoken extra detail (iOS only).
accessibility_role: Override the default ``"button"`` role.
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Pressable"``.
"""
merged_style: StyleProp
if disabled:
base = resolve_style(style)
base.setdefault("opacity", 0.4)
merged_style = base
else:
merged_style = style
return Pressable(
*children,
on_press=None if disabled else on_press,
on_long_press=None if disabled else on_long_press,
pressed_opacity=active_opacity,
style=merged_style,
accessibility_label=accessibility_label,
accessibility_hint=accessibility_hint,
accessibility_role=accessibility_role,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
key=key,
)
def ImageBackground(
*children: Element,
source: str = "",
scale_type: Optional[ScaleType] = None,
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
key: Optional[str] = None,
) -> Element:
"""Render ``children`` layered on top of a background image.
Composed entirely from existing primitives: an absolutely-filled
[`Image`][pythonnative.Image] sits behind a content
[`View`][pythonnative.View] holding ``children``. The container's
``style`` controls sizing/padding; the image stretches to fill it
via ``position: "absolute"`` and zeroed insets.
Args:
*children: Foreground content drawn over the image.
source: Image resource name or URL.
scale_type: Background fit mode (``"cover"`` is the most common
for backgrounds).
style: Style dict for the container (size, padding, alignment).
accessibility_label: Spoken description of the background image.
accessible: Override whether the image is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"View"`` wrapping
the background image and foreground content.
"""
fill = {"position": "absolute", "top": 0, "left": 0, "right": 0, "bottom": 0}
background = Image(
source,
scale_type=scale_type or "cover",
style=fill,
accessibility_label=accessibility_label,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
)
content = View(*children, style={"flex": 1})
return View(
background,
content,
style=[{"overflow": "hidden"}, resolve_style(style)],
key=key,
)
def Checkbox(
*,
value: bool = False,
on_change: Optional[Callable[[bool], None]] = None,
label: Optional[str] = None,
disabled: bool = False,
color: Optional[Color] = None,
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessibility_hint: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
key: Optional[str] = None,
) -> Element:
"""A boolean checkbox with an optional inline label.
Backed by ``android.widget.CheckBox`` on Android and a checkmark
``UIButton`` on iOS. Tapping the control (or its label) toggles the
value and fires ``on_change(new_value)``.
Args:
value: Current checked state.
on_change: Callback invoked with the new boolean state.
label: Optional text shown beside the box (also tappable).
disabled: When ``True``, the control is greyed out and inert.
color: Tint applied to the checked box.
style: Style dict (or list of dicts).
accessibility_label: Spoken description for screen readers.
accessibility_hint: Spoken extra detail (iOS only).
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Checkbox"``.
"""
return _make_element(
"Checkbox",
style=style,
key=key,
value=value,
on_change=on_change,
label=label,
disabled=disabled or None,
color=color,
accessibility_label=accessibility_label,
accessibility_hint=accessibility_hint,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
_defaults={"accessibility_role": "checkbox"},
)
def SegmentedControl(
*,
segments: Optional[List[str]] = None,
selected_index: int = 0,
on_change: Optional[Callable[[int], None]] = None,
enabled: bool = True,
tint_color: Optional[Color] = None,
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
key: Optional[str] = None,
) -> Element:
"""A horizontal multi-choice control (one selected segment at a time).
Backed by ``UISegmentedControl`` on iOS and a styled toggle row on
Android. Selecting a segment fires ``on_change(index)``.
Args:
segments: Ordered list of segment labels.
selected_index: Index of the currently selected segment.
on_change: Callback invoked with the newly selected index.
enabled: When ``False``, the control is disabled.
tint_color: Accent color for the selected segment.
style: Style dict (or list of dicts).
accessibility_label: Spoken description for screen readers.
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type
``"SegmentedControl"``.
"""
return _make_element(
"SegmentedControl",
style=style,
key=key,
segments=list(segments) if segments is not None else [],
selected_index=selected_index,
on_change=on_change,
enabled=False if enabled is False else None,
tint_color=tint_color,
accessibility_label=accessibility_label,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
)
def DatePicker(
*,
value: Optional[str] = None,
mode: Literal["date", "time", "datetime"] = "date",
on_change: Optional[Callable[[str], None]] = None,
minimum: Optional[str] = None,
maximum: Optional[str] = None,
enabled: bool = True,
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
key: Optional[str] = None,
) -> Element:
"""A native date / time picker.
Backed by ``UIDatePicker`` on iOS and a trigger button that opens
the platform ``DatePickerDialog`` / ``TimePickerDialog`` on
Android. ``value`` and the value reported to ``on_change`` are
ISO-8601 strings (``"2026-05-31"`` for ``mode="date"``, ``"14:30"``
for ``mode="time"``, ``"2026-05-31T14:30"`` for
``mode="datetime"``), so values stay JSON-serializable and
platform-agnostic.
Args:
value: Currently selected value as an ISO-8601 string.
mode: ``"date"`` (default), ``"time"``, or ``"datetime"``.
on_change: Callback invoked with the new ISO-8601 string.
minimum: Earliest selectable value (ISO-8601), if any.
maximum: Latest selectable value (ISO-8601), if any.
enabled: When ``False``, the picker is disabled.
style: Style dict (or list of dicts).
accessibility_label: Spoken description for screen readers.
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"DatePicker"``.
"""
return _make_element(
"DatePicker",
style=style,
key=key,
value=value,
mode=mode,
on_change=on_change,
minimum=minimum,
maximum=maximum,
enabled=False if enabled is False else None,
accessibility_label=accessibility_label,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
_defaults={"accessibility_role": "button"},
)
# ======================================================================
# Fragment
# ======================================================================
def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element:
"""Group children without adding a wrapping native view.
Like React's ````: groups elements without introducing an
extra container. Each child mounts as a direct sibling of the
Fragment's position in the parent's child list. Components may
also simply return a plain ``list`` of elements; ``Fragment``
exists for when the group needs a ``key`` (e.g. rendering a list
of pairs) or when a single expression reads better.
```python
pn.Column(
pn.Text("Top"),
pn.Fragment(
pn.Text("Middle A"),
pn.Text("Middle B"),
),
pn.Text("Bottom"),
)
```
Args:
*children: Child elements to expose at the parent level.
``None`` and ``False`` children are dropped, which makes
conditional rendering with ``cond and pn.Text(...)``
ergonomic.
key: Stable identity for keyed reconciliation. A keyed
Fragment moves all of its children as one unit and
preserves their state across reorders.
Returns:
An [`Element`][pythonnative.Element] of type ``"__Fragment__"``.
"""
kept = [c for c in children if c is not None and c is not False]
return Element("__Fragment__", {}, kept, key=key)
def Portal(*children: Element, key: Optional[str] = None) -> Element:
"""Render ``children`` into a full-screen overlay above everything else.
Like React DOM's ``createPortal``: the children stay part of this
component's tree for state, context, and events, but their native
views mount in a transparent overlay attached to the window (above
the screen's content) instead of inside the surrounding parent.
Use it for toasts, dropdowns, tooltips, and lightweight custom
overlays that must escape ``overflow: "hidden"`` ancestors. For a
system-styled dialog with its own presentation and dismissal
gestures, use [`Modal`][pythonnative.Modal] instead.
The overlay itself does not intercept touches; only the children
themselves are hit-testable. Children are laid out against the
full viewport, so position them with absolute insets:
```python
pn.Portal(
pn.View(
pn.Text("Saved!"),
style=pn.style(position="absolute", bottom=40, left=40, right=40),
),
)
```
Args:
*children: Overlay content.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Portal"``.
"""
return Element("Portal", {}, list(children), key=key)
# ======================================================================
# Error boundary
# ======================================================================
def ErrorBoundary(
*children: Element,
fallback: Optional[Any] = None,
on_error: Optional[Callable[[BaseException], None]] = None,
key: Optional[str] = None,
) -> Element:
"""Catch render errors in the wrapped subtree and display ``fallback`` instead.
When any descendant raises during render (initial mount, a parent
re-render, or a local state-driven update), the failed subtree is
torn down and ``fallback`` is mounted in its place. Without a
boundary the error propagates to the screen host (which shows the
dev error overlay in dev mode).
``fallback`` may be:
- An [`Element`][pythonnative.Element], shown as-is.
- ``fallback(error) -> Element``.
- ``fallback(error, reset) -> Element``, where ``reset`` is a
zero-arg callable that clears the error and remounts the
original children (fresh state), for retry buttons.
Args:
*children: Subtree to wrap.
fallback: Fallback content (see above). Required for the
boundary to actually catch; without it errors propagate
to the next boundary up.
on_error: Callback invoked with the exception when the
boundary catches, before the fallback mounts. Use it for
error reporting.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type
``"__ErrorBoundary__"``.
Example:
```python
import pythonnative as pn
pn.ErrorBoundary(
MyRiskyComponent(),
fallback=lambda err, reset: pn.Column(
pn.Text(f"Error: {err}"),
pn.Button("Retry", on_press=reset),
),
on_error=lambda err: log.exception(err),
)
```
"""
props: Dict[str, Any] = {}
if fallback is not None:
props["__fallback__"] = fallback
if on_error is not None:
props["__on_error__"] = on_error
return Element("__ErrorBoundary__", props, list(children), key=key)
def Suspense(
*children: Element,
fallback: Optional[Any] = None,
key: Optional[str] = None,
) -> Element:
"""Show ``fallback`` while descendants wait on async work, then swap in the content.
A Suspense boundary catches **suspensions** from the subtree it
wraps: an ``async def`` component body blocking on a pending
await, or a regular component calling
[`Resource.read`][pythonnative.suspense.Resource.read] on data that hasn't
arrived (see [`use_resource`][pythonnative.use_resource] and
[`lazy`][pythonnative.lazy]). While anything is pending the
boundary renders ``fallback``; when the awaited work completes it
retries the content and swaps it in. Suspended components keep
their hook state across retries, so cached resources aren't
refetched.
Two timing behaviors, matching React:
- **Initial mount**: the fallback shows until the content is ready.
- **Updates**: a component that's already on screen and suspends
again (its dependencies changed) keeps its previous content
visible and re-renders when ready; there's no fallback flash.
Args:
*children: Subtree to wrap (the async content).
fallback: Content shown while suspended: an
[`Element`][pythonnative.Element] or a zero-arg callable
returning one. Without it, suspensions propagate to the
next Suspense boundary up.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"__Suspense__"``.
Example:
```python
import pythonnative as pn
@pn.component
async def Profile(user_id: str):
user = await api.fetch_user(user_id)
return pn.Text(user.name)
@pn.component
def Screen():
return pn.Suspense(
Profile(user_id="42"),
fallback=pn.ActivityIndicator(),
)
```
"""
props: Dict[str, Any] = {}
if fallback is not None:
props["__fallback__"] = fallback
return Element("__Suspense__", props, list(children), key=key)
# ======================================================================
# Lists (native virtualization with a Python-windowed fallback)
# ======================================================================
#
# FlatList and SectionList pick between two engines:
#
# 1. **Native virtualization** (`_NativeList` -> the ``VirtualList``
# element): on Android and iOS, when every row extent is known up
# front and no windowed-only feature is requested, the list is
# backed by a real ``RecyclerView`` / ``UITableView``. The platform
# owns row recycling; each visible row hosts a nested-reconciler
# subtree (see ``pythonnative.virtual_rows``).
# 2. **Python windowing** (`_VirtualizedList`): a windowed slice of
# rows rendered into a ScrollView (leading spacer, visible rows,
# trailing spacer), the window shifting from scroll events (the
# same architecture as React Native's VirtualizedList). Because
# every windowed row lives in the *main* layout tree, rows may be
# any height: estimates steer the spacer sizes and measured extents
# correct them over time. This is the desktop path and the fallback
# for variable-height rows, grids, horizontal lists, ornaments, and
# pull-to-refresh.
_DEFAULT_ROW_EXTENT = 44.0
class _RowSpec:
"""One virtualized row: a stable key, a lazy renderer, and an extent hint."""
__slots__ = ("key", "make", "extent", "item", "index")
def __init__(
self,
key: str,
make: Callable[[], Element],
extent: Optional[float],
item: Any = None,
index: int = 0,
) -> None:
self.key = key
self.make = make
self.extent = extent
self.item = item
self.index = index
def _dispatch_scroll_command(scroll_ref: Any, name: str, args: Dict[str, Any]) -> Any:
"""Send an imperative command to the ScrollView under ``scroll_ref``."""
tag = getattr(scroll_ref, "_pn_tag", None)
if tag is None:
return None
from .native_views import get_registry
try:
return get_registry().command(tag, name, args)
except Exception:
return None
class ListController:
"""Imperative scroll handle published on a list's ``ref``.
[`FlatList`][pythonnative.FlatList] and
[`SectionList`][pythonnative.SectionList] install a
``ListController`` on ``ref.current`` (via
[`use_imperative_handle`][pythonnative.use_imperative_handle])
after mount and clear it back to ``None`` on unmount.
Example:
```python
import pythonnative as pn
@pn.component
def Chat(messages):
list_ref = pn.use_ref()
pn.use_layout_effect(
lambda: list_ref.current and list_ref.current.scroll_to_end(animated=False),
[len(messages)],
)
return pn.FlatList(data=messages, render_item=Bubble, ref=list_ref)
```
"""
__slots__ = ("_scroll_to_offset", "_scroll_to_index", "_scroll_to_end")
def __init__(
self,
scroll_to_offset: Callable[[float, bool], None],
scroll_to_index: Callable[[int, bool], None],
scroll_to_end: Callable[[bool], None],
) -> None:
self._scroll_to_offset = scroll_to_offset
self._scroll_to_index = scroll_to_index
self._scroll_to_end = scroll_to_end
def scroll_to_offset(self, offset: float, animated: bool = True) -> None:
"""Scroll to an absolute content offset in points."""
self._scroll_to_offset(offset, animated)
def scroll_to_index(self, index: int, animated: bool = True) -> None:
"""Scroll so the row at ``index`` sits at the top of the viewport."""
self._scroll_to_index(index, animated)
def scroll_to_end(self, animated: bool = True) -> None:
"""Scroll to the end of the content."""
self._scroll_to_end(animated)
@component
def _VirtualizedList(**p: Any) -> Element:
"""Shared windowing engine behind FlatList and SectionList."""
rows: List[_RowSpec] = p.get("rows") or []
n = len(rows)
horizontal: bool = bool(p.get("horizontal"))
estimated: float = float(p.get("estimated_row_extent") or _DEFAULT_ROW_EXTENT)
overscan: float = float(p.get("overscan_extent") or 0.0)
initial_extent: float = float(p.get("initial_window_extent") or 800.0)
window, set_window = use_state((0, -1))
measured: Ref[Dict[str, float]] = use_ref({}) # row key -> measured extent (points)
row_refs: Ref[Dict[str, Ref]] = use_ref({}) # row key -> Ref for live rows
end_latch = use_ref({"fired_for": -1})
viewable_ref: Ref[Dict[str, Tuple[str, ...]]] = use_ref({"keys": ()})
scroll_pos = use_ref({"offset": 0.0})
sv_ref: Ref = use_ref(None)
# ------------------------------------------------------------------
# Extent model: measured > per-row hint > estimate. ``starts`` are
# prefix sums; ``starts[n]`` is the total content extent.
# ------------------------------------------------------------------
measured_map: Dict[str, float] = measured.current
starts: List[float] = [0.0] * (n + 1)
acc = 0.0
for i, spec in enumerate(rows):
starts[i] = acc
extent = measured_map.get(spec.key)
if extent is None:
extent = spec.extent if spec.extent is not None else estimated
acc += max(0.0, float(extent))
starts[n] = acc
total_extent = acc
def _viewport_extent() -> float:
frame = sv_ref._pn_frame
if frame:
extent = frame[2] if horizontal else frame[3]
if extent and extent > 0:
return float(extent)
return initial_extent
def _window_for(offset: float, viewport: float) -> Tuple[int, int]:
if n == 0:
return (0, -1)
pad = overscan if overscan > 0 else viewport
lo = max(0.0, offset - pad)
hi = offset + viewport + pad
first = max(0, bisect.bisect_right(starts, lo, 0, n) - 1)
last = min(n - 1, bisect.bisect_left(starts, hi, 0, n))
return (first, last)
first, last = window
if last < 0 or first >= n:
first, last = _window_for(scroll_pos.current["offset"], _viewport_extent())
last = min(last, n - 1)
first = max(0, min(first, max(0, n - 1)))
# ------------------------------------------------------------------
# Scroll handling: sweep measured extents, shift the window, fire
# end-reached / viewability callbacks. State only changes when the
# window actually moves, so steady scrolling inside the overscan
# region costs no re-render.
# ------------------------------------------------------------------
on_end_reached = p.get("on_end_reached")
end_threshold = float(p.get("on_end_reached_threshold") or 0.5)
on_viewable = p.get("on_viewable_items_changed")
user_on_scroll = p.get("on_scroll")
def _sweep_measured() -> None:
for row_key, row_ref in row_refs.current.items():
frame = getattr(row_ref, "_pn_frame", None)
if frame:
extent = frame[2] if horizontal else frame[3]
if extent and extent > 0:
measured_map[row_key] = float(extent)
def _handle_scroll(payload: Any) -> None:
if isinstance(payload, dict):
offset = float(payload.get("x" if horizontal else "y", 0.0) or 0.0)
else:
offset = float(payload or 0.0)
scroll_pos.current["offset"] = offset
_sweep_measured()
viewport = _viewport_extent()
new_window = _window_for(offset, viewport)
if new_window != (first, last):
set_window(new_window)
if on_end_reached is not None and total_extent > 0:
remaining = total_extent - (offset + viewport)
if remaining end_threshold * viewport + viewport:
end_latch.current["fired_for"] = -1
if on_viewable is not None and n > 0:
v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
if keys != viewable_ref.current["keys"]:
viewable_ref.current["keys"] = keys
on_viewable(
[
{"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
for i in range(v_first, v_last + 1)
]
)
if user_on_scroll is not None:
user_on_scroll(payload)
# ------------------------------------------------------------------
# Imperative controller (scroll_to_index / offset / end) published
# on the user's ref. Rebuilt every render (deps=None) so the
# closures see fresh extents.
# ------------------------------------------------------------------
def _scroll_to_offset(offset: float, animated: bool = True) -> None:
axis = "x" if horizontal else "y"
_dispatch_scroll_command(sv_ref, "scroll_to_offset", {axis: float(offset), "animated": animated})
def _scroll_to_index(index: int, animated: bool = True) -> None:
idx = max(0, min(int(index), n - 1)) if n else 0
_scroll_to_offset(starts[idx], animated)
def _scroll_to_end(animated: bool = True) -> None:
_scroll_to_offset(max(0.0, total_extent - _viewport_extent()), animated)
use_imperative_handle(
p.get("controller_ref"),
lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
None,
)
# ------------------------------------------------------------------
# Children: header, leading spacer, windowed rows, trailing spacer,
# footer. Rows keep per-key refs so their measured extents survive
# recycling.
# ------------------------------------------------------------------
spacer_key = "width" if horizontal else "height"
children: List[Element] = []
header = p.get("header")
footer = p.get("footer")
if header is not None:
children.append(View(header, key="__pn_header__"))
if n == 0:
empty = p.get("empty")
if empty is not None:
children.append(View(empty, key="__pn_empty__"))
else:
live_refs: Dict[str, Ref] = {}
lead = starts[first]
if lead > 0:
lead_style: Dict[str, Any] = {spacer_key: lead}
children.append(View(style=lead_style, key="__pn_lead__"))
for i in range(first, last + 1):
spec = rows[i]
row_ref = row_refs.current.get(spec.key) or Ref()
live_refs[spec.key] = row_ref
children.append(View(spec.make(), ref=row_ref, key=spec.key))
row_refs.current = live_refs
trail = total_extent - starts[last + 1]
if trail > 0:
trail_style: Dict[str, Any] = {spacer_key: trail}
children.append(View(style=trail_style, key="__pn_trail__"))
if footer is not None:
children.append(View(footer, key="__pn_footer__"))
wrapper = Row if horizontal else Column
inner = wrapper(*children, style=p.get("content_container_style"))
return ScrollView(
inner,
scroll_axis="horizontal" if horizontal else "vertical",
on_scroll=_handle_scroll,
refresh_control=p.get("refresh_control"),
shows_scroll_indicator=p.get("shows_scroll_indicator", True),
style=p.get("list_style"),
ref=sv_ref,
)
def _native_lists_supported() -> bool:
"""Whether the natively virtualized list path is available.
Android (RecyclerView) and iOS (UITableView) have native handlers;
the desktop preview and off-device tests use the Python-windowed
engine. Patchable in tests to exercise the native routing.
"""
from .utils import IS_ANDROID, IS_IOS
return IS_ANDROID or IS_IOS
@component
def _NativeList(**p: Any) -> Element:
"""Platform-virtualized list: emits a ``VirtualList`` native element.
The native side (RecyclerView / UITableView) owns row windowing and
recycling; each visible row hosts a nested-reconciler subtree (see
``pythonnative.virtual_rows``). This composite adapts the FlatList /
SectionList surface onto that element: it forwards ``render_row``,
derives ``on_end_reached`` and ``on_viewable_items_changed`` from
native scroll reports, and wires the imperative scroll controller.
Requires every row's extent to be known up front (the native
virtualizers need exact heights before rows are rendered); callers
fall back to the Python-windowed engine otherwise.
"""
rows: List[_RowSpec] = p.get("rows") or []
n = len(rows)
heights: List[float] = [float(spec.extent or 0.0) for spec in rows]
uniform = len(set(heights)) Element:
if 0 None:
offset = float(payload.get("y", 0.0) or 0.0) if isinstance(payload, dict) else float(payload or 0.0)
viewport = float(payload.get("extent", 0.0) or 0.0) if isinstance(payload, dict) else 0.0
if viewport 0:
remaining = total_extent - (offset + viewport)
if remaining end_threshold * viewport + viewport:
end_latch.current["fired_for"] = -1
if on_viewable is not None and n > 0:
v_first = max(0, bisect.bisect_right(starts, offset, 0, n) - 1)
v_last = min(n - 1, bisect.bisect_left(starts, offset + viewport, 0, n))
keys = tuple(rows[i].key for i in range(v_first, v_last + 1))
if keys != viewable_ref.current["keys"]:
viewable_ref.current["keys"] = keys
on_viewable(
[
{"index": rows[i].index, "key": rows[i].key, "item": rows[i].item}
for i in range(v_first, v_last + 1)
]
)
if user_on_scroll is not None:
user_on_scroll({"x": 0.0, "y": offset})
def _scroll_to_offset(offset: float, animated: bool = True) -> None:
_dispatch_scroll_command(internal_ref, "scroll_to_offset", {"y": float(offset), "animated": animated})
def _scroll_to_index(index: int, animated: bool = True) -> None:
_dispatch_scroll_command(internal_ref, "scroll_to_index", {"index": int(index), "animated": animated})
def _scroll_to_end(animated: bool = True) -> None:
_dispatch_scroll_command(internal_ref, "scroll_to_end", {"animated": animated})
use_imperative_handle(
p.get("controller_ref"),
lambda: ListController(_scroll_to_offset, _scroll_to_index, _scroll_to_end),
None,
)
props: Dict[str, Any] = dict(p.get("list_style") or {})
props["count"] = n
if uniform:
props["row_height"] = heights[0] if heights else _DEFAULT_ROW_EXTENT
else:
props["row_heights"] = heights
props["render_row"] = _render_row
props["ref"] = internal_ref
wants_scroll = on_end_reached is not None or on_viewable is not None or user_on_scroll is not None
if wants_scroll:
props["on_scroll"] = _handle_scroll
if p.get("shows_scroll_indicator") is False:
props["shows_scroll_indicator"] = False
return Element("VirtualList", props, [])
def _all_extents_known(rows: List[_RowSpec]) -> bool:
return all(spec.extent is not None for spec in rows)
def FlatList(
*,
data: Optional[List[Any]] = None,
render_item: Optional[Callable[[Any, int], Element]] = None,
key_extractor: Optional[Callable[[Any, int], str]] = None,
item_height: Optional[float] = None,
get_item_height: Optional[Callable[[Any, int], float]] = None,
estimated_item_height: Optional[float] = None,
separator_height: float = 0,
refresh_control: Optional[Dict[str, Any]] = None,
horizontal: bool = False,
num_columns: int = 1,
list_header: Optional[Element] = None,
list_footer: Optional[Element] = None,
list_empty: Optional[Element] = None,
on_end_reached: Optional[Callable[[], None]] = None,
on_end_reached_threshold: float = 0.5,
on_viewable_items_changed: Optional[Callable[[List[Dict[str, Any]]], None]] = None,
on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
shows_scroll_indicator: bool = True,
content_container_style: StyleProp = None,
style: StyleProp = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Virtualized scrollable list that renders items from ``data`` lazily.
Only the rows inside (and just beyond) the viewport are mounted;
leading and trailing spacers stand in for everything else, and the
window shifts as the user scrolls. Rows may have **variable
heights**: pass ``item_height`` when rows are uniform,
``get_item_height`` for exact per-item extents, or nothing at all;
unknown rows start at ``estimated_item_height`` and are corrected
with their measured extent once they've been on screen.
Pass a [`Ref`][pythonnative.Ref] (from
[`use_ref`][pythonnative.use_ref]) to receive a
[`ListController`][pythonnative.ListController] on ``ref.current``:
``ref.current.scroll_to_index(i)``,
``ref.current.scroll_to_offset(pts)``, and
``ref.current.scroll_to_end()``.
Args:
data: List of arbitrary item values.
render_item: ``render_item(item, index) -> Element``. Defaults
to wrapping each item in a [`Text`][pythonnative.Text].
key_extractor: Function returning a stable key per item
(recommended whenever ``data`` can reorder).
item_height: Uniform row extent in points, when known.
get_item_height: ``get_item_height(item, index) -> float`` for
exact variable extents without measurement.
estimated_item_height: Starting extent estimate for rows whose
true size isn't known yet (default 44).
separator_height: Gap below each row, in points.
refresh_control: Optional pull-to-refresh spec from
[`RefreshControl`][pythonnative.RefreshControl].
horizontal: Scroll horizontally (extents become widths).
num_columns: Render items in a grid of this many columns.
list_header: Element rendered once before all rows.
list_footer: Element rendered once after all rows.
list_empty: Element rendered when ``data`` is empty.
on_end_reached: Called when the user scrolls within
``on_end_reached_threshold`` viewports of the end (fires
once per data length).
on_end_reached_threshold: Distance from the end, in viewport
multiples, at which ``on_end_reached`` fires.
on_viewable_items_changed: Called with a list of
``{"index", "key", "item"}`` dicts whenever the set of
visible rows changes.
on_scroll: Called with the raw scroll payload
(``{"x": , "y": }``).
shows_scroll_indicator: When ``False``, hides the scroll bar.
content_container_style: Style applied to the inner content
wrapper.
style: Style for the outer scroll container.
ref: Optional [`Ref`][pythonnative.Ref]; receives a
[`ListController`][pythonnative.ListController] on
``ref.current`` after mount.
key: Stable identity for keyed reconciliation of the list.
Returns:
A virtualized list element (a function component instance).
Example:
```python
import pythonnative as pn
items = [{"id": i, "name": f"Item {i}"} for i in range(10000)]
pn.FlatList(
data=items,
item_height=44,
render_item=lambda item, _: pn.Text(item["name"]),
key_extractor=lambda item, _: str(item["id"]),
)
```
"""
items_list = list(data or [])
sep = float(separator_height or 0.0)
def _row_key(item: Any, index: int) -> str:
if key_extractor is not None:
try:
return str(key_extractor(item, index))
except Exception:
pass
return f"__pn_row_{index}__"
def _row_extent(item: Any, index: int) -> Optional[float]:
if get_item_height is not None:
try:
return float(get_item_height(item, index)) + sep
except Exception:
return None
if item_height is not None:
return float(item_height) + sep
return None
def _make_row(item: Any, index: int) -> Callable[[], Element]:
def _make() -> Element:
el = render_item(item, index) if render_item else Text(str(item))
if sep > 0:
pad_style: Dict[str, Any] = {"padding_end" if horizontal else "padding_bottom": sep}
return View(el, style=pad_style)
return el
return _make
rows: List[_RowSpec] = []
if num_columns > 1 and not horizontal:
for start in range(0, len(items_list), num_columns):
chunk = items_list[start : start + num_columns]
def _make_group(group: List[Any] = chunk, base: int = start) -> Element:
cells = [
View(
render_item(it, base + j) if render_item else Text(str(it)),
style={"flex": 1},
key=_row_key(it, base + j),
)
for j, it in enumerate(group)
]
row = Row(*cells)
if sep > 0:
return View(row, style={"padding_bottom": sep})
return row
group_key = "__pn_grp_" + "|".join(_row_key(it, start + j) for j, it in enumerate(chunk))
extent = (float(item_height) + sep) if item_height is not None else None
rows.append(_RowSpec(group_key, _make_group, extent, item=chunk, index=start))
else:
for i, item in enumerate(items_list):
rows.append(_RowSpec(_row_key(item, i), _make_row(item, i), _row_extent(item, i), item=item, index=i))
estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
# Route to the platform virtualizer (RecyclerView / UITableView)
# when it can represent this list exactly: vertical, single-column,
# every row extent known up front, and no features that only the
# Python-windowed engine implements (ornaments, pull-to-refresh).
if (
_native_lists_supported()
and not horizontal
and num_columns == 1
and list_header is None
and list_footer is None
and list_empty is None
and refresh_control is None
and _all_extents_known(rows)
):
return _NativeList(
rows=rows,
on_end_reached=on_end_reached,
on_end_reached_threshold=on_end_reached_threshold,
on_viewable_items_changed=on_viewable_items_changed,
on_scroll=on_scroll,
shows_scroll_indicator=shows_scroll_indicator,
list_style=resolve_style(style) or None,
controller_ref=ref,
key=key,
)
return _VirtualizedList(
rows=rows,
horizontal=horizontal,
estimated_row_extent=float(estimated) + sep,
header=list_header,
footer=list_footer,
empty=list_empty,
refresh_control=refresh_control,
on_end_reached=on_end_reached,
on_end_reached_threshold=on_end_reached_threshold,
on_viewable_items_changed=on_viewable_items_changed,
on_scroll=on_scroll,
shows_scroll_indicator=shows_scroll_indicator,
content_container_style=resolve_style(content_container_style) or None,
list_style=resolve_style(style) or None,
controller_ref=ref,
key=key,
)
def SectionList(
*,
sections: Optional[List[Dict[str, Any]]] = None,
render_item: Optional[Callable[[Any, int, int], Element]] = None,
render_section_header: Optional[Callable[[Dict[str, Any], int], Element]] = None,
key_extractor: Optional[Callable[[Any, int], str]] = None,
item_height: Optional[float] = None,
get_item_height: Optional[Callable[[Any, int, int], float]] = None,
estimated_item_height: Optional[float] = None,
section_header_height: Optional[float] = None,
separator_height: float = 0,
refresh_control: Optional[Dict[str, Any]] = None,
list_header: Optional[Element] = None,
list_footer: Optional[Element] = None,
list_empty: Optional[Element] = None,
on_end_reached: Optional[Callable[[], None]] = None,
on_end_reached_threshold: float = 0.5,
on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
style: StyleProp = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""Virtualized list with section headers interleaved between row groups.
Flattens ``sections`` into a single virtualized sequence where each
entry is either a header or an item, then reuses the same windowing
engine as [`FlatList`][pythonnative.FlatList]; headers and items
may have different (and variable) heights.
Args:
sections: Each section is ``{"title": ..., "data": [...]}``.
render_item: ``render_item(item, item_index, section_index) ->
Element``.
render_section_header: ``render_section_header(section,
section_index) -> Element``. Defaults to a bold
[`Text`][pythonnative.Text] of the section title.
key_extractor: Stable key per item: ``key_extractor(item,
item_index) -> str``.
item_height: Uniform item extent in points, when known.
get_item_height: ``get_item_height(item, item_index,
section_index) -> float`` for exact variable extents.
estimated_item_height: Starting estimate for unmeasured rows.
section_header_height: Header extent in points, when known.
separator_height: Gap below each item, in points.
refresh_control: Optional pull-to-refresh spec.
list_header: Element rendered once before everything.
list_footer: Element rendered once after everything.
list_empty: Element rendered when there are no sections.
on_end_reached: Called near the end of the content.
on_end_reached_threshold: Distance from the end, in viewport
multiples, at which ``on_end_reached`` fires.
on_scroll: Called with the raw scroll payload.
style: Style for the outer scroll container.
ref: Optional [`Ref`][pythonnative.Ref]; receives a
[`ListController`][pythonnative.ListController] on
``ref.current`` after mount.
key: Stable identity for keyed reconciliation of the list.
Returns:
A virtualized list element (a function component instance).
"""
sections_list = list(sections or [])
sep = float(separator_height or 0.0)
def _header_el(section: Dict[str, Any], s_idx: int) -> Element:
if render_section_header is not None:
return render_section_header(section, s_idx)
return Text(str(section.get("title", "")), style={"bold": True, "padding": 8})
def _item_el(item: Any, i_idx: int, s_idx: int) -> Element:
if render_item is not None:
return render_item(item, i_idx, s_idx)
return Text(str(item))
rows: List[_RowSpec] = []
flat_index = 0
for s_idx, section in enumerate(sections_list):
def _make_header(sec: Dict[str, Any] = section, si: int = s_idx) -> Element:
return _header_el(sec, si)
rows.append(
_RowSpec(
f"__pn_sec_{s_idx}__",
_make_header,
float(section_header_height) if section_header_height is not None else None,
item=section,
index=flat_index,
)
)
flat_index += 1
for i_idx, item in enumerate(section.get("data", []) or []):
if key_extractor is not None:
try:
row_key = f"s{s_idx}:" + str(key_extractor(item, i_idx))
except Exception:
row_key = f"__pn_row_{s_idx}_{i_idx}__"
else:
row_key = f"__pn_row_{s_idx}_{i_idx}__"
def _make_item(it: Any = item, ii: int = i_idx, si: int = s_idx) -> Element:
el = _item_el(it, ii, si)
if sep > 0:
return View(el, style={"padding_bottom": sep})
return el
extent: Optional[float] = None
if get_item_height is not None:
try:
extent = float(get_item_height(item, i_idx, s_idx)) + sep
except Exception:
extent = None
elif item_height is not None:
extent = float(item_height) + sep
rows.append(_RowSpec(row_key, _make_item, extent, item=item, index=flat_index))
flat_index += 1
estimated = estimated_item_height if estimated_item_height is not None else (item_height or _DEFAULT_ROW_EXTENT)
# Same native routing as FlatList: headers and items become one
# flattened row sequence with per-row heights.
if (
_native_lists_supported()
and list_header is None
and list_footer is None
and list_empty is None
and refresh_control is None
and _all_extents_known(rows)
):
return _NativeList(
rows=rows,
on_end_reached=on_end_reached,
on_end_reached_threshold=on_end_reached_threshold,
on_scroll=on_scroll,
list_style=resolve_style(style) or None,
controller_ref=ref,
key=key,
)
return _VirtualizedList(
rows=rows,
horizontal=False,
estimated_row_extent=float(estimated) + sep,
header=list_header,
footer=list_footer,
empty=list_empty,
refresh_control=refresh_control,
on_end_reached=on_end_reached,
on_end_reached_threshold=on_end_reached_threshold,
on_scroll=on_scroll,
list_style=resolve_style(style) or None,
controller_ref=ref,
key=key,
)
# ======================================================================
# StatusBar / KeyboardAvoidingView / RefreshControl / Picker
# ======================================================================
def StatusBar(
*,
bar_style: Optional[Literal["light", "dark", "default"]] = None,
background_color: Optional[Color] = None,
hidden: Optional[bool] = None,
key: Optional[str] = None,
) -> Element:
"""Configure the device's status bar appearance.
StatusBar is a side-effect element: it doesn't render any visible
content but applies its props to the host platform's status bar.
Mount one near the top of your tree.
The ``bar_style`` parameter is named separately from the universal
``style`` kwarg (which is unused here) to avoid the conflict that
```` would create with the visual-style dict used
elsewhere.
Args:
bar_style: ``"light"`` (light icons over dark backgrounds),
``"dark"`` (dark icons over light backgrounds), or
``"default"`` (system default).
background_color: Color of the status-bar background (Android
only; iOS draws the bar transparent over your content).
hidden: When ``True``, the status bar is hidden.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"StatusBar"``.
"""
props: Dict[str, Any] = {}
if bar_style is not None:
props["bar_style"] = bar_style
if background_color is not None:
props["background_color"] = background_color
if hidden is not None:
props["hidden"] = hidden
return Element("StatusBar", props, [], key=key)
@component
def _KeyboardAvoidingContainer(**p: Any) -> Element:
"""Hook-driven body of `KeyboardAvoidingView`.
Subscribes to the platform-reported keyboard height via
[`use_keyboard_height`][pythonnative.use_keyboard_height] and
applies the shift according to ``behavior``:
- ``"padding"``: adds the shift as bottom padding, resizing the
content area (the default, and the right choice for forms
inside a full-height container).
- ``"position"``: translates the whole container upward without
resizing it (useful for pinned footers/toolbars).
- ``"height"``: shrinks the container's own height by the shift.
The resting height is captured via ``on_layout`` while the
keyboard is hidden, so flex-sized containers work too.
"""
keyboard = use_keyboard_height()
base_height, set_base_height = use_state(0.0)
behavior = p.get("behavior") or "padding"
offset = float(p.get("keyboard_vertical_offset") or 0.0)
shift = max(0.0, keyboard - offset) if keyboard > 0 else 0.0
style: Dict[str, Any] = dict(p.get("style") or {})
props: Dict[str, Any] = style
if behavior == "height":
def _record_layout(frame: Dict[str, float]) -> None:
if keyboard 0 and abs(measured - base_height) > 0.5:
set_base_height(measured)
props = dict(style)
props["on_layout"] = _record_layout
if shift > 0 and base_height > 0:
props["height"] = max(0.0, base_height - shift)
elif shift > 0:
if behavior == "position":
transform = list(style.get("transform") or [])
transform.append({"translate_y": -shift})
style["transform"] = transform
else:
style["padding_bottom"] = _numeric_edge_padding(style, "bottom") + shift
children = p.get("children") or []
return Element("KeyboardAvoidingView", props, list(children))
def KeyboardAvoidingView(
*children: Element,
behavior: Literal["padding", "position", "height"] = "padding",
keyboard_vertical_offset: float = 0.0,
style: StyleProp = None,
key: Optional[str] = None,
) -> Element:
"""Wrap content that should shift up when the keyboard is shown.
Subscribes to the platform-reported keyboard height (via
[`use_keyboard_height`][pythonnative.use_keyboard_height]
internally) and shifts its content so the focused text input stays
visible. On iOS the height comes from
``UIKeyboardWillShowNotification``; on Android from the window's
IME insets.
Args:
*children: Children rendered inside the avoiding container.
behavior: ``"padding"`` (adds bottom padding, resizing the
content), ``"position"`` (translates the container upward
without resizing), or ``"height"`` (shrinks the
container's height by the keyboard overlap, matching
React Native's ``"height"`` behavior).
keyboard_vertical_offset: Distance in layout units already
covered by other UI (e.g. a nav bar); subtracted from the
keyboard height before applying the shift.
style: Style dict (or list of dicts).
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] that renders a
``"KeyboardAvoidingView"`` container.
"""
return _KeyboardAvoidingContainer(
children=list(children),
behavior=behavior,
keyboard_vertical_offset=keyboard_vertical_offset,
style=resolve_style(style),
key=key,
)
def RefreshControl(
*,
refreshing: bool = False,
on_refresh: Optional[Callable[[], None]] = None,
tint_color: Optional[Color] = None,
) -> Dict[str, Any]:
"""Pull-to-refresh spec for [`ScrollView`][pythonnative.ScrollView] / [`FlatList`][pythonnative.FlatList].
Returns a plain dict that should be passed as the
``refresh_control=`` prop. Modeled as a dict (not an
[`Element`][pythonnative.Element]) so the host scroll container can
hold one without it appearing as a child node.
Args:
refreshing: Drive the spinner's visibility from a use_state
value.
on_refresh: Callback invoked when the user pulls down past the
threshold. Set ``refreshing`` to ``True`` for the duration
of the work, then back to ``False`` on completion.
tint_color: Color of the spinner.
Returns:
Dict suitable for the ``refresh_control`` prop on a scroll
container.
Example:
```python
import pythonnative as pn
@pn.component
def MyList():
refreshing, set_refreshing = pn.use_state(False)
def reload():
set_refreshing(True)
# ... fetch data ...
set_refreshing(False)
return pn.ScrollView(
pn.Text("Pull me!"),
refresh_control=pn.RefreshControl(
refreshing=refreshing, on_refresh=reload
),
)
```
"""
spec: Dict[str, Any] = {"refreshing": bool(refreshing)}
if on_refresh is not None:
spec["on_refresh"] = on_refresh
if tint_color is not None:
spec["tint_color"] = tint_color
return spec
def Picker(
*,
value: Any = None,
items: Optional[List[Dict[str, Any]]] = None,
on_change: Optional[Callable[[Any], None]] = None,
placeholder: str = "Select",
style: StyleProp = None,
accessibility_label: Optional[str] = None,
accessibility_hint: Optional[str] = None,
accessible: Optional[bool] = None,
accessibility_state: Optional[AccessibilityState] = None,
accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
test_id: Optional[str] = None,
ref: Optional[Ref] = None,
key: Optional[str] = None,
) -> Element:
"""A real native dropdown / select widget.
Renders a tappable trigger labelled with the selected item; the
iOS handler attaches a ``UIMenu`` (system dropdown) and the Android
handler uses a native ``Spinner``. Selecting an item fires
``on_change(value)``.
``items`` is an ordered list of ``{"value": Any, "label": str}``
entries (``label`` defaults to ``str(value)`` when omitted).
Args:
value: Currently selected value (matched against
``items[i]["value"]``).
items: Selectable options.
on_change: Callback invoked with the new value.
placeholder: Label shown when no item matches ``value``.
style: Style dict applied to the trigger.
accessibility_label: Spoken description for screen readers.
accessibility_hint: Spoken extra detail (iOS only).
accessible: Override whether the element is exposed to AT.
accessibility_state: Current widget state for assistive tech,
e.g. ``{"disabled": True, "selected": False}``. Recognized
keys: ``disabled``, ``selected``, ``checked``, ``busy``,
``expanded``.
accessibility_live_region: How AT announces dynamic changes to
this view: ``"none"``, ``"polite"``, or ``"assertive"``
(Android only).
test_id: Stable identifier for UI tests; exposed as
``resource-id`` on Android and ``accessibilityIdentifier``
on iOS.
ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
key: Stable identity for keyed reconciliation.
Returns:
An [`Element`][pythonnative.Element] of type ``"Picker"``.
"""
return _make_element(
"Picker",
style=style,
ref=ref,
key=key,
value=value,
items=list(items) if items is not None else [],
on_change=on_change,
placeholder=placeholder,
accessibility_label=accessibility_label,
accessibility_hint=accessibility_hint,
accessible=accessible,
accessibility_state=accessibility_state,
accessibility_live_region=accessibility_live_region,
test_id=test_id,
_defaults={"accessibility_role": "button"},
)