Module textual.cli.previews.easing

Expand source code
from __future__ import annotations

from rich.console import RenderableType
from textual._easing import EASING
from textual.app import App, ComposeResult
from textual.cli.previews.borders import TEXT
from textual.containers import Container, Horizontal, Vertical
from textual.reactive import Reactive
from textual.scrollbar import ScrollBarRender
from textual.widget import Widget
from textual.widgets import Button, Footer, Static, Input

VIRTUAL_SIZE = 100
WINDOW_SIZE = 10
START_POSITION = 0.0
END_POSITION = float(VIRTUAL_SIZE - WINDOW_SIZE)


class EasingButtons(Widget):
    def compose(self) -> ComposeResult:
        for easing in sorted(EASING, reverse=True):
            yield Button(easing, id=easing)


class Bar(Widget):
    position = Reactive.init(START_POSITION)
    animation_running = Reactive(False)

    DEFAULT_CSS = """
    
    Bar {
        background: $surface;
        color: $error;
    }

    Bar.-active {
        background: $surface;
        color: $success;
    }
    
    """

    def watch_animation_running(self, running: bool) -> None:
        self.set_class(running, "-active")

    def render(self) -> RenderableType:
        return ScrollBarRender(
            virtual_size=VIRTUAL_SIZE,
            window_size=WINDOW_SIZE,
            position=self.position,
            style=self.rich_style,
        )


class EasingApp(App):
    position = Reactive.init(START_POSITION)
    duration = Reactive.var(1.0)

    def on_load(self):
        self.bind(
            "ctrl+p", "focus('duration-input')", description="Focus: Duration Input"
        )
        self.bind("ctrl+b", "toggle_dark", description="Toggle Dark")

    def compose(self) -> ComposeResult:
        self.animated_bar = Bar()
        self.animated_bar.position = START_POSITION
        duration_input = Input("1.0", placeholder="Duration", id="duration-input")

        self.opacity_widget = Static(
            f"[b]Welcome to Textual![/]\n\n{TEXT}", id="opacity-widget"
        )

        yield EasingButtons()
        yield Vertical(
            Horizontal(
                Static("Animation Duration:", id="label"), duration_input, id="inputs"
            ),
            Horizontal(
                self.animated_bar,
                Container(self.opacity_widget, id="other"),
            ),
            Footer(),
        )

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.bell()
        self.animated_bar.animation_running = True

        def _animation_complete():
            self.animated_bar.animation_running = False

        target_position = (
            END_POSITION if self.position == START_POSITION else START_POSITION
        )
        self.animate(
            "position",
            value=target_position,
            final_value=target_position,
            duration=self.duration,
            easing=event.button.id,
            on_complete=_animation_complete,
        )

    def watch_position(self, value: int):
        self.animated_bar.position = value
        self.opacity_widget.styles.opacity = 1 - value / END_POSITION

    def on_input_changed(self, event: Input.Changed):
        if event.sender.id == "duration-input":
            new_duration = _try_float(event.value)
            if new_duration is not None:
                self.duration = new_duration

    def action_toggle_dark(self):
        self.dark = not self.dark


def _try_float(string: str) -> float | None:
    try:
        return float(string)
    except ValueError:
        return None


app = EasingApp(css_path="easing.css")
if __name__ == "__main__":
    app.run()

Classes

class Bar (*children: Widget, name: str | None = None, id: str | None = None, classes: str | None = None)

A Widget is the base class for Textual widgets.

See also [static][textual.widgets._static.Static] for starting point for your own widgets.

Expand source code
class Bar(Widget):
    position = Reactive.init(START_POSITION)
    animation_running = Reactive(False)

    DEFAULT_CSS = """
    
    Bar {
        background: $surface;
        color: $error;
    }

    Bar.-active {
        background: $surface;
        color: $success;
    }
    
    """

    def watch_animation_running(self, running: bool) -> None:
        self.set_class(running, "-active")

    def render(self) -> RenderableType:
        return ScrollBarRender(
            virtual_size=VIRTUAL_SIZE,
            window_size=WINDOW_SIZE,
            position=self.position,
            style=self.rich_style,
        )

