Source code for animaid.html_slider

"""HTMLSlider - A range slider widget with two-way binding."""

from __future__ import annotations

import html
import threading
from collections.abc import Callable
from typing import Self


[docs] class HTMLSlider: """A range slider widget with two-way numeric binding. The value property is automatically synced from the browser when the user moves the slider. Examples: >>> slider = HTMLSlider(min=0, max=100, value=50) >>> slider = HTMLSlider(min=0, max=1, step=0.1).on_change(handle_change) # With App - two-way binding >>> with App() as app: ... volume = HTMLSlider(min=0, max=100, value=75) ... app.add(volume) ... # Later, read the current value: ... print(volume.value) # Numeric value synced from browser """ def __init__( self, min: float = 0, max: float = 100, value: float | None = None, step: float = 1, ) -> None: """Create a slider widget. Args: min: The minimum value of the slider. max: The maximum value of the slider. value: The initial value (defaults to min). step: The step increment between values. """ self._min = min self._max = max self._step = step self._value = value if value is not None else min self._on_change: Callable[[float], None] | None = None self._styles: dict[str, str] = {} self._css_classes: list[str] = ["anim-slider"] self._anim_id: str | None = None self._lock = threading.Lock() @property def value(self) -> float: """Get the current value (thread-safe). The value is automatically synced from the browser when the user moves the slider. Returns: The current numeric value of the slider. """ with self._lock: return self._value @value.setter def value(self, new_value: float) -> None: """Set the value (thread-safe). Note: Setting the value programmatically does not automatically update the browser display. Use anim.refresh() to update. Args: new_value: The new value to set. """ with self._lock: self._value = new_value @property def min(self) -> float: """Get the minimum value.""" return self._min @property def max(self) -> float: """Get the maximum value.""" return self._max @property def step(self) -> float: """Get the step increment.""" return self._step
[docs] def on_change(self, callback: Callable[[float], None]) -> "HTMLSlider": """Register a callback for value changes. The callback is called when the user moves the slider. Args: callback: A function that takes the new value as argument. Returns: Self for method chaining. Examples: >>> def handle_change(value): ... print(f"Slider value: {value}") >>> slider = HTMLSlider(0, 100).on_change(handle_change) """ self._on_change = callback return self
[docs] def render(self) -> str: """Return HTML representation of this slider. Returns: A string containing valid HTML for the slider. """ attrs = self._build_attributes() # Add data-anim-id for event handling anim_id_attr = "" if self._anim_id: anim_id_attr = f' data-anim-id="{html.escape(self._anim_id)}"' range_attrs = ( f'min="{self._min}" max="{self._max}" ' f'step="{self._step}" value="{self._value}"' ) if attrs: return f'<input type="range" {attrs}{anim_id_attr} {range_attrs}>' return f'<input type="range"{anim_id_attr} {range_attrs}>'
def __html__(self) -> str: """Jinja2 auto-escaping protocol.""" return self.render()
[docs] def styled(self, **styles: str) -> "HTMLSlider": """Return a copy with additional inline styles. Args: **styles: CSS property-value pairs. Returns: A new instance with the combined styles. """ new_slider = HTMLSlider(self._min, self._max, self._value, self._step) new_slider._on_change = self._on_change new_slider._anim_id = self._anim_id new_slider._styles = dict(self._styles) new_slider._css_classes = list(self._css_classes) # Add new styles, converting underscores to hyphens for key, value in styles.items(): css_key = key.replace("_", "-") new_slider._styles[css_key] = value return new_slider
[docs] def add_class(self, *class_names: str) -> "HTMLSlider": """Return a copy with additional CSS classes. Args: *class_names: CSS class names to add. Returns: A new instance with the additional classes. """ new_slider = HTMLSlider(self._min, self._max, self._value, self._step) new_slider._on_change = self._on_change new_slider._anim_id = self._anim_id new_slider._styles = dict(self._styles) new_slider._css_classes = list(self._css_classes) + list(class_names) return new_slider
def _build_style_string(self) -> str: """Convert internal styles dict to CSS style attribute value.""" if not self._styles: return "" return "; ".join(f"{k}: {v}" for k, v in self._styles.items()) def _build_class_string(self) -> str: """Convert internal classes list to CSS class attribute value.""" if not self._css_classes: return "" return " ".join(self._css_classes) def _build_attributes(self) -> str: """Build the complete HTML attributes string.""" parts = [] class_str = self._build_class_string() if class_str: parts.append(f'class="{class_str}"') style_str = self._build_style_string() if style_str: parts.append(f'style="{style_str}"') return " ".join(parts) # Styled presets
[docs] def wide(self) -> "HTMLSlider": """Return a copy that expands to full width. Returns: A new instance with full width styling. """ return self.styled(width="100%", max_width="none")
[docs] def thin(self) -> "HTMLSlider": """Return a copy with a thinner slider track. Returns: A new instance with thin styling. """ return self.styled(height="4px")
[docs] def thick(self) -> "HTMLSlider": """Return a copy with a thicker slider track. Returns: A new instance with thick styling. """ return self.styled(height="10px")