Skip to content

Commit 7cb9000

Browse files
authored
Merge pull request #15 from willmcgugan/view-scroll
View scroll
2 parents 7f4da35 + 70d18b8 commit 7cb9000

22 files changed

+1146
-493
lines changed

examples/simple.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from textual.app import App
55
from textual.widgets.header import Header
66
from textual.widgets.placeholder import Placeholder
7-
from textual.widgets.window import Window
7+
from textual.widgets.scroll_view import ScrollView
88

99
with open("richreadme.md", "rt") as fh:
1010
readme = Markdown(fh.read(), hyperlinks=True, code_theme="fruity")
@@ -16,7 +16,7 @@ class MyApp(App):
1616

1717
async def on_startup(self, event: events.Startup) -> None:
1818
await self.view.mount_all(
19-
header=Header(self.title), left=Placeholder(), body=Window(readme)
19+
header=Header(self.title), left=Placeholder(), body=ScrollView(readme)
2020
)
2121

2222

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "textual"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
description = "Text User Interface using Rich"
55
authors = ["Will McGugan <[email protected]>"]
66
license = "MIT"
@@ -25,6 +25,7 @@ typing-extensions = { version = "^3.10.0", python = "<3.8" }
2525

2626
[tool.poetry.dev-dependencies]
2727
# rich = {git = "[email protected]:willmcgugan/rich", rev = "pretty-classes"}
28+
mypy = "^0.910"
2829

2930
[build-system]
3031
requires = ["poetry-core>=1.0.0"]

src/textual/_animator.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
import asyncio
6+
from time import time
7+
from tracemalloc import start
8+
from typing import Callable
9+
10+
from dataclasses import dataclass
11+
12+
from ._timer import Timer
13+
from ._types import MessageTarget
14+
15+
16+
EasingFunction = Callable[[float], float]
17+
18+
# https://easings.net/
19+
EASING = {
20+
"none": lambda x: 1.0,
21+
"round": lambda x: 0.0 if x < 0.5 else 1.0,
22+
"linear": lambda x: x,
23+
"in_cubic": lambda x: x * x * x,
24+
"in_out_cubic": lambda x: 4 * x * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 3) / 2,
25+
"out_cubic": lambda x: 1 - pow(1 - x, 3),
26+
}
27+
28+
29+
log = logging.getLogger("rich")
30+
31+
32+
@dataclass
33+
class Animation:
34+
obj: object
35+
attribute: str
36+
start_time: float
37+
duration: float
38+
start_value: float
39+
end_value: float
40+
easing_function: EasingFunction
41+
42+
def __call__(self, time: float) -> bool:
43+
44+
if self.duration == 0:
45+
value = self.end_value
46+
else:
47+
progress = min(1.0, (time - self.start_time) / self.duration)
48+
if self.end_value > self.start_value:
49+
eased_progress = self.easing_function(progress)
50+
value = (
51+
self.start_value
52+
+ (self.end_value - self.start_value) * eased_progress
53+
)
54+
else:
55+
eased_progress = 1 - self.easing_function(progress)
56+
value = (
57+
self.end_value
58+
+ (self.start_value - self.end_value) * eased_progress
59+
)
60+
61+
setattr(self.obj, self.attribute, value)
62+
return value == self.end_value
63+
64+
65+
class BoundAnimator:
66+
def __init__(self, animator: Animator, obj: object) -> None:
67+
self._animator = animator
68+
self._obj = obj
69+
70+
def __call__(
71+
self,
72+
attribute: str,
73+
value: float,
74+
*,
75+
duration: float | None = None,
76+
speed: float | None = None,
77+
easing: EasingFunction | str = "in_out_cubic",
78+
) -> None:
79+
easing_function = EASING[easing] if isinstance(easing, str) else easing
80+
self._animator.animate(
81+
self._obj,
82+
attribute=attribute,
83+
value=value,
84+
duration=duration,
85+
speed=speed,
86+
easing=easing_function,
87+
)
88+
89+
90+
class Animator:
91+
def __init__(self, target: MessageTarget, frames_per_second: int = 30) -> None:
92+
self._animations: dict[tuple[object, str], Animation] = {}
93+
self._timer = Timer(target, 1 / frames_per_second, target, callback=self)
94+
95+
async def start(self) -> None:
96+
asyncio.get_event_loop().create_task(self._timer.run())
97+
98+
async def stop(self) -> None:
99+
self._timer.stop()
100+
101+
def bind(self, obj: object) -> BoundAnimator:
102+
return BoundAnimator(self, obj)
103+
104+
def animate(
105+
self,
106+
obj: object,
107+
attribute: str,
108+
value: float,
109+
*,
110+
duration: float | None = None,
111+
speed: float | None = None,
112+
easing: EasingFunction = EASING["in_out_cubic"],
113+
) -> None:
114+
115+
start_time = time()
116+
117+
animation_key = (obj, attribute)
118+
if animation_key in self._animations:
119+
self._animations[animation_key](start_time)
120+
121+
start_value = getattr(obj, attribute)
122+
if duration is not None:
123+
animation_duration = duration
124+
else:
125+
animation_duration = abs(value - start_value) / (speed or 50)
126+
127+
animation = Animation(
128+
obj,
129+
attribute=attribute,
130+
start_time=start_time,
131+
duration=animation_duration,
132+
start_value=start_value,
133+
end_value=value,
134+
easing_function=easing,
135+
)
136+
self._animations[animation_key] = animation
137+
self._timer.resume()
138+
139+
async def __call__(self) -> None:
140+
if not self._animations:
141+
self._timer.pause()
142+
else:
143+
animation_time = time()
144+
animation_keys = list(self._animations.keys())
145+
for animation_key in animation_keys:
146+
animation = self._animations[animation_key]
147+
if animation(animation_time):
148+
del self._animations[animation_key]

