Source code for build_tools.tui_common.controls.sliders

"""
Float slider control widget.

This module provides the FloatSlider widget for float parameter control
with keyboard and mouse support.

**Features:**

- Keyboard navigation: ``+``/``-`` or ``j``/``k`` or arrow keys to adjust
- Configurable step size and decimal precision
- Optional suffix text display
- Posts ``Changed`` message when value updates
- Focusable with visual feedback

**Example Usage:**

.. code-block:: python

    from build_tools.tui_common.controls import FloatSlider
    from textual import on

    class MyApp(App):
        def compose(self) -> ComposeResult:
            yield FloatSlider(
                label="Temperature",
                value=0.5,
                min_val=0.0,
                max_val=1.0,
                step=0.1,
                precision=2,
                suffix="bias",
                id="temperature-slider",
            )

        @on(FloatSlider.Changed)
        def on_slider_changed(self, event: FloatSlider.Changed) -> None:
            print(f"Slider {event.widget_id} = {event.value}")
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from textual.message import Message
from textual.widgets import Label, Static

if TYPE_CHECKING:
    from textual.app import ComposeResult


[docs] class FloatSlider(Static): """ Float slider widget with keyboard and mouse support. A horizontal widget displaying a label, current value in brackets, and optional suffix. Users can adjust the value using keyboard shortcuts while the widget has focus. Attributes: label_text: Display label shown before the value value: Current float value (clamped to min/max range) min_val: Minimum allowed value max_val: Maximum allowed value step: Amount to increment/decrement per keypress precision: Number of decimal places to display suffix: Optional text shown after the value Keybindings: - ``+`` or ``=`` or ``j`` or ``down``: Increment value by step - ``-`` or ``_`` or ``k`` or ``up``: Decrement value by step Messages: - :class:`Changed`: Posted when value changes, includes new value and widget ID CSS Classes: - ``.slider-label``: Label element (width: 15, right-aligned) - ``.slider-value``: Value display (width: 8, centered, highlighted on focus) - ``.slider-suffix``: Suffix text (auto width, muted color) """ # ------------------------------------------------------------------------- # Widget-level bindings for parameter adjustment # These don't interfere with app-level bindings (q, a, v, etc.) # ------------------------------------------------------------------------- BINDINGS = [ ("+", "increment", "Increment"), ("=", "increment", "Increment"), ("j", "increment", "Increment"), ("down", "increment", "Increment"), ("-", "decrement", "Decrement"), ("_", "decrement", "Decrement"), ("k", "decrement", "Decrement"), ("up", "decrement", "Decrement"), ]
[docs] class Changed(Message): """ Message posted when the slider value changes. Attributes: value: The new float value after the change widget_id: The ID of the widget that posted this message, or None """
[docs] def __init__(self, value: float, widget_id: str | None) -> None: """ Initialize the Changed message. Args: value: The new slider value widget_id: ID of the slider widget that changed """ super().__init__() self.value = value self.widget_id = widget_id
# ------------------------------------------------------------------------- # Default styling using Textual's CSS variables for theme compatibility # ------------------------------------------------------------------------- DEFAULT_CSS = """ FloatSlider { layout: horizontal; height: 1; width: 100%; } FloatSlider .slider-label { width: 15; text-align: right; padding-right: 1; } FloatSlider .slider-value { width: 8; text-align: center; background: $boost; } FloatSlider:focus .slider-value { background: $accent; text-style: bold; } FloatSlider .slider-suffix { width: auto; padding-left: 1; color: $text-muted; } """
[docs] def __init__( self, label: str, value: float, min_val: float, max_val: float, step: float = 0.1, precision: int = 1, suffix: str | None = None, *args, **kwargs, ) -> None: """ Initialize float slider. Args: label: Display label shown before the value value: Initial value (will be clamped to min/max range) min_val: Minimum allowed value max_val: Maximum allowed value step: Increment/decrement step size (default: 0.1) precision: Number of decimal places to display (default: 1) suffix: Optional static suffix text to display after value *args: Additional positional arguments passed to Static **kwargs: Additional keyword arguments passed to Static """ super().__init__(*args, **kwargs) self.label_text = label # Clamp initial value to valid range self.value = max(min_val, min(value, max_val)) self.min_val = min_val self.max_val = max_val self.step = step self.precision = precision self.suffix = suffix or ""
[docs] def compose(self) -> ComposeResult: """ Create the slider layout. Layout: [Label:] [value] [suffix] Yields: Label widgets for label, value display, and suffix """ yield Label(f"{self.label_text}:", classes="slider-label") # Format value with specified precision in brackets format_str = f"[{{:.{self.precision}f}}]" yield Label( format_str.format(self.value), classes="slider-value", id="value-display", ) yield Label(self.suffix, classes="slider-suffix")
[docs] def on_mount(self) -> None: """ Configure widget after mounting. Makes the widget focusable for keyboard navigation. Focus state is used to highlight the value display. """ self.can_focus = True
[docs] def action_increment(self) -> None: """ Increment value by step, clamped to max. Only posts Changed message if value actually changed. """ old_value = self.value self.value = min(self.value + self.step, self.max_val) if self.value != old_value: self._update_display() self.post_message(self.Changed(self.value, self.id))
[docs] def action_decrement(self) -> None: """ Decrement value by step, clamped to min. Only posts Changed message if value actually changed. """ old_value = self.value self.value = max(self.value - self.step, self.min_val) if self.value != old_value: self._update_display() self.post_message(self.Changed(self.value, self.id))
[docs] def set_value(self, value: float) -> None: """ Set value programmatically, clamped to range. Use this method to update the slider value from code. Posts Changed message if value actually changed. Args: value: New value (will be clamped to min/max range) """ old_value = self.value self.value = max(self.min_val, min(value, self.max_val)) if self.value != old_value: self._update_display() self.post_message(self.Changed(self.value, self.id))
def _update_display(self) -> None: """ Update the displayed value label. Called after value changes to sync the UI. Silently handles cases where widget isn't mounted yet. """ try: display = self.query_one("#value-display", Label) format_str = f"[{{:.{self.precision}f}}]" display.update(format_str.format(self.value)) except Exception: # nosec B110 - Widget may not be mounted yet pass