Skip to content

Commit fe7da51

Browse files
committed
new scrollars
1 parent 8e34da9 commit fe7da51

File tree

5 files changed

+77
-68
lines changed

5 files changed

+77
-68
lines changed

src/textual/_line_cache.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from rich.segment import Segment
1010

1111

12+
log = logging.getLogger("rich")
13+
14+
1215
class LineCache:
1316
def __init__(self, lines: list[list[Segment]]) -> None:
1417
self.lines = lines
@@ -33,13 +36,12 @@ def dirty(self) -> bool:
3336
def __rich_console__(
3437
self, console: Console, options: ConsoleOptions
3538
) -> RenderResult:
36-
new_line = Segment.line()
39+
3740
for line in self.lines:
3841
yield from line
3942

4043
def render(self, x: int, y: int) -> Iterable[Segment]:
4144
move_to = Control.move_to
42-
new_line = Segment.line()
4345
for offset_y, (line, dirty) in enumerate(zip(self.lines, self._dirty), y):
4446
if dirty:
4547
yield move_to(x, offset_y).segment

src/textual/scrollbar.py

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from math import ceil
44
import logging
5-
from typing import Iterable
65

6+
7+
from rich.color import Color
8+
from rich.style import Style
79
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
810
from rich.segment import Segment
911
from rich.style import Style
@@ -17,7 +19,7 @@ def __init__(
1719
lines: list[list[Segment]],
1820
height: int,
1921
virtual_height: int,
20-
position: int,
22+
position: float,
2123
overlay: bool = False,
2224
) -> None:
2325
self.lines = lines
@@ -31,6 +33,7 @@ def __rich_console__(
3133
) -> RenderResult:
3234
bar = render_bar(
3335
size=self.height,
36+
window_size=len(self.lines),
3437
virtual_size=self.virtual_height,
3538
position=self.position,
3639
)
@@ -46,56 +49,48 @@ def render_bar(
4649
virtual_size: float = 50,
4750
window_size: float = 20,
4851
position: float = 0,
49-
bar_style: Style | None = None,
50-
back_style: Style | None = None,
52+
back_color: str = "#555555",
53+
bar_color: str = "bright_magenta",
5154
ascii_only: bool = False,
5255
vertical: bool = True,
5356
) -> list[Segment]:
54-
if vertical:
55-
if ascii_only:
56-
solid = "|"
57-
half_start = "|"
58-
half_end = "|"
59-
else:
60-
solid = "┃"
61-
half_start = "╻"
62-
half_end = "╹"
63-
else:
64-
if ascii_only:
65-
solid = "-"
66-
half_start = "-"
67-
half_end = "-"
68-
else:
69-
solid = "━"
70-
half_start = "╺"
71-
half_end = "╸"
72-
73-
_bar_style = bar_style or Style.parse("bright_magenta")
74-
_back_style = back_style or Style.parse("#555555")
75-
76-
_Segment = Segment
7757

78-
start_bar_segment = _Segment(half_start, _bar_style)
79-
end_bar_segment = _Segment(half_end, _bar_style)
80-
bar_segment = _Segment(solid, _bar_style)
58+
if ascii_only:
59+
bars = ["|", "|", "|", "|", "|", "|", "|", "|", "|"]
60+
else:
61+
bars = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
8162

82-
start_back_segment = _Segment(half_end, _bar_style)
83-
end_back_segment = _Segment(half_end, _back_style)
84-
back_segment = _Segment(solid, _back_style)
63+
back = Color.parse(back_color)
64+
bar = Color.parse(bar_color)
8565

86-
segments = [back_segment] * size
66+
_Segment = Segment
67+
_Style = Style
68+
segments = [_Segment(" ", _Style(bgcolor=back))] * int(size)
8769

8870
step_size = virtual_size / size
8971

90-
start = position / step_size
91-
# end = (position + window_size) / step_size
92-
end = start + window_size / step_size
72+
start = int(position / step_size * 8)
73+
end = start + int(window_size / step_size * 8)
74+
75+
start_index, start_bar = divmod(start, 8)
76+
end_index, end_bar = divmod(end, 8)
9377

94-
start_index = int(start)
95-
end_index = start_index + ceil(window_size / step_size)
96-
bar_height = end_index - start_index
78+
if start_index == end_index:
79+
if start_index < len(segments):
80+
segments[start_index] = _Segment(" ", _Style(bgcolor=bar))
81+
else:
82+
segments[start_index:end_index] = [_Segment(" ", _Style(bgcolor=bar))] * (
83+
end_index - start_index
84+
)
9785

98-
segments[start_index:end_index] = [bar_segment] * bar_height
86+
if start_index < len(segments):
87+
segments[start_index] = _Segment(
88+
bars[7 - start_bar], _Style(bgcolor=back, color=bar)
89+
)
90+
if end_index < len(segments):
91+
segments[end_index] = _Segment(
92+
bars[7 - end_bar], _Style(color=back, bgcolor=bar)
93+
)
9994

10095
return segments
10196

