pytermgui.widgets.interactive.slider

This module contains the Slider class.

View Source
"""This module contains the `Slider` class."""

from __future__ import annotations
from typing import Any, Callable

from ...ansi_interface import MouseAction, MouseEvent
from ...input import keys
from ...helpers import real_length
from .. import styles as w_styles
from ..base import Widget

FILLED_SELECTED_STYLE = w_styles.MarkupFormatter("[72]{item}")
UNFILLED_STYLE = w_styles.MarkupFormatter("[240]{item}")


class Slider(Widget):  # pylint: disable=too-many-instance-attributes
    """A Widget to display & configure scalable data.

    By default, this Widget will act like a slider you might find in a
    settings page, allowing percentage-based selection of magnitude.
    Using `WindowManager` it can even be dragged around by the user using
    the mouse.
    """

    locked: bool
    """Disallow mouse input, hide cursor and lock current state"""

    chars = {"cursor": "", "fill": "", "rail": "━", "delimiter": ["[", "]"]}

    styles = {
        "delimiter": UNFILLED_STYLE,
        "filled": w_styles.MarkupFormatter("[white]{item}"),
        "cursor": FILLED_SELECTED_STYLE,
        "filled_selected": FILLED_SELECTED_STYLE,
        "unfilled": UNFILLED_STYLE,
        "unfilled_selected": UNFILLED_STYLE,
    }

    keys = {
        "increase": {keys.RIGHT, keys.CTRL_F, "l", "+"},
        "decrease": {keys.LEFT, keys.CTRL_B, "h", "-"},
    }

    def __init__(
        self,
        onchange: Callable[[float], Any] | None = None,
        locked: bool = False,
        **attrs: Any
    ) -> None:
        """Initializes a Slider.

        Args:
            onchange: The callable called every time the value
                is updated.
            locked: Whether this Slider should accept value changes.
        """

        self._value = 0.0

        super().__init__(**attrs)
        self._selectables_length = 1

        self.is_locked = locked
        self.onchange = onchange

    @property
    def value(self) -> float:
        """Returns the value of this Slider.

        Returns:
            A floating point number between 0.0 and 1.0.
        """

        return self._value

    @value.setter
    def value(self, new: float) -> None:
        """Updates the value."""

        if self.is_locked:
            return

        self._value = max(0.0, min(new, 1.0))

        if self.onchange is not None:
            self.onchange(self._value)

    def handle_key(self, key: str) -> bool:
        """Moves the slider cursor."""

        if self.execute_binding(key):
            return True

        if key in self.keys["increase"]:
            self.value += 0.1
            return True

        if key in self.keys["decrease"]:
            self.value -= 0.1
            return True

        return False

    def handle_mouse(self, event: MouseEvent) -> bool:
        """Moves the slider cursor."""

        delimiter = self._get_char("delimiter")[0]
        if event.action in [MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG]:
            offset = event.position[0] - self.pos[0] + 1 - real_length(delimiter)
            self.value = max(0, min(offset / self.width, 1.0))
            return True

        return False

    def get_lines(self) -> list[str]:
        """Gets slider lines."""

        rail = self._get_char("rail")
        cursor = self._get_char("cursor") or rail
        delimiters = self._get_char("delimiter")

        assert isinstance(delimiters, list)
        assert isinstance(cursor, str)
        assert isinstance(rail, str)

        if self.selected_index is None:
            filled_style = self._get_style("filled")
            unfilled_style = self._get_style("unfilled")
        else:
            filled_style = self._get_style("filled_selected")
            unfilled_style = self._get_style("unfilled_selected")

        cursor = self._get_style("cursor")(cursor)
        unfilled = unfilled_style(rail)
        filled = filled_style(rail)

        for i, delimiter in enumerate(delimiters):
            delimiters[i] = self._get_style("delimiter")(delimiter)

        count = round(self.width * self.value) - 1

        chars = [delimiters[0]]
        width = self.width - real_length("".join(delimiters))
        for i in range(width):
            if i == count and not self.is_locked and self.selected_index is not None:
                chars.append(cursor)
                continue

            if i <= count:
                chars.append(filled)
                continue

            chars.append(unfilled)

        chars.append(delimiters[1])
        line = "".join(chars)
        self.width = real_length(line)

        return [line]
