From 9d998eb982696d8853f6a7095f7df14c445e58ec Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 11 Jul 2021 15:47:00 +0100 Subject: [PATCH] Scroll view now uses grid --- examples/calculator.py | 2 +- src/textual/layout.py | 4 ++ src/textual/layouts/grid.py | 48 +++++++++++++++++++++--- src/textual/page.py | 4 ++ src/textual/view.py | 1 + src/textual/widgets/_button.py | 58 +++++++++++++++++++++++++++++ src/textual/widgets/_scroll_view.py | 29 ++++++++++++--- 7 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 src/textual/widgets/_button.py diff --git a/examples/calculator.py b/examples/calculator.py index 514dc7ca4..4a10b2f36 100644 --- a/examples/calculator.py +++ b/examples/calculator.py @@ -74,7 +74,7 @@ class CalculatorApp(App): layout.place( *buttons.values(), - numbers=Static(Padding(numbers, (0, 1)), style="white on rgb(51,51,51)"), + numbers=Static(Padding(numbers, (0, 1), style="white on rgb(51,51,51)")), zero=make_button("0"), ) diff --git a/src/textual/layout.py b/src/textual/layout.py index e9c79302c..fa8a306fc 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -25,6 +25,7 @@ log = logging.getLogger("rich") if TYPE_CHECKING: from .widget import Widget, WidgetID + from .view import View class NoWidget(Exception): @@ -137,6 +138,9 @@ class Layout(ABC): ) -> dict[Widget, OrderedRegion]: ... + async def mount_all(self, view: "View") -> None: + await view.mount(*self.get_widgets()) + @property def map(self) -> dict[Widget, OrderedRegion]: return self._layout_map diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index 46e4f8656..418338fee 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -11,6 +11,7 @@ from .._layout_resolve import layout_resolve from .._loop import loop_last from ..geometry import Dimensions, Point, Region from ..layout import Layout, OrderedRegion +from ..view import View from ..widget import Widget if sys.version_info >= (3, 8): @@ -49,14 +50,16 @@ class GridLayout(Layout): self.rows: list[GridOptions] = [] self.areas: dict[str, GridArea] = {} self.widgets: dict[Widget, str | None] = {} - self.column_gap = 1 - self.row_gap = 1 + self.column_gap = 0 + self.row_gap = 0 self.column_repeat = False self.row_repeat = False self.column_align: GridAlign = "start" self.row_align: GridAlign = "start" self.column_gutter: int = 0 self.row_gutter: int = 0 + self.hidden_columns: set[str] = set() + self.hidden_rows: set[str] = set() if gap is not None: if isinstance(gap, tuple): @@ -75,6 +78,18 @@ class GridLayout(Layout): super().__init__() + def hide_row(self, row_name: str) -> None: + self.hidden_rows.add(row_name) + + def show_row(self, row_name: str) -> None: + self.hidden_rows.discard(row_name) + + def hide_column(self, column_name: str) -> None: + self.hidden_rows.add(column_name) + + def show_column(self, column_name: str) -> None: + self.hidden_rows.discard(column_name) + def add_column( self, name: str, @@ -277,14 +292,33 @@ class GridLayout(Layout): return names, tracks, len(spans), max_size + def add_widget(widget: Widget, region: Region, order: tuple[int, int]): + region = region + offset + widget.layout_offset + map[widget] = OrderedRegion(region, order) + if isinstance(widget, View): + sub_map = widget.layout.generate_map( + region.width, region.height, offset=region.origin + ) + map.update(sub_map) + container = Dimensions( width - self.column_gutter * 2, height - self.row_gutter * 2 ) column_names, column_tracks, column_count, column_size = resolve_tracks( - self.columns, container.width, self.column_gap, self.column_repeat + [ + options + for options in self.columns + if options.name not in self.hidden_columns + ], + container.width, + self.column_gap, + self.column_repeat, ) row_names, row_tracks, row_count, row_size = resolve_tracks( - self.rows, container.height, self.row_gap, self.row_repeat + [options for options in self.rows if options.name not in self.hidden_rows], + container.height, + self.row_gap, + self.row_repeat, ) grid_size = Dimensions(column_size, row_size) @@ -323,7 +357,8 @@ class GridLayout(Layout): self.column_align, self.row_align, ) - map[widget] = OrderedRegion(region + gutter, (0, order)) + # map[widget] = OrderedRegion(region + gutter + offset, (0, order)) + add_widget(widget, region + gutter, (0, order)) order += 1 # Widgets with no area assigned. @@ -355,7 +390,8 @@ class GridLayout(Layout): self.column_align, self.row_align, ) - map[widget] = OrderedRegion(region + gutter, (0, order)) + # map[widget] = OrderedRegion(region + gutter + offset, (0, order)) + add_widget(widget, region + gutter, (0, order)) order += 1 return map diff --git a/src/textual/page.py b/src/textual/page.py index 8595036e7..9ea812e47 100644 --- a/src/textual/page.py +++ b/src/textual/page.py @@ -1,5 +1,7 @@ from __future__ import annotations +from logging import getLogger + from rich.console import Console, ConsoleOptions, RenderableType, RenderResult from rich.padding import Padding, PaddingDimensions from rich.segment import Segment @@ -10,6 +12,8 @@ from .geometry import Dimensions, Point from .message import Message from .widget import Widget, Reactive +log = getLogger("rich") + class PageUpdate(Message): def can_batch(self, message: "Message") -> bool: diff --git a/src/textual/view.py b/src/textual/view.py index abc38a0fd..53656b18f 100644 --- a/src/textual/view.py +++ b/src/textual/view.py @@ -135,6 +135,7 @@ class View(Widget): region = self.get_widget_region(widget) else: widget, region = self.get_widget_at(event.x, event.y) + log.debug("WIDGET %r", widget) except NoWidget: await self.app.set_mouse_over(None) else: diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py new file mode 100644 index 000000000..79c2e6bb7 --- /dev/null +++ b/src/textual/widgets/_button.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from rich.align import Align +from rich.console import Console, ConsoleOptions, RenderResult, RenderableType +from rich.padding import Padding +from rich.panel import Panel +import rich.repr +from rich.style import StyleType + +from ..reactive import Reactive +from ..widget import Widget + + +class Expand: + def __init__(self, renderable: RenderableType) -> None: + self.renderable = renderable + + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + width = options.max_width + height = options.height or 1 + yield from console.render( + self.renderable, options.update_dimensions(width, height) + ) + + +class ButtonRenderable: + def __init__(self, label: RenderableType, style: StyleType = "") -> None: + self.label = label + self.style = style + + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + width = options.max_width + height = options.height or 1 + + yield Align.center( + self.label, vertical="middle", style=self.style, width=width, height=height + ) + + +class Button(Widget): + def __init__( + self, + label: RenderableType, + name: str | None = None, + style: StyleType = "white on dark_blue", + ): + self.label = label + self.name = name or str(label) + self.style = style + super().__init__() + + def render(self) -> RenderableType: + return ButtonRenderable(self.label, style=self.style) + return Align.center(self.label, vertical="middle", style=self.style) diff --git a/src/textual/widgets/_scroll_view.py b/src/textual/widgets/_scroll_view.py index 914ff2794..84789d458 100644 --- a/src/textual/widgets/_scroll_view.py +++ b/src/textual/widgets/_scroll_view.py @@ -7,18 +7,19 @@ from rich.style import StyleType from .. import events +from ..layouts.grid import GridLayout from ..message import Message from ..scrollbar import ScrollTo, ScrollBar from ..geometry import clamp from ..page import Page -from ..views import DockView +from ..view import View from ..reactive import Reactive log = logging.getLogger("rich") -class ScrollView(DockView): +class ScrollView(View): def __init__( self, renderable: RenderableType | None = None, @@ -29,12 +30,23 @@ class ScrollView(DockView): ) -> None: self.fluid = fluid self._vertical_scrollbar = ScrollBar(vertical=True) + self._horizontal_scrollbar = ScrollBar(vertical=False) self._page = Page(renderable or "", style=style) - super().__init__(name=name) + layout = GridLayout() + layout.add_column("main") + layout.add_column("vertical", size=1) + layout.add_row("main") + layout.add_row("horizontal", size=1) + layout.add_areas( + content="main,main", vertical="vertical,main", horizontal="main,horizontal" + ) + layout.hide_row("horizontal") + super().__init__(name=name, layout=layout) x: Reactive[float] = Reactive(0) y: Reactive[float] = Reactive(0) + target_x: Reactive[float] = Reactive(0) target_y: Reactive[float] = Reactive(0) def validate_y(self, value: float) -> float: @@ -52,8 +64,15 @@ class ScrollView(DockView): self.require_repaint() async def on_mount(self, event: events.Mount) -> None: - await self.dock(self._vertical_scrollbar, edge="right", size=1) - await self.dock(self._page, edge="top") + assert isinstance(self.layout, GridLayout) + self.layout.place( + content=self._page, + vertical=self._vertical_scrollbar, + horizontal=self._horizontal_scrollbar, + ) + await self.layout.mount_all(self) + # await self.dock(self._vertical_scrollbar, edge="right", size=1) + # await self.dock(self._page, edge="top") def scroll_up(self) -> None: self.target_y += 1.5