@@ -107,12 +102,12 @@ def render_bar(
107102
console = Console()
108103

109104
bar = render_bar(
110-
size=20,
105+
size=10,
111106
virtual_size=100,
112-
window_size=50,
113-
position=0,
114-
vertical=False,
107+
window_size=20,
108+
position=80,
109+
vertical=True,
115110
ascii_only=False,
116111
)
117112

118-
console.print(Segments(bar, new_lines=False))
113+
console.print(Segments(bar, new_lines=True))

src/textual/view.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,8 @@ async def on_message(self, message: Message) -> None:
117117
for layout, (region, render) in self.layout.map.items():
118118
if layout.renderable is widget:
119119
assert isinstance(widget, Widget)
120-
start = time()
121120
update = widget.render_update(region.x, region.y)
122121
segments = Segments(update)
123-
log.debug(
124-
"RENDER UPDATE %r rendered in %.1fms",
125-
widget,
126-
(time() - start) * 1000.0,
127-
)
128122
self.console.print(segments, end="")
129123

130124
async def on_create(self, event: events.Created) -> None:

src/textual/widget.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from logging import getLogger
44
from typing import (
5+
Callable,
56
ClassVar,
67
Generic,
78
Iterable,
@@ -43,8 +44,11 @@ def can_batch(self, message: Message) -> bool:
4344

4445

4546
class Reactive(Generic[T]):
46-
def __init__(self, default: T) -> None:
47+
def __init__(
48+
self, default: T, validator: Callable[[object, T], T] | None = None
49+
) -> None:
4750
self._default = default
51+
self.validator = validator
4852

4953
def __set_name__(self, owner: "Widget", name: str) -> None:
5054
self.internal_name = f"_{name}"
@@ -56,6 +60,8 @@ def __get__(self, obj: "Widget", obj_type: type[object]) -> T:
5660
def __set__(self, obj: "Widget", value: T) -> None:
5761
if getattr(obj, self.internal_name) != value:
5862
log.debug("%s -> %s", self.internal_name, value)
63+
if self.validator:
64+
value = self.validator(obj, value)
5965
setattr(obj, self.internal_name, value)
6066
obj.require_repaint()
6167

@@ -129,7 +135,7 @@ async def forward_input_event(self, event: events.Event) -> None:
129135
await self.post_message(event)
130136

131137
async def refresh(self) -> None:
132-
self._line_cache = None
138+
# self._line_cache = None
133139
await self.repaint()
134140

135141
async def repaint(self) -> None:

src/textual/widgets/window.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing_extensions import Literal
1111

1212
from rich.console import Console, ConsoleOptions, RenderableType
13+
from rich.padding import Padding
1314
from rich.segment import Segment
1415

1516
from .. import events
@@ -28,22 +29,27 @@ def __init__(
2829
) -> None:
2930
self.renderable = renderable
3031
self.y_scroll = y_scroll
31-
self._virtual_size: Dimensions | None = None
32+
self._virtual_size: Dimensions = Dimensions(0, 0)
3233
self._renderable_updated = True
3334
self._lines: list[list[Segment]] = []
3435
super().__init__()
3536

36-
position: Reactive[int] = Reactive(60)
37+
def _validate_position(self, position: float) -> float:
38+
_position = position
39+
validated_pos = min(
40+
max(0, position), self._virtual_size.height - self.size.height
41+
)
42+
log.debug("virtual_size=%r size=%r", self._virtual_size, self.size)
43+
log.debug("%r %r", _position, validated_pos)
44+
return validated_pos
45+
46+
position: Reactive[float] = Reactive(60, validator=_validate_position)
3747
show_vertical_bar: Reactive[bool] = Reactive(True)
3848

3949
@property
4050
def virtual_size(self) -> Dimensions:
4151
return self._virtual_size or self.size
4252

43-
def require_repaint(self) -> None:
44-
del self._lines[:]
45-
return super().require_repaint()
46-
4753
def get_lines(
4854
self, console: Console, options: ConsoleOptions
4955
) -> list[list[Segment]]:
@@ -52,28 +58,29 @@ def get_lines(
5258
if self.show_vertical_bar and self.y_scroll != "overlay":
5359
width -= 1
5460
self._lines = console.render_lines(
55-
self.renderable, options.update_width(width)
61+
Padding(self.renderable, 1), options.update_width(width)
5662
)
63+
self._virtual_size = Dimensions(0, len(self._lines))
5764
return self._lines
5865

5966
def update(self, renderable: RenderableType) -> None:
6067
self.renderable = renderable
6168
del self._lines[:]
6269

6370
def render(self, console: Console, options: ConsoleOptions) -> RenderableType:
64-
height = options.height or console.height
71+
height = self.size.height
6572
lines = self.get_lines(console, options)
66-
73+
position = int(self.position)
74+
log.debug("%r, %r, %r", height, self._virtual_size, self.position)
6775
return VerticalBar(
68-
lines[self.position : self.position + height],
76+
lines[position : position + height],
6977
height,
70-
len(lines),
78+
self._virtual_size.height,
7179
self.position,
7280
overlay=self.y_scroll == "overlay",
7381
)
7482

7583
async def on_key(self, event: events.Key) -> None:
76-
log.debug("window.on_key; %s", event)
7784
if event.key == "down":
7885
self.position += 1
7986
elif event.key == "up":
@@ -84,3 +91,8 @@ async def on_mouse_scroll_up(self, event: events.MouseScrollUp) -> None:
8491

8592
async def on_mouse_scroll_down(self, event: events.MouseScrollUp) -> None:
8693
self.position -= 1
94+
95+
async def on_resize(self, event: events.Resize) -> None:
96+
del self._lines[:]
97+
self.position = self.position
98+
self.require_repaint()

0 commit comments

Comments
 (0)