Merge pull request #740 from Textualize/easing-examples

Easing preview
This commit is contained in:
Will McGugan
2022-09-08 11:37:55 +01:00
committed by GitHub
7 changed files with 173 additions and 8 deletions

View File

@@ -9,6 +9,7 @@ class JustABox(App):
yield Static("Hello, world!", classes="box1")
app = JustABox(watch_css=True, css_path="../darren/just_a_box.css")
if __name__ == "__main__":
app = JustABox(css_path="../darren/just_a_box.css", watch_css=True)
app.run()

View File

@@ -172,6 +172,14 @@ def run_app(import_name: str, dev: bool, press: str) -> None:
@run.command("borders")
def borders():
"""Explore the border styles available in Textual."""
from ..devtools import borders
from textual.cli.previews import borders
borders.app.run()
@run.command("easing")
def easing():
"""Explore the animation easing functions available in Textual."""
from textual.cli.previews import easing
easing.app.run()

View File

View File

@@ -0,0 +1,40 @@
EasingButtons > Button {
width: 100%;
}
EasingButtons {
dock: left;
overflow: auto auto;
width: 20;
}
#bar-container {
content-align: center middle;
}
#duration-input {
width: 30;
}
#inputs {
padding: 1;
height: auto;
dock: top;
}
Bar {
width: 1fr;
}
#other {
width: 1fr;
background: #555555;
padding: 1;
height: 100%;
border-left: vkey $background;
}
#opacity-widget {
padding: 1;
background: $panel;
border: wide #969696;
}

View File

@@ -0,0 +1,113 @@
from __future__ import annotations
from rich.console import RenderableType
from textual import layout
from textual._easing import EASING
from textual.app import ComposeResult, App
from textual.cli.previews.borders import TEXT
from textual.reactive import Reactive
from textual.scrollbar import ScrollBarRender
from textual.widget import Widget
from textual.widgets import Button, Static, Footer
from textual.widgets import TextInput
from textual.widgets._text_input import TextWidgetBase
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)
def render(self) -> RenderableType:
return ScrollBarRender(
virtual_size=VIRTUAL_SIZE,
window_size=WINDOW_SIZE,
position=self.position,
style="green" if self.animation_running else "red",
)
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 = TextInput(
placeholder="Duration", initial="1.0", id="duration-input"
)
self.opacity_widget = Static(
f"[b]Welcome to Textual![/]\n\n{TEXT}", id="opacity-widget"
)
yield EasingButtons()
yield layout.Vertical(
layout.Vertical(Static("Animation Duration:"), duration_input, id="inputs"),
layout.Horizontal(
self.animated_bar,
layout.Container(self.opacity_widget, id="other"),
),
Footer(),
)
def on_button_pressed(self, event: Button.Pressed) -> None:
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_text_widget_base_changed(self, event: TextWidgetBase.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()

View File

@@ -100,6 +100,8 @@ class ScrollBarRender:
back = back_color
bar = bar_color
len_bars = len(bars)
width_thickness = thickness if vertical else 1
_Segment = Segment
@@ -110,11 +112,11 @@ class ScrollBarRender:
if window_size and size and virtual_size and size != virtual_size:
step_size = virtual_size / size
start = int(position / step_size * 8)
end = start + max(8, int(ceil(window_size / step_size * 8)))
start = int(position / step_size * len_bars)
end = start + max(len_bars, int(ceil(window_size / step_size * len_bars)))
start_index, start_bar = divmod(start, 8)
end_index, end_bar = divmod(end, 8)
start_index, start_bar = divmod(max(0, start), len_bars)
end_index, end_bar = divmod(max(0, end), len_bars)
upper = {"@click": "scroll_up"}
lower = {"@click": "scroll_down"}
@@ -129,8 +131,9 @@ class ScrollBarRender:
_Segment(blank, _Style(bgcolor=bar, meta=foreground_meta))
] * (end_index - start_index)
# Apply the smaller bar characters to head and tail of scrollbar for more "granularity"
if start_index < len(segments):
bar_character = bars[7 - start_bar]
bar_character = bars[len_bars - 1 - start_bar]
if bar_character != " ":
segments[start_index] = _Segment(
bar_character * width_thickness,
@@ -139,7 +142,7 @@ class ScrollBarRender:
else _Style(bgcolor=bar, color=back, meta=foreground_meta),
)
if end_index < len(segments):
bar_character = bars[7 - end_bar]
bar_character = bars[len_bars - 1 - end_bar]
if bar_character != " ":
segments[end_index] = _Segment(
bar_character * width_thickness,