Ancestors

Class variables

var COMPONENT_CLASSES : ClassVar[set[str]]
var DEFAULT_CSS

Instance variables

var animation_running : ReactiveType

Reactive descriptor.

Args

default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
layout : bool, optional
Perform a layout on change. Defaults to False.
repaint : bool, optional
Perform a repaint on change. Defaults to True.
init : bool, optional
Call watchers on initialize (post mount). Defaults to False.
Expand source code
def __get__(self, obj: Reactable, obj_type: type[object]) -> ReactiveType:
    value: _NotSet | ReactiveType = getattr(obj, self.internal_name, _NOT_SET)
    if isinstance(value, _NotSet):
        # No value present, we need to set the default
        init_name = f"_default_{self.name}"
        default = getattr(obj, init_name)
        default_value = default() if callable(default) else default
        # Set and return the value
        setattr(obj, self.internal_name, default_value)
        if self._init:
            self._check_watchers(obj, self.name, default_value, first_set=True)
        return default_value
    return value
var position : ReactiveType

Reactive descriptor.

Args

default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
layout : bool, optional
Perform a layout on change. Defaults to False.
repaint : bool, optional
Perform a repaint on change. Defaults to True.
init : bool, optional
Call watchers on initialize (post mount). Defaults to False.
Expand source code
def __get__(self, obj: Reactable, obj_type: type[object]) -> ReactiveType:
    value: _NotSet | ReactiveType = getattr(obj, self.internal_name, _NOT_SET)
    if isinstance(value, _NotSet):
        # No value present, we need to set the default
        init_name = f"_default_{self.name}"
        default = getattr(obj, init_name)
        default_value = default() if callable(default) else default
        # Set and return the value
        setattr(obj, self.internal_name, default_value)
        if self._init:
            self._check_watchers(obj, self.name, default_value, first_set=True)
        return default_value
    return value

Methods

def watch_animation_running(self, running: bool) ‑> None
Expand source code
def watch_animation_running(self, running: bool) -> None:
    self.set_class(running, "-active")

Inherited members

class EasingApp (driver_class: Type[Driver] | None = None, css_path: CSSPathType = None, watch_css: bool = False)

The base class for Textual Applications.

Args

driver_class (Type[Driver] | None, optional): Driver class or None to auto-detect. Defaults to None.
title (str | None, optional): Title of the application. If None, the title is set to the name of the App subclass. Defaults to None.
css_path (str | PurePath | None, optional): Path to CSS or None for no CSS file. Defaults to None.
watch_css : bool, optional
Watch CSS for changes. Defaults to False.
Expand source code
class EasingApp(App):
    position = Reactive.init(START_POSITION)
    duration = Reactive.var(1.0)

    def on_load(self):
        self.bind(
            "ctrl+p", "focus('duration-input')", description="Focus: Duration Input"
        )
        self.bind("ctrl+b", "toggle_dark", description="Toggle Dark")

    def compose(self) -> ComposeResult:
        self.animated_bar = Bar()
        self.animated_bar.position = START_POSITION
        duration_input = Input("1.0", placeholder="Duration", id="duration-input")

        self.opacity_widget = Static(
            f"[b]Welcome to Textual![/]\n\n{TEXT}", id="opacity-widget"
        )

        yield EasingButtons()
        yield Vertical(
            Horizontal(
                Static("Animation Duration:", id="label"), duration_input, id="inputs"
            ),
            Horizontal(
                self.animated_bar,
                Container(self.opacity_widget, id="other"),
            ),
            Footer(),
        )

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.bell()
        self.animated_bar.animation_running = True

        def _animation_complete():
            self.animated_bar.animation_running = False

        target_position = (
            END_POSITION if self.position == START_POSITION else START_POSITION
        )
        self.animate(
            "position",
            value=target_position,
            final_value=target_position,
            duration=self.duration,
            easing=event.button.id,
            on_complete=_animation_complete,
        )

    def watch_position(self, value: int):
        self.animated_bar.position = value
        self.opacity_widget.styles.opacity = 1 - value / END_POSITION

    def on_input_changed(self, event: Input.Changed):
        if event.sender.id == "duration-input":
            new_duration = _try_float(event.value)
            if new_duration is not None:
                self.duration = new_duration

    def action_toggle_dark(self):
        self.dark = not self.dark

