Source code for animaid.containers.base

"""Base class for container widgets."""

from __future__ import annotations

import uuid
from typing import TYPE_CHECKING, Any

from animaid.css_types import (
    AlignItems,
    CSSValue,
    FlexWrap,
    JustifyContent,
    Size,
    Spacing,
)
from animaid.html_object import HTMLObject

if TYPE_CHECKING:
    from animaid.animate import App


def _to_css(value: object) -> str:
    """Convert a value to its CSS string representation."""
    if hasattr(value, "to_css"):
        return str(value.to_css())
    return str(value)


[docs] class HTMLContainer(HTMLObject): """Base class for all container widgets. Containers hold child elements and provide layout capabilities. They can be nested and support reactive updates when children change. Example: >>> container = HTMLContainer([HTMLString("Hello"), HTMLString("World")]) >>> container.render() '<div>...</div>' """ _styles: dict[str, str] _css_classes: list[str] _children: list[Any] _anim_id: str | None _anim: "App | None" _obs_id: str def __init__( self, children: list[Any] | None = None, **styles: str | CSSValue, ) -> None: """Create a new container. Args: children: List of child elements (HTMLObject instances or any renderable). **styles: Initial CSS styles (underscores converted to hyphens). """ self._children = list(children) if children else [] self._styles = {} self._css_classes = [] self._anim_id = None self._anim = None self._obs_id = str(uuid.uuid4()) for key, value in styles.items(): css_key = key.replace("_", "-") self._styles[css_key] = _to_css(value) def _notify(self) -> None: """Publish change notification via pypubsub.""" try: from pubsub import pub pub.sendMessage("animaid.changed", obs_id=self._obs_id) except ImportError: pass # pypubsub not installed
[docs] def render(self) -> str: """Render the container and all children. Returns: HTML string with container div and rendered children. """ children_html = self._render_children() attrs = self._build_attributes() if attrs: return f"<div {attrs}>{children_html}</div>" return f"<div>{children_html}</div>"
def _render_children(self) -> str: """Render all children to HTML. Returns: Concatenated HTML of all children. """ parts = [] for child in self._children: if hasattr(child, "render"): parts.append(child.render()) else: # Escape plain strings for safety import html parts.append(html.escape(str(child))) return "".join(parts) def __html__(self) -> str: """Jinja2 auto-escaping protocol.""" return self.render() # ========================================================================= # Child Management # =========================================================================
[docs] def append(self, child: Any) -> "HTMLContainer": """Add a child element and trigger update. Args: child: The element to add. Returns: Self for method chaining. """ self._children.append(child) self._notify() return self
[docs] def extend(self, children: list[Any]) -> "HTMLContainer": """Add multiple child elements. Args: children: List of elements to add. Returns: Self for method chaining. """ self._children.extend(children) self._notify() return self
[docs] def insert(self, index: int, child: Any) -> "HTMLContainer": """Insert a child at a specific position. Args: index: Position to insert at. child: The element to insert. Returns: Self for method chaining. """ self._children.insert(index, child) self._notify() return self
[docs] def remove(self, child: Any) -> "HTMLContainer": """Remove a child element. Args: child: The element to remove. Returns: Self for method chaining. """ self._children.remove(child) self._notify() return self
[docs] def pop(self, index: int = -1) -> Any: """Remove and return a child at the given position. Args: index: Position to remove from (default: last). Returns: The removed child element. """ child = self._children.pop(index) self._notify() return child
[docs] def clear(self) -> "HTMLContainer": """Remove all children. Returns: Self for method chaining. """ self._children.clear() self._notify() return self
@property def children(self) -> list[Any]: """Get the list of children (read-only copy).""" return list(self._children) def __len__(self) -> int: """Return the number of children.""" return len(self._children) def __iter__(self): """Iterate over children.""" return iter(self._children) def __getitem__(self, index: int) -> Any: """Get a child by index.""" return self._children[index] # ========================================================================= # Styling Methods # =========================================================================
[docs] def styled(self, **styles: str | CSSValue) -> "HTMLContainer": """Apply additional inline styles. Args: **styles: CSS property-value pairs (underscores become hyphens). Returns: Self for method chaining. """ for key, value in styles.items(): css_key = key.replace("_", "-") self._styles[css_key] = _to_css(value) self._notify() return self
[docs] def add_class(self, *class_names: str) -> "HTMLContainer": """Add CSS classes. Args: *class_names: CSS class names to add. Returns: Self for method chaining. """ for name in class_names: if name not in self._css_classes: self._css_classes.append(name) self._notify() return self
[docs] def remove_class(self, *class_names: str) -> "HTMLContainer": """Remove CSS classes. Args: *class_names: CSS class names to remove. Returns: Self for method chaining. """ for name in class_names: if name in self._css_classes: self._css_classes.remove(name) self._notify() return self
# ========================================================================= # Common Layout Methods # =========================================================================
[docs] def gap(self, size: Size | str | int) -> "HTMLContainer": """Set the gap between child elements. Args: size: Gap size (Size, CSS string like "10px", or int pixels). Returns: Self for method chaining. """ if isinstance(size, int): size = Size.px(size) self._styles["gap"] = _to_css(size) self._notify() return self
[docs] def padding(self, size: Spacing | Size | str | int) -> "HTMLContainer": """Set internal padding. Args: size: Padding size (Spacing, Size, CSS string, or int pixels). Returns: Self for method chaining. """ if isinstance(size, int): size = Size.px(size) self._styles["padding"] = _to_css(size) self._notify() return self
[docs] def margin(self, size: Spacing | Size | str | int) -> "HTMLContainer": """Set external margin. Args: size: Margin size (Spacing, Size, CSS string, or int pixels). Returns: Self for method chaining. """ if isinstance(size, int): size = Size.px(size) self._styles["margin"] = _to_css(size) self._notify() return self
[docs] def width(self, size: Size | str | int) -> "HTMLContainer": """Set container width. Args: size: Width (Size, CSS string, or int pixels). Returns: Self for method chaining. """ if isinstance(size, int): size = Size.px(size) self._styles["width"] = _to_css(size) self._notify() return self
[docs] def height(self, size: Size | str | int) -> "HTMLContainer": """Set container height. Args: size: Height (Size, CSS string, or int pixels). Returns: Self for method chaining. """ if isinstance(size, int): size = Size.px(size) self._styles["height"] = _to_css(size) self._notify() return self
[docs] def max_width(self, size: Size | str | int) -> "HTMLContainer": """Set maximum container width. Args: size: Maximum width (Size, CSS string, or int pixels). Returns: Self for method chaining. """ if isinstance(size, int): size = Size.px(size) self._styles["max-width"] = _to_css(size) self._notify() return self
[docs] def max_height(self, size: Size | str | int) -> "HTMLContainer": """Set maximum container height. Args: size: Maximum height (Size, CSS string, or int pixels). Returns: Self for method chaining. """ if isinstance(size, int): size = Size.px(size) self._styles["max-height"] = _to_css(size) self._notify() return self
[docs] def min_width(self, size: Size | str | int) -> "HTMLContainer": """Set minimum container width. Args: size: Minimum width (Size, CSS string, or int pixels). Returns: Self for method chaining. """ if isinstance(size, int): size = Size.px(size) self._styles["min-width"] = _to_css(size) self._notify() return self
[docs] def min_height(self, size: Size | str | int) -> "HTMLContainer": """Set minimum container height. Args: size: Minimum height (Size, CSS string, or int pixels). Returns: Self for method chaining. """ if isinstance(size, int): size = Size.px(size) self._styles["min-height"] = _to_css(size) self._notify() return self
# ========================================================================= # Full-Window Layout Methods # =========================================================================
[docs] def full_width(self) -> "HTMLContainer": """Expand container to fill the full width of its parent. Returns: Self for method chaining. Example: >>> row = HTMLRow([...]).full_width() """ self._styles["width"] = "100%" self._notify() return self
[docs] def full_height(self) -> "HTMLContainer": """Expand container to fill the full viewport height. Returns: Self for method chaining. Example: >>> column = HTMLColumn([...]).full_height() """ self._styles["min-height"] = "100vh" self._notify() return self
[docs] def full_screen(self) -> "HTMLContainer": """Expand container to fill the entire viewport (width and height). Returns: Self for method chaining. Example: >>> layout = HTMLColumn([...]).full_screen() """ self._styles["width"] = "100%" self._styles["min-height"] = "100vh" self._notify() return self
[docs] def expand(self) -> "HTMLContainer": """Make this container expand to fill available space in a flex parent. Use this on a child container to make it grow and fill remaining space. Returns: Self for method chaining. Example: >>> # Header, expandable content, footer layout >>> layout = HTMLColumn([ ... HTMLString("Header"), ... HTMLColumn([HTMLString("Content")]).expand(), ... HTMLString("Footer"), ... ]).full_height() """ self._styles["flex"] = "1" self._notify() return self