big table

This commit is contained in:
Will McGugan
2021-09-05 08:46:47 +01:00
parent 3dba37e550
commit 771167c136
8 changed files with 79 additions and 23 deletions

View File

@@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Changed message handlers to use prefix handle\_ - Changed message handlers to use prefix handle\_
- Renamed messages to drop the Message suffix - 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 ## [0.1.10] - 2021-08-25
### Added ### Added

View File

@@ -1,9 +1,8 @@
from rich.table import Table from rich.table import Table
from rich.measure import Measurement
from textual import events from textual import events
from textual.app import App from textual.app import App
from textual.widgets import Header, Footer, ScrollView from textual.widgets import ScrollView
class MyApp(App): class MyApp(App):
@@ -14,19 +13,17 @@ class MyApp(App):
async def on_mount(self, event: events.Mount) -> None: async def on_mount(self, event: events.Mount) -> None:
self.body = body = ScrollView() self.body = body = ScrollView(auto_width=True)
#body.virtual_size.width = 300
await self.view.dock(body) await self.view.dock(body)
async def add_content(): async def add_content():
table = Table(title="Demo", width=1000) table = Table(title="Demo")
for i in range(40): 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): 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) await body.update(table)

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "textual" name = "textual"
version = "0.1.10" version = "0.1.11"
homepage = "https://github.com/willmcgugan/textual" homepage = "https://github.com/willmcgugan/textual"
description = "Text User Interface using Rich" description = "Text User Interface using Rich"
authors = ["Will McGugan <willmcgugan@gmail.com>"] authors = ["Will McGugan <willmcgugan@gmail.com>"]

View File

@@ -10,6 +10,7 @@ from rich.control import Control
import rich.repr import rich.repr
from rich.screen import Screen from rich.screen import Screen
from rich.console import Console, RenderableType from rich.console import Console, RenderableType
from rich.measure import Measurement
from rich.traceback import Traceback from rich.traceback import Traceback
from . import events from . import events
@@ -347,6 +348,21 @@ class App(MessagePump):
except Exception: except Exception:
self.panic() 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]: def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
"""Get the widget under the given coordinates. """Get the widget under the given coordinates.
@@ -455,7 +471,7 @@ class App(MessagePump):
return True return True
async def on_key(self, event: events.Key) -> None: async def on_key(self, event: events.Key) -> None:
self.log("App.on_key") # self.log("App.on_key")
await self.press(event.key) await self.press(event.key)
async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: async def on_shutdown_request(self, event: events.ShutdownRequest) -> None:

View File

@@ -50,25 +50,32 @@ class ReflowResult(NamedTuple):
resized: set[Widget] resized: set[Widget]
@rich.repr.auto
class LayoutUpdate: class LayoutUpdate:
def __init__(self, lines: Lines, x: int, y: int) -> None: def __init__(self, lines: Lines, region: Region) -> None:
self.lines = lines self.lines = lines
self.x = x self.region = region
self.y = y
def __rich_console__( def __rich_console__(
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions
) -> RenderResult: ) -> RenderResult:
yield Control.home().segment yield Control.home().segment
x = self.x x = self.region.x
new_line = Segment.line() new_line = Segment.line()
move_to = Control.move_to 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 move_to(x, y).segment
yield from line yield from line
if not last: if not last:
yield new_line 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): class Layout(ABC):
"""Responsible for arranging Widgets in a view and rendering them.""" """Responsible for arranging Widgets in a view and rendering them."""
@@ -378,6 +385,7 @@ class Layout(ABC):
update_region = region.intersection(clip) update_region = region.intersection(clip)
update_lines = self.render(console, crop=update_region).lines 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 return update

View File

@@ -13,17 +13,27 @@ from ..widget import Widget
class VerticalLayout(Layout): 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.z = z
self.gutter = gutter or (0, 0) self.gutter = gutter or (0, 0)
self._widgets: list[Widget] = [] self._widgets: list[Widget] = []
self._max_widget_width = 0
super().__init__() super().__init__()
def add(self, widget: Widget) -> None: def add(self, widget: Widget) -> None:
self._widgets.append(widget) self._widgets.append(widget)
self._max_widget_width = max(widget.app.measure(widget), self._max_widget_width)
def clear(self) -> None: def clear(self) -> None:
del self._widgets[:] del self._widgets[:]
self._max_widget_width = 0
def get_widgets(self) -> Iterable[Widget]: def get_widgets(self) -> Iterable[Widget]:
return self._widgets return self._widgets
@@ -34,7 +44,11 @@ class VerticalLayout(Layout):
index = 0 index = 0
width, height = size width, height = size
gutter_height, gutter_width = self.gutter 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 x = gutter_width
y = gutter_height y = gutter_height
@@ -54,4 +68,9 @@ class VerticalLayout(Layout):
region = Region(x, y, render_width, render_height) region = Region(x, y, render_width, render_height)
add_widget(widget, region - scroll, viewport) 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 return map

View File

@@ -3,11 +3,11 @@ from __future__ import annotations
from rich.console import RenderableType from rich.console import RenderableType
from .. import events from .. import events
from ..geometry import Offset, Size from ..geometry import Size
from ..layouts.vertical import VerticalLayout from ..layouts.vertical import VerticalLayout
from ..view import View from ..view import View
from ..message import Message from ..message import Message
from ..messages import Update, Layout from .. import messages
from ..widget import Widget from ..widget import Widget
from ..widgets import Static from ..widgets import Static
@@ -22,10 +22,11 @@ class WindowView(View, layout=VerticalLayout):
self, self,
widget: RenderableType | Widget, widget: RenderableType | Widget,
*, *,
auto_width: bool = False,
gutter: tuple[int, int] = (0, 1), gutter: tuple[int, int] = (0, 1),
name: str | None = None name: str | None = 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) self.widget = widget if isinstance(widget, Widget) else Static(widget)
layout.add(self.widget) layout.add(self.widget)
super().__init__(name=name, layout=layout) super().__init__(name=name, layout=layout)
@@ -40,10 +41,16 @@ class WindowView(View, layout=VerticalLayout):
self.refresh(layout=True) self.refresh(layout=True)
await self.emit(WindowChange(self)) 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() message.prevent_default()
await self.emit(WindowChange(self)) 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: async def watch_virtual_size(self, size: Size) -> None:
await self.emit(WindowChange(self)) await self.emit(WindowChange(self))

View File

@@ -21,6 +21,7 @@ class ScrollView(View):
self, self,
contents: RenderableType | Widget | None = None, contents: RenderableType | Widget | None = None,
*, *,
auto_width: bool = False,
name: str | None = None, name: str | None = None,
style: StyleType = "", style: StyleType = "",
fluid: bool = True, fluid: bool = True,
@@ -30,7 +31,9 @@ class ScrollView(View):
self.fluid = fluid self.fluid = fluid
self.vscroll = ScrollBar(vertical=True) self.vscroll = ScrollBar(vertical=True)
self.hscroll = ScrollBar(vertical=False) 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 = GridLayout()
layout.add_column("main") layout.add_column("main")
layout.add_column("vscroll", size=1) layout.add_column("vscroll", size=1)