View Source
class Slider(Widget):  # pylint: disable=too-many-instance-attributes
    """A Widget to display & configure scalable data.

    By default, this Widget will act like a slider you might find in a
    settings page, allowing percentage-based selection of magnitude.
    Using `WindowManager` it can even be dragged around by the user using
    the mouse.
    """

    locked: bool
    """Disallow mouse input, hide cursor and lock current state"""

    chars = {"cursor": "", "fill": "", "rail": "━", "delimiter": ["[", "]"]}

    styles = {
        "delimiter": UNFILLED_STYLE,
        "filled": w_styles.MarkupFormatter("[white]{item}"),
        "cursor": FILLED_SELECTED_STYLE,
        "filled_selected": FILLED_SELECTED_STYLE,
        "unfilled": UNFILLED_STYLE,
        "unfilled_selected": UNFILLED_STYLE,
    }

    keys = {
        "increase": {keys.RIGHT, keys.CTRL_F, "l", "+"},
        "decrease": {keys.LEFT, keys.CTRL_B, "h", "-"},
    }

    def __init__(
        self,
        onchange: Callable[[float], Any] | None = None,
        locked: bool = False,
        **attrs: Any
    ) -> None:
        """Initializes a Slider.

        Args:
            onchange: The callable called every time the value
                is updated.
            locked: Whether this Slider should accept value changes.
        """

        self._value = 0.0

        super().__init__(**attrs)
        self._selectables_length = 1

        self.is_locked = locked
        self.onchange = onchange

    @property
    def value(self) -> float:
        """Returns the value of this Slider.

        Returns:
            A floating point number between 0.0 and 1.0.
        """

        return self._value

    @value.setter
    def value(self, new: float) -> None:
        """Updates the value."""

        if self.is_locked:
            return

        self._value = max(0.0, min(new, 1.0))

        if self.onchange is not None:
            self.onchange(self._value)

    def handle_key(self, key: str) -> bool:
        """Moves the slider cursor."""

        if self.execute_binding(key):
            return True

        if key in self.keys["increase"]:
            self.value += 0.1
            return True

        if key in self.keys["decrease"]:
            self.value -= 0.1
            return True

        return False

    def handle_mouse(self, event: MouseEvent) -> bool:
        """Moves the slider cursor."""

        delimiter = self._get_char("delimiter")[0]
        if event.action in [MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG]:
            offset = event.position[0] - self.pos[0] + 1 - real_length(delimiter)
            self.value = max(0, min(offset / self.width, 1.0))
            return True

        return False

    def get_lines(self) -> list[str]:
        """Gets slider lines."""

        rail = self._get_char("rail")
        cursor = self._get_char("cursor") or rail
        delimiters = self._get_char("delimiter")

        assert isinstance(delimiters, list)
        assert isinstance(cursor, str)
        assert isinstance(rail, str)

        if self.selected_index is None:
            filled_style = self._get_style("filled")
            unfilled_style = self._get_style("unfilled")
        else:
            filled_style = self._get_style("filled_selected")
            unfilled_style = self._get_style("unfilled_selected")

        cursor = self._get_style("cursor")(cursor)
        unfilled = unfilled_style(rail)
        filled = filled_style(rail)

        for i, delimiter in enumerate(delimiters):
            delimiters[i] = self._get_style("delimiter")(delimiter)

        count = round(self.width * self.value) - 1

        chars = [delimiters[0]]
        width = self.width - real_length("".join(delimiters))
        for i in range(width):
            if i == count and not self.is_locked and self.selected_index is not None:
                chars.append(cursor)
                continue

            if i <= count:
                chars.append(filled)
                continue

            chars.append(unfilled)

        chars.append(delimiters[1])
        line = "".join(chars)
        self.width = real_length(line)

        return [line]

A Widget to display & configure scalable data.

By default, this Widget will act like a slider you might find in a settings page, allowing percentage-based selection of magnitude. Using WindowManager it can even be dragged around by the user using the mouse.

