From 771167c13684bb126e0c501cafde4f9b866cb49b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Sep 2021 08:46:47 +0100 Subject: [PATCH] big table --- CHANGELOG.md | 6 ++++++ examples/big_table.py | 13 +++++-------- pyproject.toml | 2 +- src/textual/app.py | 18 +++++++++++++++++- src/textual/layout.py | 20 ++++++++++++++------ src/textual/layouts/vertical.py | 23 +++++++++++++++++++++-- src/textual/views/_window_view.py | 15 +++++++++++---- src/textual/widgets/_scroll_view.py | 5 ++++- 8 files changed, 79 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3faee7c2a..4867d077a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Changed message handlers to use prefix handle\_ - Renamed messages to drop the Message suffix +### Added + +- Added App.measure +- Added auto_width to Vertical Layout, WindowView, an ScrollView +- Added big_table.py example + ## [0.1.10] - 2021-08-25 ### Added diff --git a/examples/big_table.py b/examples/big_table.py index 62ed95380..33b576e3d 100644 --- a/examples/big_table.py +++ b/examples/big_table.py @@ -1,9 +1,8 @@ from rich.table import Table -from rich.measure import Measurement from textual import events from textual.app import App -from textual.widgets import Header, Footer, ScrollView +from textual.widgets import ScrollView class MyApp(App): @@ -14,19 +13,17 @@ class MyApp(App): async def on_mount(self, event: events.Mount) -> None: - self.body = body = ScrollView() - #body.virtual_size.width = 300 + self.body = body = ScrollView(auto_width=True) await self.view.dock(body) async def add_content(): - table = Table(title="Demo", width=1000) + table = Table(title="Demo") for i in range(40): - table.add_column(f'Col {i + 1}', style='magenta') - + table.add_column(f"Col {i + 1}", style="magenta") for i in range(200): - table.add_row(*[f'cell {i},{j}' for j in range(40)]) + table.add_row(*[f"cell {i},{j}" for j in range(40)]) await body.update(table) diff --git a/pyproject.toml b/pyproject.toml index d012d6a1d..9c5a36e4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.1.10" +version = "0.1.11" homepage = "https://github.com/willmcgugan/textual" description = "Text User Interface using Rich" authors = ["Will McGugan "] diff --git a/src/textual/app.py b/src/textual/app.py index f10ed1fbf..88ac99e79 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -10,6 +10,7 @@ from rich.control import Control import rich.repr from rich.screen import Screen from rich.console import Console, RenderableType +from rich.measure import Measurement from rich.traceback import Traceback from . import events @@ -347,6 +348,21 @@ class App(MessagePump): except Exception: self.panic() + def measure(self, renderable: RenderableType, max_width=100_000) -> int: + """Get the optimal width for a widget or renderable. + + Args: + renderable (RenderableType): A renderable (including Widget) + max_width ([type], optional): Maximum width. Defaults to 100_000. + + Returns: + int: Number of cells required to render. + """ + measurement = Measurement.get( + self.console, self.console.options.update(max_width=max_width), renderable + ) + return measurement.maximum + def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]: """Get the widget under the given coordinates. @@ -455,7 +471,7 @@ class App(MessagePump): return True async def on_key(self, event: events.Key) -> None: - self.log("App.on_key") + # self.log("App.on_key") await self.press(event.key) async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: diff --git a/src/textual/layout.py b/src/textual/layout.py index dfba4b4e0..ef00f18d8 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -50,25 +50,32 @@ class ReflowResult(NamedTuple): resized: set[Widget] +@rich.repr.auto class LayoutUpdate: - def __init__(self, lines: Lines, x: int, y: int) -> None: + def __init__(self, lines: Lines, region: Region) -> None: self.lines = lines - self.x = x - self.y = y + self.region = region def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: yield Control.home().segment - x = self.x + x = self.region.x new_line = Segment.line() move_to = Control.move_to - for last, (y, line) in loop_last(enumerate(self.lines, self.y)): + for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)): yield move_to(x, y).segment yield from line if not last: yield new_line + def __rich_repr__(self) -> rich.repr.Result: + x, y, width, height = self.region + yield "x", x + yield "y", y + yield "width", width + yield "height", height + class Layout(ABC): """Responsible for arranging Widgets in a view and rendering them.""" @@ -378,6 +385,7 @@ class Layout(ABC): update_region = region.intersection(clip) update_lines = self.render(console, crop=update_region).lines - update = LayoutUpdate(update_lines, update_region.x, update_region.y) + update = LayoutUpdate(update_lines, update_region) + log(update) return update diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index 692dab3ca..d32914481 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -13,17 +13,27 @@ from ..widget import Widget class VerticalLayout(Layout): - def __init__(self, *, z: int = 0, gutter: tuple[int, int] | None = None): + def __init__( + self, + *, + auto_width: bool = False, + z: int = 0, + gutter: tuple[int, int] | None = None + ): + self.auto_width = auto_width self.z = z self.gutter = gutter or (0, 0) self._widgets: list[Widget] = [] + self._max_widget_width = 0 super().__init__() def add(self, widget: Widget) -> None: self._widgets.append(widget) + self._max_widget_width = max(widget.app.measure(widget), self._max_widget_width) def clear(self) -> None: del self._widgets[:] + self._max_widget_width = 0 def get_widgets(self) -> Iterable[Widget]: return self._widgets @@ -34,7 +44,11 @@ class VerticalLayout(Layout): index = 0 width, height = size gutter_height, gutter_width = self.gutter - render_width = width - gutter_width * 2 + render_width = ( + max(width, self._max_widget_width) + gutter_width * 2 + if self.auto_width + else width - gutter_width * 2 + ) x = gutter_width y = gutter_height @@ -54,4 +68,9 @@ class VerticalLayout(Layout): region = Region(x, y, render_width, render_height) add_widget(widget, region - scroll, viewport) + x, y, width, height = map.contents_region + map.contents_region = Region( + x, y, width + self.gutter[0], height + self.gutter[1] + ) + return map diff --git a/src/textual/views/_window_view.py b/src/textual/views/_window_view.py index feab3dfdb..5e424edd6 100644 --- a/src/textual/views/_window_view.py +++ b/src/textual/views/_window_view.py @@ -3,11 +3,11 @@ from __future__ import annotations from rich.console import RenderableType from .. import events -from ..geometry import Offset, Size +from ..geometry import Size from ..layouts.vertical import VerticalLayout from ..view import View from ..message import Message -from ..messages import Update, Layout +from .. import messages from ..widget import Widget from ..widgets import Static @@ -22,10 +22,11 @@ class WindowView(View, layout=VerticalLayout): self, widget: RenderableType | Widget, *, + auto_width: bool = False, gutter: tuple[int, int] = (0, 1), name: str | None = None ) -> None: - layout = VerticalLayout(gutter=gutter) + layout = VerticalLayout(gutter=gutter, auto_width=auto_width) self.widget = widget if isinstance(widget, Widget) else Static(widget) layout.add(self.widget) super().__init__(name=name, layout=layout) @@ -40,10 +41,16 @@ class WindowView(View, layout=VerticalLayout): self.refresh(layout=True) await self.emit(WindowChange(self)) - async def handle_update(self, message: Update) -> None: + async def handle_update(self, message: messages.Update) -> None: message.prevent_default() await self.emit(WindowChange(self)) + async def handle_layout(self, message: messages.Layout) -> None: + self.log("TRANSLATING layout") + self.layout.require_update() + message.stop() + self.refresh() + async def watch_virtual_size(self, size: Size) -> None: await self.emit(WindowChange(self)) diff --git a/src/textual/widgets/_scroll_view.py b/src/textual/widgets/_scroll_view.py index 74338d57e..1a2cf7206 100644 --- a/src/textual/widgets/_scroll_view.py +++ b/src/textual/widgets/_scroll_view.py @@ -21,6 +21,7 @@ class ScrollView(View): self, contents: RenderableType | Widget | None = None, *, + auto_width: bool = False, name: str | None = None, style: StyleType = "", fluid: bool = True, @@ -30,7 +31,9 @@ class ScrollView(View): self.fluid = fluid self.vscroll = ScrollBar(vertical=True) self.hscroll = ScrollBar(vertical=False) - self.window = WindowView("" if contents is None else contents) + self.window = WindowView( + "" if contents is None else contents, auto_width=auto_width + ) layout = GridLayout() layout.add_column("main") layout.add_column("vscroll", size=1)