diff --git a/examples/grid.py b/examples/grid.py new file mode 100644 index 000000000..16c16b1f6 --- /dev/null +++ b/examples/grid.py @@ -0,0 +1,36 @@ +from textual.app import App +from textual import events +from textual.view import View +from textual.widgets import Placeholder +from textual.layouts.grid import GridLayout + + +class GridTest(App): + async def on_load(self, event: events.Load) -> None: + await self.bind("q,ctrl+c", "quit", "Quit") + + async def on_startup(self, event: events.Startup) -> None: + + layout = GridLayout() + view = await self.push_view(View(layout=layout)) + + layout.add_column(fraction=1, name="left", minimum_size=20) + layout.add_column(size=30, name="center") + layout.add_column(fraction=1, name="right") + + layout.add_row(fraction=1, name="top") + layout.add_row(fraction=2, name="middle") + layout.add_row(fraction=1, name="bottom") + + layout.add_area("area1", "left", "top") + layout.add_area("area2", "center", "middle") + layout.add_area("area3", ("left-start", "right-end"), "bottom") + layout.add_area("area4", "right", ("top-start", "middle-end")) + + await view.mount(layout.add_widget(Placeholder(name="area1"), "area1")) + await view.mount(layout.add_widget(Placeholder(name="area2"), "area2")) + await view.mount(layout.add_widget(Placeholder(name="area3"), "area3")) + await view.mount(layout.add_widget(Placeholder(name="area4"), "area4")) + + +GridTest.run(title="Grid Test") \ No newline at end of file diff --git a/src/textual/app.py b/src/textual/app.py index f74fc0530..96999a46f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -25,7 +25,7 @@ from ._context import active_app from ._event_broker import extract_handler_actions, NoHandler from .keys import Binding from .driver import Driver -from .layouts.dock import DockLayout, Dock, DockEdge, DockOptions +from .layouts.dock import DockLayout, Dock from ._linux_driver import LinuxDriver from .message_pump import MessagePump from .message import Message diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py index c795b1c9c..63d77169b 100644 --- a/src/textual/layouts/dock.py +++ b/src/textual/layouts/dock.py @@ -4,13 +4,12 @@ import sys from collections import defaultdict from dataclasses import dataclass import logging -from typing import TYPE_CHECKING, Mapping, Sequence +from typing import TYPE_CHECKING, Sequence from rich._ratio import ratio_resolve from ..geometry import Region, Point from ..layout import Layout, OrderedRegion -from .._types import Lines if sys.version_info >= (3, 8): from typing import Literal diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py new file mode 100644 index 000000000..6b5688e33 --- /dev/null +++ b/src/textual/layouts/grid.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import NamedTuple + +from rich._ratio import ratio_resolve + +from ..geometry import Point, Region +from ..layout import Layout, OrderedRegion +from ..widget import Widget + + +@dataclass +class GridOptions: + size: int | None = None + fraction: int = 1 + minimum_size: int = 1 + name: str | None = None + + @property + def ratio(self) -> int: + return self.fraction + + +class GridArea(NamedTuple): + col_start: str + col_end: str + row_start: str + row_end: str + + +class GridLayout(Layout): + def __init__(self) -> None: + self.columns: list[GridOptions] = [] + self.rows: list[GridOptions] = [] + self.areas: dict[str, GridArea] = {} + self.widgets: dict[Widget, str | None] = {} + super().__init__() + + def add_column( + self, + *, + size: int | None = None, + fraction: int = 1, + minimum_size: int = 1, + name: str | None = None, + ) -> None: + options = GridOptions( + size=size, fraction=fraction, minimum_size=minimum_size, name=name + ) + self.columns.append(options) + + def add_row( + self, + *, + size: int | None = None, + fraction: int = 1, + minimum_size: int = 1, + name: str | None = None, + ) -> None: + options = GridOptions( + size=size, fraction=fraction, minimum_size=minimum_size, name=name + ) + self.rows.append(options) + + def add_area( + self, name: str, columns: str | tuple[str, str], rows: str | tuple[str, str] + ): + if isinstance(columns, str): + column_start = f"{columns}-start" + column_end = f"{columns}-end" + else: + column_start, column_end = columns + + if isinstance(rows, str): + row_start = f"{rows}-start" + row_end = f"{rows}-end" + else: + row_start, row_end = rows + + area = GridArea(column_start, column_end, row_start, row_end) + self.areas[name] = area + + def add_widget(self, widget: Widget, area: str | None = None) -> Widget: + self.widgets[widget] = area + return widget + + def generate_map( + self, width: int, height: int, offset: Point = Point(0, 0) + ) -> dict[Widget, OrderedRegion]: + def resolve(size, edges) -> list[int]: + sizes = ratio_resolve(size, edges) + total = 0 + for _size in sizes: + total += _size + yield total + + columns = [ + ("", 0), + *( + (column.name, column_width) + for column, column_width in zip( + self.columns, resolve(width, self.columns) + ) + ), + ] + column_tracks = {} + for (_, track1), (name2, track2) in zip(columns, columns[1:]): + column_tracks[f"{name2}-end"] = track2 + column_tracks[f"{name2}-start"] = track1 + + rows = [ + ("", 0), + *( + (row.name, column_height) + for row, column_height in zip(self.rows, resolve(height, self.rows)) + ), + ] + row_tracks = {} + for (_, track1), (name2, track2) in zip(rows, rows[1:]): + row_tracks[f"{name2}-end"] = track2 + row_tracks[f"{name2}-start"] = track1 + + order = 1 + + map = {} + for widget, area in self.widgets.items(): + if not area: + continue + column_start, column_end, row_start, row_end = self.areas[area] + x1 = column_tracks[column_start] + x2 = column_tracks[column_end] + y1 = row_tracks[row_start] + y2 = row_tracks[row_end] + map[widget] = OrderedRegion(Region.from_corners(x1, y1, x2, y2), (0, order)) + order += 1 + + return map + + +if __name__ == "__main__": + layout = GridLayout() + layout.add_column(size=20, name="left") + layout.add_column(fraction=2, name="middle") + layout.add_column(fraction=1, name="right") + + layout.add_row(fraction=1, name="top") + layout.add_row(fraction=2, name="bottom") + + layout.add_area("center", "middle", "top") + + from ..widgets import Static + + layout.add_widget(Static("foo"), "center") + + from rich import print + + print(layout.widgets) + + map = layout.generate_map(100, 80) + print(map) \ No newline at end of file diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 324a520e5..1b3e37e5e 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -27,7 +27,9 @@ class Placeholder(Widget, can_focus=True): def render(self) -> RenderableType: return Panel( - Align.center(Pretty(self), vertical="middle"), + Align.center( + Pretty(self, no_wrap=True, overflow="ellipsis"), vertical="middle" + ), title=self.__class__.__name__, border_style="green" if self.mouse_over else "blue", box=box.HEAVY if self.has_focus else box.ROUNDED,