#   Slider( onchange: Optional[Callable[[float], Any]] = None, locked: bool = False, **attrs: Any )
View Source
    def __init__(
        self,
        onchange: Callable[[float], Any] | None = None,
        locked: bool = False,
        **attrs: Any
    ) -> None:
        """Initializes a Slider.

        Args:
            onchange: The callable called every time the value
                is updated.
            locked: Whether this Slider should accept value changes.
        """

        self._value = 0.0

        super().__init__(**attrs)
        self._selectables_length = 1

        self.is_locked = locked
        self.onchange = onchange

Initializes a Slider.

Args
  • onchange: The callable called every time the value is updated.
  • locked: Whether this Slider should accept value changes.
#   locked: bool

Disallow mouse input, hide cursor and lock current state

#   chars: dict[str, typing.Union[typing.List[str], str]] = {'cursor': '', 'fill': '', 'rail': '━', 'delimiter': ['[', ']']}

Default characters for this class

#   styles: dict[str, typing.Callable[[int, str], str]] = {'delimiter': MarkupFormatter(markup='[240]{item}', ensure_reset=True, ensure_strip=False), 'filled': MarkupFormatter(markup='[white]{item}', ensure_reset=True, ensure_strip=False), 'cursor': MarkupFormatter(markup='[72]{item}', ensure_reset=True, ensure_strip=False), 'filled_selected': MarkupFormatter(markup='[72]{item}', ensure_reset=True, ensure_strip=False), 'unfilled': MarkupFormatter(markup='[240]{item}', ensure_reset=True, ensure_strip=False), 'unfilled_selected': MarkupFormatter(markup='[240]{item}', ensure_reset=True, ensure_strip=False)}

Default styles for this class

#   keys: dict[str, set[str]] = {'increase': {'\x1b[C', 'l', '\x06', '+'}, 'decrease': {'\x1b[D', '\x02', '-', 'h'}}

Groups of keys that are used in handle_key

#   value: float

Returns the value of this Slider.

Returns

A floating point number between 0.0 and 1.0.

#   def handle_key(self, key: str) -> bool:
View Source
    def handle_key(self, key: str) -> bool:
        """Moves the slider cursor."""

        if self.execute_binding(key):
            return True

        if key in self.keys["increase"]:
            self.value += 0.1
            return True

        if key in self.keys["decrease"]:
            self.value -= 0.1
            return True

        return False

Moves the slider cursor.

#   def handle_mouse(self, event: pytermgui.ansi_interface.MouseEvent) -> bool:
View Source
    def handle_mouse(self, event: MouseEvent) -> bool:
        """Moves the slider cursor."""

        delimiter = self._get_char("delimiter")[0]
        if event.action in [MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG]:
            offset = event.position[0] - self.pos[0] + 1 - real_length(delimiter)
            self.value = max(0, min(offset / self.width, 1.0))
            return True

        return False

Moves the slider cursor.

#   def get_lines(self) -> list[str]:
View Source
    def get_lines(self) -> list[str]:
        """Gets slider lines."""

        rail = self._get_char("rail")
        cursor = self._get_char("cursor") or rail
        delimiters = self._get_char("delimiter")

        assert isinstance(delimiters, list)
        assert isinstance(cursor, str)
        assert isinstance(rail, str)

        if self.selected_index is None:
            filled_style = self._get_style("filled")
            unfilled_style = self._get_style("unfilled")
        else:
            filled_style = self._get_style("filled_selected")
            unfilled_style = self._get_style("unfilled_selected")

        cursor = self._get_style("cursor")(cursor)
        unfilled = unfilled_style(rail)
        filled = filled_style(rail)

        for i, delimiter in enumerate(delimiters):
            delimiters[i] = self._get_style("delimiter")(delimiter)

        count = round(self.width * self.value) - 1

        chars = [delimiters[0]]
        width = self.width - real_length("".join(delimiters))
        for i in range(width):
            if i == count and not self.is_locked and self.selected_index is not None:
                chars.append(cursor)
                continue

            if i <= count:
                chars.append(filled)
                continue

            chars.append(unfilled)

        chars.append(delimiters[1])
        line = "".join(chars)
        self.width = real_length(line)

        return [line]

Gets slider lines.