Ancestors

Class variables

var CSS_PATH : CSSPathType
var SCREENS : dict[str, Screen]
var SUB_TITLE : str | None
var TITLE : str | None

Instance variables

var duration : ReactiveType

Reactive descriptor.

Args

default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
layout : bool, optional
Perform a layout on change. Defaults to False.
repaint : bool, optional
Perform a repaint on change. Defaults to True.
init : bool, optional
Call watchers on initialize (post mount). Defaults to False.
Expand source code
def __get__(self, obj: Reactable, obj_type: type[object]) -> ReactiveType:
    value: _NotSet | ReactiveType = getattr(obj, self.internal_name, _NOT_SET)
    if isinstance(value, _NotSet):
        # No value present, we need to set the default
        init_name = f"_default_{self.name}"
        default = getattr(obj, init_name)
        default_value = default() if callable(default) else default
        # Set and return the value
        setattr(obj, self.internal_name, default_value)
        if self._init:
            self._check_watchers(obj, self.name, default_value, first_set=True)
        return default_value
    return value
var position : ReactiveType

Reactive descriptor.

Args

default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
layout : bool, optional
Perform a layout on change. Defaults to False.
repaint : bool, optional
Perform a repaint on change. Defaults to True.
init : bool, optional
Call watchers on initialize (post mount). Defaults to False.
Expand source code
def __get__(self, obj: Reactable, obj_type: type[object]) -> ReactiveType:
    value: _NotSet | ReactiveType = getattr(obj, self.internal_name, _NOT_SET)
    if isinstance(value, _NotSet):
        # No value present, we need to set the default
        init_name = f"_default_{self.name}"
        default = getattr(obj, init_name)
        default_value = default() if callable(default) else default
        # Set and return the value
        setattr(obj, self.internal_name, default_value)
        if self._init:
            self._check_watchers(obj, self.name, default_value, first_set=True)
        return default_value
    return value

Methods

def on_button_pressed(self, event: Button.Pressed) ‑> None
Expand source code
def on_button_pressed(self, event: Button.Pressed) -> None:
    self.bell()
    self.animated_bar.animation_running = True

    def _animation_complete():
        self.animated_bar.animation_running = False

    target_position = (
        END_POSITION if self.position == START_POSITION else START_POSITION
    )
    self.animate(
        "position",
        value=target_position,
        final_value=target_position,
        duration=self.duration,
        easing=event.button.id,
        on_complete=_animation_complete,
    )
def on_input_changed(self, event: Input.Changed)
Expand source code
def on_input_changed(self, event: Input.Changed):
    if event.sender.id == "duration-input":
        new_duration = _try_float(event.value)
        if new_duration is not None:
            self.duration = new_duration
def on_load(self)
Expand source code
def on_load(self):
    self.bind(
        "ctrl+p", "focus('duration-input')", description="Focus: Duration Input"
    )
    self.bind("ctrl+b", "toggle_dark", description="Toggle Dark")
def watch_position(self, value: int)
Expand source code
def watch_position(self, value: int):
    self.animated_bar.position = value
    self.opacity_widget.styles.opacity = 1 - value / END_POSITION

Inherited members

class EasingButtons (*children: Widget, name: str | None = None, id: str | None = None, classes: str | None = None)

A Widget is the base class for Textual widgets.

See also [static][textual.widgets._static.Static] for starting point for your own widgets.

Expand source code
class EasingButtons(Widget):
    def compose(self) -> ComposeResult:
        for easing in sorted(EASING, reverse=True):
            yield Button(easing, id=easing)

Ancestors

Class variables

var COMPONENT_CLASSES : ClassVar[set[str]]

Inherited members