mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
0
src/textual/cli/previews/__init__.py
Normal file
0
src/textual/cli/previews/__init__.py
Normal file
40
src/textual/cli/previews/easing.css
Normal file
40
src/textual/cli/previews/easing.css
Normal 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;
|
||||
}
|
||||
113
src/textual/cli/previews/easing.py
Normal file
113
src/textual/cli/previews/easing.py
Normal 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()
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user