mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
scroll in border
This commit is contained in:
59
examples/borders.css
Normal file
59
examples/borders.css
Normal file
@@ -0,0 +1,59 @@
|
||||
Screen {
|
||||
/* text-background: #212121; */
|
||||
}
|
||||
|
||||
#borders {
|
||||
layout: vertical;
|
||||
text-background: #212121;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
Lorem.border {
|
||||
height: 12;
|
||||
margin: 2 4;
|
||||
text-background: #303f9f;
|
||||
}
|
||||
|
||||
Lorem.round {
|
||||
border: round #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.solid {
|
||||
border: solid #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.double {
|
||||
border: double #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.dashed {
|
||||
border: dashed #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.heavy {
|
||||
border: heavy #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.inner {
|
||||
border: inner #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.outer {
|
||||
border: outer #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.hkey {
|
||||
border: hkey #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.vkey {
|
||||
border: vkey #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.tall {
|
||||
border: tall #8bc34a;
|
||||
}
|
||||
|
||||
Lorem.wide {
|
||||
border: wide #8bc34a;
|
||||
}
|
||||
58
examples/borders.py
Normal file
58
examples/borders.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from rich.console import Group
|
||||
from rich.padding import Padding
|
||||
from rich.text import Text
|
||||
|
||||
from textual.app import App
|
||||
from textual.renderables.gradient import VerticalGradient
|
||||
from textual import events
|
||||
from textual.widgets import Placeholder
|
||||
from textual.widget import Widget
|
||||
|
||||
lorem = Text.from_markup(
|
||||
"""[#C5CAE9]Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """,
|
||||
justify="full",
|
||||
)
|
||||
|
||||
|
||||
class Lorem(Widget):
|
||||
def render(self) -> Text:
|
||||
return Padding(lorem, 1)
|
||||
|
||||
|
||||
class Background(Widget):
|
||||
def render(self):
|
||||
return VerticalGradient("#212121", "#212121")
|
||||
|
||||
|
||||
class BordersApp(App):
|
||||
"""Sandbox application used for testing/development by Textual developers"""
|
||||
|
||||
def on_load(self):
|
||||
self.bind("q", "quit", "Quit")
|
||||
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
|
||||
borders = [
|
||||
Lorem(classes={"border", border})
|
||||
for border in (
|
||||
"round",
|
||||
"solid",
|
||||
"double",
|
||||
"dashed",
|
||||
"heavy",
|
||||
"inner",
|
||||
"outer",
|
||||
"hkey",
|
||||
"vkey",
|
||||
"tall",
|
||||
"wide",
|
||||
)
|
||||
]
|
||||
borders_view = Background(*borders)
|
||||
borders_view.show_vertical_scrollbar = True
|
||||
|
||||
self.mount(borders=borders_view)
|
||||
|
||||
|
||||
BordersApp.run(css_file="borders.css", log="textual.log")
|
||||
@@ -1,15 +1,18 @@
|
||||
#uber1 {
|
||||
/* border: heavy green;*/
|
||||
layout: vertical;
|
||||
/* text: on dark_green;*/
|
||||
overflow-y: scroll;
|
||||
text: on dark_green;
|
||||
overflow: hidden auto;
|
||||
border: heavy white;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
height: 8;
|
||||
|
||||
min-width: 80;
|
||||
|
||||
/* height: 8; */
|
||||
margin: 1 2;
|
||||
/* margin: 1 2; */
|
||||
text-background: dark_blue;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class BasicApp(App):
|
||||
|
||||
def on_load(self):
|
||||
self.bind("q", "quit", "Quit")
|
||||
self.bind("d", "dump")
|
||||
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
@@ -26,15 +27,15 @@ class BasicApp(App):
|
||||
Placeholder(id="child3", classes={"list-item"}),
|
||||
Placeholder(classes={"list-item"}),
|
||||
Placeholder(classes={"list-item"}),
|
||||
Placeholder(classes={"list-item"}),
|
||||
Placeholder(classes={"list-item"}),
|
||||
Placeholder(classes={"list-item"}),
|
||||
Placeholder(classes={"list-item"}),
|
||||
Placeholder(classes={"list-item"}),
|
||||
Placeholder(classes={"list-item"}),
|
||||
# Placeholder(classes={"list-item"}),
|
||||
# Placeholder(classes={"list-item"}),
|
||||
# Placeholder(classes={"list-item"}),
|
||||
# Placeholder(classes={"list-item"}),
|
||||
# Placeholder(classes={"list-item"}),
|
||||
# Placeholder(classes={"list-item"}),
|
||||
# Placeholder(id="child3", classes={"list-item"}),
|
||||
)
|
||||
uber1.show_vertical_scrollbar = True
|
||||
uber1.show_horizontal_scrollbar = True
|
||||
|
||||
self.mount(
|
||||
uber1=uber1
|
||||
@@ -47,5 +48,8 @@ class BasicApp(App):
|
||||
def action_quit(self):
|
||||
self.panic(self.screen.tree)
|
||||
|
||||
def action_dump(self):
|
||||
self.panic(str(self.app.registry))
|
||||
|
||||
BasicApp.run(css_file="uber.css", log="textual.log", log_verbosity=0)
|
||||
|
||||
BasicApp.run(css_file="uber.css", log="textual.log", log_verbosity=1)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from .geometry import Size, Offset, Region
|
||||
from .layout import WidgetPlacement
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
def arrange(
|
||||
widget: Widget, size: Size, scroll: Offset
|
||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||
|
||||
assert widget.layout is not None
|
||||
|
||||
placements, widgets = widget.layout.arrange(
|
||||
widget,
|
||||
size - (widget.show_vertical_scrollbar, widget.show_horizontal_scrollbar),
|
||||
scroll,
|
||||
)
|
||||
|
||||
return placements, widgets
|
||||
@@ -44,8 +44,6 @@ BORDER_LOCATIONS: dict[
|
||||
"vkey": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"tall": ((1, 0, 1), (1, 0, 1), (1, 0, 1)),
|
||||
"wide": ((1, 1, 1), (0, 1, 0), (1, 1, 1)),
|
||||
# "tall": ("101", "101▏", "101"),
|
||||
# "wide": ("111", "010", "111"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from rich.style import Style
|
||||
from . import errors, log
|
||||
from .geometry import Region, Offset, Size
|
||||
|
||||
from ._arrange import arrange
|
||||
|
||||
from ._loop import loop_last
|
||||
from ._types import Lines
|
||||
from .widget import Widget
|
||||
@@ -194,6 +194,7 @@ class Compositor:
|
||||
order: tuple[int, ...],
|
||||
clip: Region,
|
||||
) -> None:
|
||||
widget.pre_render()
|
||||
widgets.add(widget)
|
||||
total_region = region
|
||||
styles_offset = widget.styles.offset
|
||||
@@ -205,11 +206,18 @@ class Compositor:
|
||||
|
||||
if widget.layout is not None:
|
||||
scroll = widget.scroll
|
||||
total_region = region.size.region
|
||||
sub_clip = clip.intersection(region)
|
||||
|
||||
placements, arranged_widgets = arrange(widget, region.size, scroll)
|
||||
# Container region is minus border
|
||||
container_region = region.shrink(widget.styles.gutter)
|
||||
|
||||
child_region = widget._arrange_container(container_region)
|
||||
sub_clip = clip.intersection(child_region)
|
||||
|
||||
total_region = child_region.reset_origin
|
||||
|
||||
placements, arranged_widgets = widget.layout.arrange(
|
||||
widget, child_region.size, scroll
|
||||
)
|
||||
widgets.update(arranged_widgets)
|
||||
placements = sorted(placements, key=attrgetter("order"))
|
||||
|
||||
@@ -218,22 +226,21 @@ class Compositor:
|
||||
if sub_widget is not None:
|
||||
add_widget(
|
||||
sub_widget,
|
||||
sub_region + region.origin - scroll,
|
||||
sub_region + child_region.origin - scroll,
|
||||
sub_widget.z + (z,),
|
||||
sub_clip,
|
||||
)
|
||||
|
||||
for chrome_widget, chrome_region in widget.arrange_chrome(region.size):
|
||||
|
||||
render_region = RenderRegion(
|
||||
chrome_region + region.origin + layout_offset,
|
||||
for chrome_widget, chrome_region in widget._arrange_scrollbars(
|
||||
container_region.size
|
||||
):
|
||||
map[chrome_widget] = RenderRegion(
|
||||
chrome_region + container_region.origin + layout_offset,
|
||||
order,
|
||||
clip,
|
||||
total_region.size,
|
||||
chrome_region.size,
|
||||
)
|
||||
|
||||
map[chrome_widget] = render_region
|
||||
|
||||
map[widget] = RenderRegion(
|
||||
region + layout_offset, order, clip, total_region.size
|
||||
)
|
||||
@@ -374,6 +381,9 @@ class Compositor:
|
||||
overlaps = Region.overlaps
|
||||
|
||||
for widget, region, _order, clip in widget_regions:
|
||||
if not region:
|
||||
# log(widget, region)
|
||||
continue
|
||||
if region in clip:
|
||||
yield region, clip, widget._get_lines()
|
||||
elif overlaps(clip, region):
|
||||
@@ -421,7 +431,6 @@ class Compositor:
|
||||
screen_region = Region(0, 0, width, height)
|
||||
|
||||
crop_region = crop.intersection(screen_region) if crop else screen_region
|
||||
# log("RENDER", crop=crop_region)
|
||||
|
||||
_Segment = Segment
|
||||
divide = _Segment.divide
|
||||
@@ -443,8 +452,9 @@ class Compositor:
|
||||
|
||||
for region, clip, lines in renders:
|
||||
render_region = intersection(region, clip)
|
||||
# if not render_region:
|
||||
# continue
|
||||
for y, line in zip(render_region.y_range, lines):
|
||||
|
||||
first_cut, last_cut = render_region.x_extents
|
||||
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
|
||||
|
||||
|
||||
@@ -84,6 +84,31 @@ class StylesBuilder:
|
||||
except Exception as error:
|
||||
self.error(declaration.name, declaration.token, str(error))
|
||||
|
||||
def _process_enum_multiple(
|
||||
self, name, tokens: list[Token], valid_values: set[str], count: int
|
||||
) -> tuple[str, ...]:
|
||||
if len(tokens) > count or not tokens:
|
||||
self.error(name, tokens[0], f"expected 1 to {count} tokens here")
|
||||
results = []
|
||||
append = results.append
|
||||
for token in tokens:
|
||||
token_name, value, _, _, location, _ = token
|
||||
if token_name != "token":
|
||||
self.error(
|
||||
name,
|
||||
token,
|
||||
f"invalid token {value!r}; expected {friendly_list(valid_values)}",
|
||||
)
|
||||
append(value)
|
||||
|
||||
short_results = results[:]
|
||||
|
||||
while len(results) < count:
|
||||
results.extend(short_results)
|
||||
results = results[:count]
|
||||
|
||||
return tuple(results)
|
||||
|
||||
def _process_enum(
|
||||
self, name: str, tokens: list[Token], valid_values: set[str]
|
||||
) -> str:
|
||||
@@ -139,7 +164,7 @@ class StylesBuilder:
|
||||
if not tokens:
|
||||
return
|
||||
if len(tokens) == 1:
|
||||
self.styles._rules[name] = Scalar.parse(tokens[0].value)
|
||||
self.styles._rules[name.replace("-", "_")] = Scalar.parse(tokens[0].value)
|
||||
else:
|
||||
self.error(name, tokens[0], "a single scalar is expected")
|
||||
|
||||
@@ -188,6 +213,14 @@ class StylesBuilder:
|
||||
) -> None:
|
||||
self._process_scalar(name, tokens)
|
||||
|
||||
def process_overflow(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
rules = self.styles._rules
|
||||
overflow_x, overflow_y = self._process_enum_multiple(
|
||||
name, tokens, VALID_OVERFLOW, 2
|
||||
)
|
||||
rules["overflow_x"] = cast(Overflow, overflow_x)
|
||||
rules["overflow_y"] = cast(Overflow, overflow_y)
|
||||
|
||||
def process_overflow_x(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
|
||||
@@ -20,6 +20,8 @@ VALID_BORDER: Final = {
|
||||
"outer",
|
||||
"hkey",
|
||||
"vkey",
|
||||
"tall",
|
||||
"wide",
|
||||
}
|
||||
VALID_EDGE: Final = {"top", "right", "bottom", "left"}
|
||||
VALID_LAYOUT: Final = {"dock", "vertical", "grid"}
|
||||
|
||||
@@ -192,8 +192,13 @@ class StylesBase(ABC):
|
||||
Returns:
|
||||
Spacing: Space around widget.
|
||||
"""
|
||||
has_rule = self.has_rule
|
||||
spacing = Spacing()
|
||||
|
||||
return self.margin
|
||||
spacing += self.padding
|
||||
spacing += self.border.spacing
|
||||
|
||||
return spacing
|
||||
|
||||
@abstractmethod
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
|
||||
@@ -131,9 +131,12 @@ class DOMNode(MessagePump):
|
||||
|
||||
@property
|
||||
def css_identifier_styled(self) -> Text:
|
||||
tokens = Text(self.__class__.__name__)
|
||||
tokens = Text.styled(self.__class__.__name__)
|
||||
if self.id is not None:
|
||||
tokens.append(f"#{self.id}", style="bold")
|
||||
if self.classes:
|
||||
tokens.append(".")
|
||||
tokens.append(".".join(class_name for class_name in self.classes), "italic")
|
||||
if self.name:
|
||||
tokens.append(f"[name={self.name}]", style="underline")
|
||||
return tokens
|
||||
|
||||
@@ -291,6 +291,12 @@ class Region(NamedTuple):
|
||||
"""A range object for Y coordinates"""
|
||||
return range(self.y, self.y + self.height)
|
||||
|
||||
@property
|
||||
def reset_origin(self) -> Region:
|
||||
"""An region of the same size at the origin."""
|
||||
_, _, width, height = self
|
||||
return Region(0, 0, width, height)
|
||||
|
||||
def __add__(self, other: object) -> Region:
|
||||
if isinstance(other, tuple):
|
||||
ox, oy = other
|
||||
@@ -428,7 +434,7 @@ class Region(NamedTuple):
|
||||
)
|
||||
return new_region
|
||||
|
||||
def shrink(self, margin: Spacing) -> Region:
|
||||
def shrink(self, margin: tuple[int, int, int, int]) -> Region:
|
||||
"""Shrink a region by pushing each edge inwards.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -144,13 +144,13 @@ class MessagePump:
|
||||
self._child_tasks.add(timer.start())
|
||||
return timer
|
||||
|
||||
async def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
"""Run a callback after processing all messages and refreshing the screen.
|
||||
|
||||
Args:
|
||||
callback (Callable): A callable.
|
||||
"""
|
||||
await self.post_message(
|
||||
self.post_message_no_wait(
|
||||
events.Callback(self, partial(callback, *args, **kwargs))
|
||||
)
|
||||
|
||||
@@ -306,7 +306,8 @@ class MessagePump:
|
||||
return self.post_message_no_wait(message)
|
||||
|
||||
async def on_callback(self, event: events.Callback) -> None:
|
||||
await event.callback()
|
||||
await invoke(event.callback)
|
||||
# await event.callback()
|
||||
|
||||
def emit_no_wait(self, message: Message) -> bool:
|
||||
if self._parent:
|
||||
|
||||
@@ -11,12 +11,6 @@ if TYPE_CHECKING:
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class Refresh(Message):
|
||||
def can_replace(self, message: Message) -> bool:
|
||||
return isinstance(message, Refresh)
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class Update(Message, verbosity=3):
|
||||
def __init__(self, sender: MessagePump, widget: Widget):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from math import ceil
|
||||
|
||||
import rich.repr
|
||||
from rich.color import Color
|
||||
from rich.console import ConsoleOptions, RenderResult, RenderableType
|
||||
@@ -115,7 +117,7 @@ class ScrollBarRender:
|
||||
step_size = virtual_size / size
|
||||
|
||||
start = int(position / step_size * 8)
|
||||
end = start + max(8, int(window_size / step_size * 8))
|
||||
end = start + max(8, int(ceil(window_size / step_size * 8)))
|
||||
|
||||
start_index, start_bar = divmod(start, 8)
|
||||
end_index, end_bar = divmod(end, 8)
|
||||
@@ -148,7 +150,8 @@ class ScrollBarRender:
|
||||
else _Style(bgcolor=back, color=bar, meta=foreground_meta),
|
||||
)
|
||||
else:
|
||||
segments = [_Segment(blank)] * int(size)
|
||||
style = _Style(bgcolor=back)
|
||||
segments = [_Segment(blank, style=style)] * int(size)
|
||||
if vertical:
|
||||
return Segments(segments, new_lines=True)
|
||||
else:
|
||||
|
||||
@@ -38,7 +38,14 @@ from .renderables.opacity import Opacity
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .scrollbar import ScrollBar, ScrollTo
|
||||
from .scrollbar import (
|
||||
ScrollBar,
|
||||
ScrollTo,
|
||||
ScrollUp,
|
||||
ScrollDown,
|
||||
ScrollLeft,
|
||||
ScrollRight,
|
||||
)
|
||||
|
||||
|
||||
class RenderCache(NamedTuple):
|
||||
@@ -93,15 +100,14 @@ class Widget(DOMNode):
|
||||
scroll_y = Reactive(0.0)
|
||||
scroll_target_x = Reactive(0.0)
|
||||
scroll_target_y = Reactive(0.0)
|
||||
# virtual_size = Reactive(Size(0, 0))
|
||||
show_vertical_scrollbar = Reactive(False)
|
||||
show_horizontal_scrollbar = Reactive(False)
|
||||
show_vertical_scrollbar = Reactive(False, layout=True)
|
||||
show_horizontal_scrollbar = Reactive(False, layout=True)
|
||||
|
||||
async def watch_scroll_x(self, new_value: float) -> None:
|
||||
self.hscroll.position = round(new_value)
|
||||
self.hscroll.position = int(new_value)
|
||||
|
||||
async def watch_scroll_y(self, new_value: float) -> None:
|
||||
self.vscroll.position = round(new_value)
|
||||
self.vscroll.position = int(new_value)
|
||||
|
||||
def validate_scroll_x(self, value: float) -> float:
|
||||
return clamp(value, 0, self.max_scroll_x)
|
||||
@@ -117,11 +123,11 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def max_scroll_x(self) -> float:
|
||||
return max(0, self.virtual_size.width - self.size.width)
|
||||
return max(0, self.virtual_size.width - self.scroll_size.width)
|
||||
|
||||
@property
|
||||
def max_scroll_y(self) -> float:
|
||||
return max(0, self.virtual_size.height - self.size.height)
|
||||
return max(0, self.virtual_size.height - self.scroll_size.height)
|
||||
|
||||
@property
|
||||
def vscroll(self) -> ScrollBar:
|
||||
@@ -152,12 +158,44 @@ class Widget(DOMNode):
|
||||
if self._horizontal_scrollbar is not None:
|
||||
return self._horizontal_scrollbar
|
||||
self._horizontal_scrollbar = scroll_bar = ScrollBar(
|
||||
vertical=True, name="vertical"
|
||||
vertical=False, name="horizontal"
|
||||
)
|
||||
self.app.register(self, scroll_bar)
|
||||
|
||||
self.app.start_widget(self, scroll_bar)
|
||||
return scroll_bar
|
||||
|
||||
def _refresh_scrollbars(self) -> None:
|
||||
"""Refresh scrollbar visibility."""
|
||||
if not self.is_container:
|
||||
return
|
||||
styles = self.styles
|
||||
overflow_x = styles.overflow_x
|
||||
overflow_y = styles.overflow_y
|
||||
|
||||
if overflow_x == "hidden":
|
||||
self.show_horizontal_scrollbar = False
|
||||
elif overflow_x == "scroll":
|
||||
self.show_horizontal_scrollbar = True
|
||||
elif overflow_x == "auto":
|
||||
self.show_horizontal_scrollbar = (
|
||||
self.virtual_size.width > self.scroll_size.width
|
||||
)
|
||||
|
||||
if overflow_y == "hidden":
|
||||
self.show_vertical_scrollbar = False
|
||||
elif overflow_y == "scroll":
|
||||
self.show_vertical_scrollbar = True
|
||||
elif overflow_y == "auto":
|
||||
self.show_vertical_scrollbar = (
|
||||
self.virtual_size.height > self.scroll_size.height
|
||||
)
|
||||
self.log(
|
||||
"REFRESH_SCROLLBARS",
|
||||
self,
|
||||
self.virtual_size.height,
|
||||
self.scroll_size.height,
|
||||
)
|
||||
|
||||
@property
|
||||
def scrollbars_enabled(self) -> tuple[bool, bool]:
|
||||
"""A tuple of booleans that indicate if scrollbars are enabled.
|
||||
@@ -185,21 +223,28 @@ class Widget(DOMNode):
|
||||
animate (bool, optional): Animate to new scroll position. Defaults to False.
|
||||
"""
|
||||
|
||||
scroll_limit = False
|
||||
self.log(
|
||||
"scroll_to", x=x, y=y, max_x=self.max_scroll_x, max_y=self.max_scroll_y
|
||||
)
|
||||
|
||||
if animate:
|
||||
# TODO: configure animation speed
|
||||
if x is not None:
|
||||
self.scroll_target_x = x
|
||||
self.animate(
|
||||
"scroll_x", self.scroll_target_x, speed=80, easing="out_cubic"
|
||||
)
|
||||
if y is not None:
|
||||
self.scroll_target_y = y
|
||||
# TODO: Speed to be configurable or a setting
|
||||
self.animate("scroll_x", self.scroll_target_x, speed=80, easing="out_cubic")
|
||||
self.animate("scroll_y", self.scroll_target_y, speed=80, easing="out_cubic")
|
||||
self.animate(
|
||||
"scroll_y", self.scroll_target_y, speed=80, easing="out_cubic"
|
||||
)
|
||||
|
||||
else:
|
||||
if x is not None:
|
||||
self.scroll_x = x
|
||||
self.scroll_target_x = self.scroll_x = x
|
||||
if y is not None:
|
||||
self.scroll_y = y
|
||||
self.scroll_target_y = self.scroll_y = y
|
||||
self.refresh(layout=True)
|
||||
|
||||
def scroll_home(self, animate: bool = True) -> None:
|
||||
@@ -208,6 +253,12 @@ class Widget(DOMNode):
|
||||
def scroll_end(self, animate: bool = True) -> None:
|
||||
self.scroll_to(0, self.max_scroll_y, animate=animate)
|
||||
|
||||
def scroll_left(self, animate: bool = True) -> None:
|
||||
self.scroll_to(x=self.scroll_target_x - 1.5, animate=animate)
|
||||
|
||||
def scroll_right(self, animate: bool = True) -> None:
|
||||
self.scroll_to(x=self.scroll_target_x + 1.5, animate=animate)
|
||||
|
||||
def scroll_up(self, animate: bool = True) -> None:
|
||||
self.scroll_to(y=self.scroll_target_y + 1.5, animate=animate)
|
||||
|
||||
@@ -215,16 +266,20 @@ class Widget(DOMNode):
|
||||
self.scroll_to(y=self.scroll_target_y - 1.5, animate=animate)
|
||||
|
||||
def scroll_page_up(self, animate: bool = True) -> None:
|
||||
self.scroll_to(y=self.scroll_target_y - self.size.height, animate=animate)
|
||||
self.scroll_to(
|
||||
y=self.scroll_target_y - self.scroll_size.height, animate=animate
|
||||
)
|
||||
|
||||
def scroll_page_down(self, animate: bool = True) -> None:
|
||||
self.scroll_to(y=self.scroll_target_y + self.size.height, animate=animate)
|
||||
self.scroll_to(
|
||||
y=self.scroll_target_y + self.scroll_size.height, animate=animate
|
||||
)
|
||||
|
||||
def scroll_page_left(self, animate: bool = True) -> None:
|
||||
self.scroll_to(x=self.scroll_target_x - self.size.width, animate=animate)
|
||||
self.scroll_to(x=self.scroll_target_x - self.scroll_size.width, animate=animate)
|
||||
|
||||
def scroll_page_right(self, animate: bool = True) -> None:
|
||||
self.scroll_to(x=self.scroll_target_x + self.size.width, animate=animate)
|
||||
self.scroll_to(x=self.scroll_target_x + self.scroll_size.width, animate=animate)
|
||||
|
||||
def __init_subclass__(cls, can_focus: bool = True) -> None:
|
||||
super().__init_subclass__()
|
||||
@@ -240,7 +295,36 @@ class Widget(DOMNode):
|
||||
if pseudo_classes:
|
||||
yield "pseudo_classes", set(pseudo_classes)
|
||||
|
||||
def arrange_chrome(self, size: Size) -> Iterable[tuple[Widget, Region]]:
|
||||
def _arrange_container(self, region: Region) -> Region:
|
||||
|
||||
show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled
|
||||
|
||||
# self.log(self.styles.gutter)
|
||||
# region = region.shrink(self.styles.gutter)
|
||||
|
||||
if show_horizontal_scrollbar and show_vertical_scrollbar:
|
||||
(region, _, _, _) = region.split(-1, -1)
|
||||
return region
|
||||
elif show_vertical_scrollbar:
|
||||
region, _ = region.split_vertical(-1)
|
||||
return region
|
||||
elif show_horizontal_scrollbar:
|
||||
region, _ = region.split_horizontal(-1)
|
||||
return region
|
||||
return region
|
||||
|
||||
def _arrange_scrollbars(self, size: Size) -> Iterable[tuple[Widget, Region]]:
|
||||
"""Arrange the 'chrome' widgets (typically scrollbars) for a layout element.
|
||||
|
||||
Args:
|
||||
size (Size): _description_
|
||||
|
||||
Returns:
|
||||
Iterable[tuple[Widget, Region]]: _description_
|
||||
|
||||
Yields:
|
||||
Iterator[Iterable[tuple[Widget, Region]]]: _description_
|
||||
"""
|
||||
region = size.region
|
||||
show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled
|
||||
|
||||
@@ -251,6 +335,7 @@ class Widget(DOMNode):
|
||||
horizontal_scrollbar_region,
|
||||
_,
|
||||
) = region.split(-1, -1)
|
||||
|
||||
if vertical_scrollbar_region:
|
||||
yield self.vscroll, vertical_scrollbar_region
|
||||
if horizontal_scrollbar_region:
|
||||
@@ -301,7 +386,7 @@ class Widget(DOMNode):
|
||||
renderable,
|
||||
styles.border,
|
||||
inner_color=renderable_text_style.bgcolor,
|
||||
outer_color=renderable_text_style.bgcolor,
|
||||
outer_color=parent_text_style.bgcolor,
|
||||
)
|
||||
|
||||
if styles.outline:
|
||||
@@ -310,7 +395,7 @@ class Widget(DOMNode):
|
||||
styles.outline,
|
||||
outline=True,
|
||||
inner_color=renderable_text_style.bgcolor,
|
||||
outer_color=renderable_text_style.bgcolor,
|
||||
outer_color=parent_text_style.bgcolor,
|
||||
)
|
||||
|
||||
if styles.opacity:
|
||||
@@ -326,6 +411,10 @@ class Widget(DOMNode):
|
||||
def size(self) -> Size:
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def scroll_size(self) -> Size:
|
||||
return self._size - self.styles.gutter.totals
|
||||
|
||||
@property
|
||||
def virtual_size(self) -> Size:
|
||||
return self._virtual_size
|
||||
@@ -387,21 +476,26 @@ class Widget(DOMNode):
|
||||
if self._size != size or self._virtual_size != virtual_size:
|
||||
self._size = size
|
||||
self._virtual_size = virtual_size
|
||||
self._refresh_scrollbars()
|
||||
width, height = self.scroll_size
|
||||
if self.show_vertical_scrollbar:
|
||||
self.vscroll.window_virtual_size = virtual_size.height
|
||||
self.vscroll.window_size = size.height
|
||||
self.vscroll.window_size = height
|
||||
self.vscroll.refresh()
|
||||
if self.show_horizontal_scrollbar:
|
||||
self.hscroll.window_virtual_size = virtual_size.width
|
||||
self.hscroll.window_size = size.width
|
||||
self.hscroll.window_size = width
|
||||
self.hscroll.refresh()
|
||||
|
||||
self.scroll_to(self.scroll_x, self.scroll_y)
|
||||
self.refresh()
|
||||
self.call_later(self._refresh_scrollbars)
|
||||
|
||||
def render_lines(self) -> None:
|
||||
width, height = self.size
|
||||
renderable = self.render_styled()
|
||||
options = self.console.options.update_dimensions(width, height)
|
||||
|
||||
lines = self.console.render_lines(renderable, options)
|
||||
self.render_cache = RenderCache(self.size, lines)
|
||||
|
||||
@@ -432,8 +526,8 @@ class Widget(DOMNode):
|
||||
offset_x, offset_y = self.screen.get_offset(self)
|
||||
return self.screen.get_style_at(x + offset_x, y + offset_y)
|
||||
|
||||
async def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
await self.app.call_later(callback, *args, **kwargs)
|
||||
def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
self.app.call_later(callback, *args, **kwargs)
|
||||
|
||||
async def forward_event(self, event: events.Event) -> None:
|
||||
event.set_forwarded()
|
||||
@@ -457,6 +551,9 @@ class Widget(DOMNode):
|
||||
self._repaint_required = True
|
||||
self.check_idle()
|
||||
|
||||
def pre_render(self) -> None:
|
||||
self._refresh_scrollbars()
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
"""Get renderable for widget.
|
||||
|
||||
@@ -466,7 +563,7 @@ class Widget(DOMNode):
|
||||
|
||||
# Default displays a pretty repr in the center of the screen
|
||||
|
||||
label = f"{self.css_identifier_styled} {self.size} {self.virtual_size}"
|
||||
label = self.css_identifier_styled
|
||||
|
||||
return Align.center(label, vertical="middle")
|
||||
|
||||
@@ -532,25 +629,29 @@ class Widget(DOMNode):
|
||||
await self.dispatch_key(event)
|
||||
|
||||
def on_mouse_scroll_down(self) -> None:
|
||||
self.scroll_down()
|
||||
self.scroll_down(animate=True)
|
||||
|
||||
def on_mouse_scroll_up(self) -> None:
|
||||
self.scroll_up()
|
||||
self.scroll_up(animate=True)
|
||||
|
||||
def handle_scroll_to(self, message: ScrollTo) -> None:
|
||||
self.scroll_to(message.x, message.y, animate=message.animate)
|
||||
|
||||
def handle_scroll_up(self, event) -> None:
|
||||
def handle_scroll_up(self, event: ScrollUp) -> None:
|
||||
self.scroll_page_up()
|
||||
event.stop()
|
||||
|
||||
def handle_scroll_down(self) -> None:
|
||||
def handle_scroll_down(self, event: ScrollDown) -> None:
|
||||
self.scroll_page_down()
|
||||
event.stop()
|
||||
|
||||
def handle_scroll_left(self) -> None:
|
||||
def handle_scroll_left(self, event: ScrollLeft) -> None:
|
||||
self.scroll_page_left()
|
||||
event.stop()
|
||||
|
||||
def handle_scroll_right(self) -> None:
|
||||
def handle_scroll_right(self, event: ScrollRight) -> None:
|
||||
self.scroll_page_right()
|
||||
event.stop()
|
||||
|
||||
def key_home(self) -> bool:
|
||||
if self.is_container:
|
||||
@@ -564,15 +665,27 @@ class Widget(DOMNode):
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_left(self) -> bool:
|
||||
if self.is_container:
|
||||
self.scroll_left()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_right(self) -> bool:
|
||||
if self.is_container:
|
||||
self.scroll_right()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_down(self) -> bool:
|
||||
if self.is_container:
|
||||
self.scroll_down()
|
||||
self.scroll_up()
|
||||
return True
|
||||
return False
|
||||
|
||||
def key_up(self) -> bool:
|
||||
if self.is_container:
|
||||
self.scroll_up()
|
||||
self.scroll_down()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user