scroll in border

This commit is contained in:
Will McGugan
2022-03-21 11:28:13 +00:00
parent 479a9aed45
commit 89033238c4
16 changed files with 370 additions and 100 deletions

59
examples/borders.css Normal file
View 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
View 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")

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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"),
}

View File

@@ -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)]

View File

@@ -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:

View File

@@ -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"}

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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):

View File

@@ -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:

View File

@@ -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