src/textual/_line_cache.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
from typing import Iterable
77

8+
from rich.cells import cell_len
89
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
910
from rich.control import Control
1011
from rich.segment import Segment
12+
from rich.style import Style
1113

1214
from ._loop import loop_last
1315

@@ -57,3 +59,15 @@ def render(self, x: int, y: int, width: int, height: int) -> Iterable[Segment]:
5759
if not last:
5860
yield new_line
5961
self._dirty[:] = [False] * len(self.lines)
62+
63+
def get_style_at(self, x: int, y: int) -> Style:
64+
try:
65+
line = self.lines[y]
66+
except IndexError:
67+
return Style.null()
68+
end = 0
69+
for segment in line:
70+
end += cell_len(segment.text)
71+
if x < end:
72+
return segment.style or Style.null()
73+
return Style.null()

src/textual/_timer.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ def __init__(
4040
self._callback = callback
4141
self._repeat = repeat
4242
self._stop_event = Event()
43+
self._active = Event()
44+
self._active.set()
4345

4446
def __rich_repr__(self) -> RichReprResult:
4547
yield self._interval
@@ -54,13 +56,21 @@ def target(self) -> MessageTarget:
5456
return target
5557

5658
def stop(self) -> None:
59+
self._active.set()
5760
self._stop_event.set()
5861

62+
def pause(self) -> None:
63+
self._active.clear()
64+
65+
def resume(self) -> None:
66+
self._active.set()
67+
5968
async def run(self) -> None:
6069
count = 0
6170
_repeat = self._repeat
6271
_interval = self._interval
6372
_wait = self._stop_event.wait
73+
_wait_active = self._active.wait
6474
start = monotonic()
6575
try:
6676
while _repeat is None or count <= _repeat:
@@ -78,5 +88,6 @@ async def run(self) -> None:
7888
except EventTargetGone:
7989
break
8090
count += 1
91+
await _wait_active()
8192
except CancelledError:
8293
pass

src/textual/_xterm_parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ def parse(self, on_token: TokenCallback) -> Generator[Awaitable, str, None]:
6565

6666
while not self.is_eof:
6767
character = yield read1()
68-
log.debug("character=%r", character)
68+
# log.debug("character=%r", character)
6969
if character == ESC and ((yield self.peek_buffer()) or more_data()):
7070
sequence: str = character
7171
while True:
7272
sequence += yield read1()
73-
log.debug(f"sequence=%r", sequence)
73+
# log.debug(f"sequence=%r", sequence)
7474
keys = get_ansi_sequence(sequence, None)
7575
if keys is not None:
7676
for key in keys:

0 commit comments

Comments
 (0)