[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/pythonnative/pythonnative/main/src/pythonnative/style.py [Back]  [Original]

"""The typed [`Style`][pythonnative.Style], style resolution, and style sheets.

PythonNative ships a single, fully typed [`Style`][pythonnative.Style]
``TypedDict`` that enumerates every supported style property and
constrains enum-shaped values with [`typing.Literal`][typing.Literal].
Editors and type checkers (mypy, pyright) autocomplete and validate
every key: ``flex_direction="collumn"`` is a static error, not a silent
runtime no-op.

Style values are plain dicts at runtime, so they're trivial to compose,
diff, and store. Pass one dict or a list of them (later entries win,
``None`` entries are skipped); [`resolve_style`][pythonnative.style.resolve_style]
flattens the list and resolves the ``inset*`` shorthands.

Group related styles in a [`StyleSheet`][pythonnative.StyleSheet]
namespace, and derive theme-dependent styles with
[`use_styles`][pythonnative.use_styles]:

```python
import pythonnative as pn


class Styles(pn.StyleSheet):
    container = pn.style(padding=16, gap=12)
    title = pn.style(font_size=24, font_weight="700")


pn.Column(pn.Text("Hello", style=Styles.title), style=Styles.container)
```
"""

import difflib
from typing import (
    Any,
    Dict,
    List,
    Literal,
    NotRequired,
    Optional,
    Sequence,
    Tuple,
    TypedDict,
    Union,
    Unpack,
    get_args,
)

from . import diagnostics

# ======================================================================
# Atomic value types
# ======================================================================


class DynamicColor(TypedDict):
    """A color pair resolved against the current color scheme.

    Every renderer (iOS, Android, and the browser preview) picks
    ``light`` or ``dark`` when the style is applied and switches when
    the appearance changes.
    """

    light: str
    dark: str


Color = Union[str, DynamicColor]
"""Color value: ``"#RRGGBB"``, ``"#AARRGGBB"``, any string a platform
handler recognizes (e.g., ``"red"``), or a
[`DynamicColor`][pythonnative.DynamicColor] ``{"light": ..., "dark": ...}``
pair. Strings are stored verbatim and parsed by the handler at apply-time,
so palettes from third-party libraries pass through unchanged."""

Dimension = Union[int, float, str]
"""A length value. Numbers are points/dp; strings ending in ``"%"`` are
parent-relative percentages."""


class EdgeInsets(TypedDict, total=False):
    """Per-edge spacing values used for ``padding`` and ``margin``.

    Mirrors React Native's ``EdgeInsets`` shape with PythonNative's
    convenience aliases. Any subset of keys may be supplied.
    """

    top: Dimension
    right: Dimension
    bottom: Dimension
    left: Dimension
    horizontal: Dimension
    vertical: Dimension
    all: Dimension


EdgeValue = Union[Dimension, EdgeInsets]
"""Padding/margin value: a uniform number, a ``"%"`` string, or an
[`EdgeInsets`][pythonnative.EdgeInsets] dict."""


class AccessibilityState(TypedDict, total=False):
    """Current widget state exposed to assistive technology.

    Passed via the ``accessibility_state=`` prop on interactive
    components. All keys are optional; omitted keys are treated as
    unset (not ``False``).

    Attributes:
        disabled: The widget is visible but not interactive.
        selected: The widget is currently selected (e.g. the active
            tab).
        checked: Toggle/checkbox state. ``"mixed"`` represents a
            tri-state checkbox.
        busy: The widget is loading or otherwise temporarily busy.
        expanded: A disclosure/accordion is currently expanded.
    """

    disabled: bool
    selected: bool
    checked: Union[bool, Literal["mixed"]]
    busy: bool
    expanded: bool


class AccessibilityValue(TypedDict, total=False):
    """The current value of a range-like or text-valued widget, for assistive technology.

    Passed via ``accessibility_value=`` (a plain string is also
    accepted and equivalent to ``{"text": ...}``). Use ``min``, ``max``,
    and ``now`` for sliders and progress indicators, ``text`` for
    everything else.

    Attributes:
        min: Lower bound of the range.
        max: Upper bound of the range.
        now: Current value within the range.
        text: Spoken description of the current value.
    """

    min: float
    max: float
    now: float
    text: str


class AccessibilityAction(TypedDict):
    """One custom action a screen reader may invoke on a view.

    Passed via ``accessibility_actions=[...]``; the view's
    ``on_accessibility_action`` callback receives the action ``name``.
    The standard names ``"activate"``, ``"increment"``, ``"decrement"``,
    ``"longpress"``, ``"magicTap"``, and ``"escape"`` map to the
    platform's built-in actions; any other name is a custom action
    announced with ``label``.

    Attributes:
        name: Identifier reported to ``on_accessibility_action``.
        label: Spoken description of a custom action.
    """

    name: str
    label: NotRequired[str]


ImportantForAccessibility = Literal["auto", "yes", "no", "no_hide_descendants"]
"""``important_for_accessibility`` value: whether assistive technology
sees the view (``"yes"``), skips it (``"no"``), skips it and its subtree
(``"no_hide_descendants"``), or decides itself (``"auto"``)."""


class ShadowOffset(TypedDict):
    """Shadow displacement in points."""

    width: float
    height: float


ShadowOffsetValue = Union[ShadowOffset, Tuple[float, float], List[float]]
"""``shadow_offset`` / ``text_shadow_offset`` value: an
[`ShadowOffset`][pythonnative.ShadowOffset] dict or an ``(x, y)`` pair."""


# ----------------------------------------------------------------------
# Transforms
# ----------------------------------------------------------------------


class TransformRotate(TypedDict):
    """Rotation around the z axis in degrees (numeric) or with an explicit unit suffix."""

    rotate: Union[float, str]


class TransformRotateX(TypedDict):
    """Rotation around the x axis (a 3D tilt) in degrees or with a unit suffix."""

    rotate_x: Union[float, str]


class TransformRotateY(TypedDict):
    """Rotation around the y axis (a 3D flip) in degrees or with a unit suffix."""

    rotate_y: Union[float, str]


class TransformRotateZ(TypedDict):
    """Rotation around the z axis; an explicit spelling of ``rotate``."""

    rotate_z: Union[float, str]


class TransformScale(TypedDict):
    """Uniform scale transform."""

    scale: float


class TransformScaleX(TypedDict):
    """Horizontal-only scale transform."""

    scale_x: float


class TransformScaleY(TypedDict):
    """Vertical-only scale transform."""

    scale_y: float


class TransformTranslate(TypedDict, total=False):
    """Translation transform along one or both axes."""

    translate_x: float
    translate_y: float


class TransformSkewX(TypedDict):
    """Horizontal skew in degrees or with a unit suffix."""

    skew_x: Union[float, str]


class TransformSkewY(TypedDict):
    """Vertical skew in degrees or with a unit suffix."""

    skew_y: Union[float, str]


class TransformPerspective(TypedDict):
    """Perspective distance for the 3D rotations that follow it in the list."""

    perspective: float


TransformEntry = Union[
    TransformRotate,
    TransformRotateX,
    TransformRotateY,
    TransformRotateZ,
    TransformScale,
    TransformScaleX,
    TransformScaleY,
    TransformTranslate,
    TransformSkewX,
    TransformSkewY,
    TransformPerspective,
    Dict[str, Any],
]
"""A single transform operation. Every renderer supports the whole set;
iOS composes them into a ``CATransform3D``."""

TransformSpec = Union[TransformEntry, List[TransformEntry]]
"""``transform`` style value: a single operation or an ordered list."""


# ----------------------------------------------------------------------
# Enum-shaped style values
# ----------------------------------------------------------------------

FlexDirection = Literal["row", "column", "row_reverse", "column_reverse"]
FlexWrap = Literal["nowrap", "wrap", "wrap_reverse"]
AlignContent = Literal[
    "flex_start",
    "center",
    "flex_end",
    "stretch",
    "space_between",
    "space_around",
    "space_evenly",
]
LayoutDirection = Literal["ltr", "rtl"]
JustifyContent = Literal[
    "flex_start",
    "center",
    "flex_end",
    "space_between",
    "space_around",
    "space_evenly",
    "start",
    "leading",
    "top",
    "end",
    "trailing",
    "bottom",
]
AlignItems = Literal[
    "stretch",
    "flex_start",
    "center",
    "flex_end",
    "baseline",
    "auto",
    "start",
    "leading",
    "top",
    "end",
    "trailing",
    "bottom",
    "fill",
]
AlignSelf = AlignItems
Position = Literal["relative", "absolute"]
Display = Literal["flex", "none"]
"""``display`` style value. ``"none"`` removes the node and its subtree
from layout entirely (no size, gap, or margin contribution; every frame
in the subtree is ``(0, 0, 0, 0)``)."""
Overflow = Literal["visible", "hidden", "scroll"]
TextAlign = Literal["left", "center", "right", "justify", "start", "end"]
TextDecoration = Literal["none", "underline", "line_through"]
TextTransform = Literal["none", "uppercase", "lowercase", "capitalize"]
"""``text_transform`` style value. Applied in Python before the string
reaches the native label, so measurement and rendering agree."""
MarginValue = Union[Dimension, Literal["auto"]]
"""A margin length: points, a ``"%"`` string, or ``"auto"`` to absorb free space."""
FontWeight = Literal[
    "normal",
    "bold",
    "100",
    "200",
    "300",
    "400",
    "500",
    "600",
    "700",
    "800",
    "900",
]
ScaleType = Literal["cover", "contain", "stretch", "center"]
BorderStyle = Literal["solid", "dashed", "dotted"]
"""``border_style`` style value, drawn the same way on every renderer."""
KeyboardType = Literal[
    "default",
    "email_address",
    "number_pad",
    "decimal_pad",
    "phone_pad",
    "url",
    "ascii",
    "numbers_and_punctuation",
    "web_search",
    "visible_password",
]
"""``keyboard_type`` value. ``"ascii"``, ``"numbers_and_punctuation"``,
and ``"web_search"`` are iOS keyboards (Android and the browser fall back
to the closest input type); ``"visible_password"`` is Android's
unmasked password keyboard."""
AutoCapitalize = Literal["none", "sentences", "words", "characters"]
ReturnKeyType = Literal["default", "done", "go", "next", "send", "search"]
PointerEvents = Literal["auto", "none", "box_none", "box_only"]
"""Touch-handling mode for a view and its subtree:

- ``"auto"``: the view and its children receive touches (default).
- ``"none"``: neither the view nor its children receive touches;
  touches pass through to whatever is underneath.
- ``"box_none"``: the view itself ignores touches but its children
  still receive them (decorative overlays with interactive content).
- ``"box_only"``: the view receives touches but its children do not.
"""


# ======================================================================
# Style TypedDict
# ======================================================================


class Style(TypedDict, total=False):
    """Statically-typed style dictionary.

    Lists every style property recognized by the built-in components
    plus their layout engine, with enum-shaped values constrained via
    [`Literal`][typing.Literal]. ``Style`` is a `total=False` TypedDict
    so any subset of keys is valid at construction time.

    Custom native components may accept additional, unlisted keys;
    they are ignored by the built-in handlers but flow through the
    reconciler unmodified, so third-party handlers can read them.

    Example:
        ```python
        import pythonnative as pn

        title: pn.Style = {
            "font_size": 24,
            "color": "#0A84FF",
            "text_align": "center",
        }

        pn.Text("Hello", style=title)
        ```
    """

    # --- Layout: sizing ---
    width: Dimension
    height: Dimension
    min_width: Dimension
    max_width: Dimension
    min_height: Dimension
    max_height: Dimension
    aspect_ratio: float

    # --- Layout: flex ---
    flex: float
    flex_grow: float
    flex_shrink: float
    flex_basis: Dimension
    flex_direction: FlexDirection
    flex_wrap: FlexWrap
    justify_content: JustifyContent
    align_items: AlignItems
    align_self: AlignSelf
    align_content: AlignContent
    direction: LayoutDirection
    display: Display

    # --- Layout: position ---
    position: Position
    top: Dimension
    right: Dimension
    bottom: Dimension
    left: Dimension
    start: Dimension
    end: Dimension
    # Shorthands resolved by ``resolve_style`` into the four edges above;
    # explicit ``top`` / ``right`` / ``bottom`` / ``left`` keys win.
    inset: Dimension
    inset_horizontal: Dimension
    inset_vertical: Dimension

    # --- Layout: spacing ---
    padding: EdgeValue
    padding_top: Dimension
    padding_bottom: Dimension
    padding_left: Dimension
    padding_right: Dimension
    padding_start: Dimension
    padding_end: Dimension
    padding_horizontal: Dimension
    padding_vertical: Dimension
    margin: Union[MarginValue, EdgeInsets]
    margin_top: MarginValue
    margin_bottom: MarginValue
    margin_left: MarginValue
    margin_right: MarginValue
    margin_start: MarginValue
    margin_end: MarginValue
    margin_horizontal: MarginValue
    margin_vertical: MarginValue
    gap: float
    row_gap: float
    column_gap: float

    # --- Visual: clipping & overflow ---
    overflow: Overflow

    # --- Visual: colors ---
    background_color: Color
    color: Color
    border_color: Color
    placeholder_color: Color
    tint_color: Color

    # --- Visual: borders ---
    border_width: float
    border_style: BorderStyle
    border_radius: float
    border_top_left_radius: float
    border_top_right_radius: float
    border_bottom_left_radius: float
    border_bottom_right_radius: float
    border_top_width: float
    border_right_width: float
    border_bottom_width: float
    border_left_width: float
    border_top_color: Color
    border_right_color: Color
    border_bottom_color: Color
    border_left_color: Color

    # --- Visual: typography ---
    font_size: float
    font_family: str
    font_weight: FontWeight
    bold: bool
    italic: bool
    text_align: TextAlign
    text_decoration: TextDecoration
    text_transform: TextTransform
    line_height: float
    letter_spacing: float
    max_lines: int
    text_shadow_color: Color
    text_shadow_offset: ShadowOffsetValue
    text_shadow_radius: float

    # --- Visual: shadows / effects ---
    # Shadows follow React Native's platform split: the ``shadow_*``
    # keys draw on iOS and in the browser preview, ``elevation`` draws
    # Android's material shadow. Each platform ignores the other's keys.
    shadow_color: Color
    shadow_offset: ShadowOffsetValue
    shadow_opacity: float
    shadow_radius: float
    elevation: float
    opacity: float
    transform: TransformSpec

    # --- Interaction & stacking ---
    z_index: int
    pointer_events: PointerEvents


StyleProp = Union[Style, Sequence[Optional[Style]], None]
"""Public type for the ``style`` parameter on every component factory.

Accepts a [`Style`][pythonnative.Style] TypedDict, a sequence of them
with ``None`` entries skipped, or ``None``. ``Style`` is a ``TypedDict``,
so a plain dict literal type-checks against it and still works at
runtime."""


# ======================================================================
# Runtime helpers
# ======================================================================


def style(**properties: Unpack[Style]) -> Style:
    """Construct a [`Style`][pythonnative.Style] from keyword arguments.

    Equivalent to ``Style(...)`` but reads more naturally inside
    expressions, and typed with ``Unpack[Style]`` so a type checker
    rejects a misspelled key such as ``pn.style(colour="red")``:

    ```python
    pn.View(child, style=pn.style(padding=16, background_color="#fff"))
    ```

    At runtime the keys aren't checked here; the factories warn once
    per unknown key in dev mode through
    [`validate_style_keys`][pythonnative.style.validate_style_keys].

    Args:
        **properties: Style key/value pairs.

    Returns:
        A fresh ``Style`` dict containing the supplied entries.
    """
    return properties


_INSET_SHORTHANDS: Dict[str, Tuple[str, ...]] = {
    "inset": ("top", "right", "bottom", "left"),
    "inset_horizontal": ("left", "right"),
    "inset_vertical": ("top", "bottom"),
}
"""Position shorthands and the edges each one fills, least specific first."""


def _expand_insets(result: Dict[str, Any]) -> None:
    """Replace ``inset*`` shorthands with the four edge keys, in place.

    Mirrors the padding vocabulary: ``inset`` fills every edge, the axis
    shorthands override it, and an explicit ``top`` / ``right`` /
    ``bottom`` / ``left`` wins over both, regardless of key order. The
    wire only ever carries the four edges.
    """
    explicit: Dict[str, Any] = {edge: result[edge] for edge in ("top", "right", "bottom", "left") if edge in result}
    for shorthand, edges in _INSET_SHORTHANDS.items():
        if shorthand not in result:
            continue
        value = result.pop(shorthand)
        if value is None:
            continue
        for edge in edges:
            result[edge] = value
    result.update(explicit)


def resolve_style(value: StyleProp) -> Dict[str, Any]:
    """Flatten a `style` prop into a single dict.

    Accepts ``None``, a single dict, or a sequence of dicts (later
    entries override earlier ones, mirroring React Native's array-style
    pattern), then resolves the ``inset``, ``inset_horizontal``, and
    ``inset_vertical`` shorthands into ``top`` / ``right`` / ``bottom``
    / ``left``. Used by every built-in element factory in
    `pythonnative.components`.

    Args:
        value: The raw value of the component's `style` argument.

    Returns:
        A flat dict suitable for the native handler. Always a fresh
        dict, never the input.
    """
    if value is None:
        return {}
    if isinstance(value, dict):
        result: Dict[str, Any] = dict(value)
    else:
        result = {}
        for entry in value:
            if entry:
                result.update(entry)
    if result.keys() & _INSET_SHORTHANDS.keys():
        _expand_insets(result)
    return result


_KNOWN_STYLE_KEYS = frozenset(Style.__annotations__)
"""Every key declared on the [`Style`][pythonnative.Style] TypedDict."""

_STYLE_VALUE_CHOICES: Dict[str, frozenset] = {
    "display": frozenset(get_args(Display)),
    "position": frozenset(get_args(Position)),
    "direction": frozenset(get_args(LayoutDirection)),
    "text_decoration": frozenset(get_args(TextDecoration)),
    "text_transform": frozenset(get_args(TextTransform)),
    "pointer_events": frozenset(get_args(PointerEvents)),
    "border_style": frozenset(get_args(BorderStyle)),
}
"""Keys whose string values are checked against their ``Literal`` choices
in dev mode. Only keys whose handlers accept exactly the declared
literals are listed, so friendly aliases (``align_items: "leading"``,
``font_weight: "semibold"``) never trigger false positives."""


def _is_number(value: Any) -> bool:
    return isinstance(value, (int, float)) and not isinstance(value, bool)


def _text_shadow_offset_ok(value: Any) -> bool:
    if isinstance(value, dict):
        return all(_is_number(value.get(k, 0)) for k in ("width", "height"))
    if isinstance(value, (tuple, list)) and len(value) == 2:
        return all(_is_number(v) for v in value)
    return False


def _bad_value_message(key: str, value: Any) -> Optional[str]:
    """Return a description of why ``value`` is invalid for ``key``, or ``None``."""
    choices = _STYLE_VALUE_CHOICES.get(key)
    if choices is not None:
        if not isinstance(value, str) or value not in choices:
            allowed = ", ".join(repr(c) for c in sorted(choices))
            return f"Invalid value {value!r} for style key {key!r}; expected one of {allowed}."
        return None
    if key == "text_shadow_offset" and not _text_shadow_offset_ok(value):
        return (
            f"Invalid value {value!r} for style key 'text_shadow_offset'; "
            "expected {'width': x, 'height': y} or an (x, y) pair."
        )
    if key == "text_shadow_radius" and (not _is_number(value) or value < 0):
        return f"Invalid value {value!r} for style key 'text_shadow_radius'; expected a non-negative number."
    return None


def validate_style_keys(style_dict: Dict[str, Any], owner: str = "") -> None:
    """Warn (once per key) about style keys no built-in handler reads.

    Dev-mode only; production skips the scan entirely. Typos get a
    "did you mean" suggestion via fuzzy matching against the declared
    [`Style`][pythonnative.Style] keys. Unknown keys still flow through
    to handlers untouched, so custom components that read extra keys
    keep working (at the cost of one dev warning).

    A small set of enum-shaped keys (``display``, ``position``,
    ``direction``, ``text_decoration``, ``text_transform``,
    ``pointer_events``, ``border_style``) and the ``text_shadow_*`` keys also have their
    values checked; a bad value warns once per key/value pair and is
    otherwise passed through (the engine and handlers ignore values
    they don't recognize).

    Args:
        style_dict: The flattened style dict about to be merged into an
            element's props.
        owner: Element type name for the warning message (e.g.
            ``"Text"``).
    """
    if not diagnostics.is_dev() or not style_dict:
        return
    for key, value in style_dict.items():
        if key in _KNOWN_STYLE_KEYS:
            if value is None:
                continue
            problem = _bad_value_message(key, value)
            if problem is not None:
                diagnostics.warn_once(
                    problem + (f" (on {owner})" if owner else ""),
                    key=f"style-value:{owner}:{key}:{value!r}",
                )
            continue
        matches = difflib.get_close_matches(str(key), _KNOWN_STYLE_KEYS, n=1)
        hint = f" Did you mean {matches[0]!r}?" if matches else ""
        diagnostics.warn_once(
            f"Unknown style key {key!r}"
            + (f" on {owner}" if owner else "")
            + f".{hint} Built-in components ignore keys they don't recognize.",
            key=f"style:{owner}:{key}",
        )


# ======================================================================
# StyleSheet
# ======================================================================


class StyleSheet:
    """Base class for a namespace of named styles.

    Subclass it and assign [`Style`][pythonnative.Style] dicts as class
    attributes. Attribute access is typed, so a misspelled style name is
    a static error, and every style is checked for unknown keys once in
    dev mode:

    ```python
    class Styles(pn.StyleSheet):
        card = pn.style(padding=16, border_radius=12)
        title = pn.style(font_size=17, font_weight="600")


    pn.View(pn.Text("Hello", style=Styles.title), style=Styles.card)
    ```

    Compose styles with a list (``style=[Styles.card, {"opacity": 0.5}]``).
    For styles derived from the theme, see
    [`use_styles`][pythonnative.use_styles]. A style sheet is a
    namespace, not an object: instantiating one raises ``TypeError``.
    """

    def __init_subclass__(cls, **kwargs: Any) -> None:
        super().__init_subclass__(**kwargs)
        for name, value in vars(cls).items():
            if not name.startswith("_") and isinstance(value, dict):
                validate_style_keys(value, owner=f"{cls.__name__}.{name}")

    def __new__(cls, *args: Any, **kwargs: Any) -> "StyleSheet":
        """Refuse instantiation: a style sheet is a namespace, not an object."""
        raise TypeError(f"{cls.__name__} is a namespace of styles; use {cls.__name__}. instead of calling it")


ABSOLUTE_FILL: Style = {"position": "absolute", "top": 0, "right": 0, "bottom": 0, "left": 0}
"""A style that absolutely fills the parent, like React Native's ``StyleSheet.absoluteFill``."""


__all__ = [
    "ABSOLUTE_FILL",
    "AccessibilityAction",
    "AccessibilityState",
    "AccessibilityValue",
    "AlignItems",
    "AlignSelf",
    "AutoCapitalize",
    "BorderStyle",
    "Color",
    "Dimension",
    "Display",
    "DynamicColor",
    "EdgeInsets",
    "EdgeValue",
    "FlexDirection",
    "FontWeight",
    "ImportantForAccessibility",
    "JustifyContent",
    "KeyboardType",
    "MarginValue",
    "Overflow",
    "PointerEvents",
    "Position",
    "ReturnKeyType",
    "ScaleType",
    "ShadowOffset",
    "ShadowOffsetValue",
    "Style",
    "StyleProp",
    "StyleSheet",
    "TextAlign",
    "TextDecoration",
    "TextTransform",
    "TransformEntry",
    "TransformPerspective",
    "TransformRotate",
    "TransformRotateX",
    "TransformRotateY",
    "TransformRotateZ",
    "TransformScale",
    "TransformScaleX",
    "TransformScaleY",
    "TransformSkewX",
    "TransformSkewY",
    "TransformSpec",
    "TransformTranslate",
    "resolve_style",
    "style",
    "validate_style_keys",
]

Web Proxy Viewer  |  New URL  |  Original Page