From a0acf61a28370c3b8f9c4c6638fe779722cb195b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 11 Sep 2021 08:51:54 +0100 Subject: [PATCH] simplify arrange --- examples/animation.py | 2 +- src/textual/layout.py | 37 ++++++++++++++------ src/textual/layout_map.py | 27 ++++++++++----- src/textual/layouts/dock.py | 28 +++++++++------ src/textual/layouts/grid.py | 28 ++++++--------- src/textual/layouts/vertical.py | 22 +++++------- src/textual/view.py | 52 ++++++++++++++++------------ src/textual/widgets/_placeholder.py | 1 + src/textual/widgets/_tree_control.py | 12 ++++++- 9 files changed, 124 insertions(+), 85 deletions(-) diff --git a/examples/animation.py b/examples/animation.py index 4531e5114..1d1300c2d 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -22,7 +22,7 @@ class SmoothApp(App): """Called when user hits 'b' key.""" self.show_bar = not self.show_bar - async def on_mount(self, event: events.Mount) -> None: + async def on_mount(self) -> None: """Build layout here.""" footer = Footer() self.bar = Placeholder(name="left") diff --git a/src/textual/layout.py b/src/textual/layout.py index ef00f18d8..baf594d42 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -50,6 +50,14 @@ class ReflowResult(NamedTuple): resized: set[Widget] +class WidgetPlacement(NamedTuple): + + widget: Widget + region: Region + order: tuple[int, ...] + clip: Region + + @rich.repr.auto class LayoutUpdate: def __init__(self, lines: Lines, region: Region) -> None: @@ -103,6 +111,13 @@ class Layout(ABC): def reset(self) -> None: self._cuts = None + def build_map(self, console: Console, scroll: Offset) -> LayoutMap: + size = Size(console.width, console.height) + layout_map = LayoutMap(size) + for widget, region, order, clip in self.arrange(size, size.region, scroll): + layout_map.add_widget(widget, region, order, clip) + return layout_map + def reflow( self, console: Console, width: int, height: int, scroll: Offset ) -> ReflowResult: @@ -111,12 +126,14 @@ class Layout(ABC): self.width = width self.height = height - map = self.generate_map( - console, - Size(width, height), - Region(0, 0, width, height), - scroll, - ) + # map = self.arrange( + # console, + # Size(width, height), + # Region(0, 0, width, height), + # scroll, + # ) + + map = self.build_map(console, scroll) self._require_update = False old_widgets = set() if self.map is None else set(self.map.keys()) @@ -150,9 +167,9 @@ class Layout(ABC): ... @abstractmethod - def generate_map( - self, console: Console, size: Size, viewport: Region, scroll: Offset - ) -> LayoutMap: + def arrange( + self, size: Size, viewport: Region, scroll: Offset + ) -> Iterable[WidgetPlacement]: """Generate a layout map that defines where on the screen the widgets will be drawn. Args: @@ -161,7 +178,7 @@ class Layout(ABC): viewport (Region): Screen relative viewport. Returns: - LayoutMap: [description] + Iterable[WidgetPlacement]: An iterable of widget location """ async def mount_all(self, view: "View") -> None: diff --git a/src/textual/layout_map.py b/src/textual/layout_map.py index aa9488b14..5756d3b9f 100644 --- a/src/textual/layout_map.py +++ b/src/textual/layout_map.py @@ -4,6 +4,7 @@ from rich.console import Console from typing import ItemsView, KeysView, ValuesView, NamedTuple +from . import log from .geometry import Region, Size from .widget import Widget @@ -42,7 +43,6 @@ class LayoutMap: def add_widget( self, - console: Console, widget: Widget, region: Region, order: tuple[int, ...], @@ -50,16 +50,25 @@ class LayoutMap: ) -> None: from .view import View - region += widget.layout_offset - self.widgets[widget] = RenderRegion(region, order, clip) - self.contents_region = self.contents_region.union(region) + if widget in self.widgets: + return + + self.widgets[widget] = RenderRegion(region + widget.layout_offset, order, clip) + self.contents_region = self.contents_region.union(region + widget.layout_offset) if isinstance(widget, View): - sub_map = widget.layout.generate_map( - console, region.size, clip, widget.scroll + widget_placements = list( + widget.layout.arrange(region.size, clip, widget.scroll) ) - widget.virtual_size = sub_map.virtual_size - for sub_widget, (sub_region, sub_order, sub_clip) in sub_map.items(): + total_region = Region(0, 0, 0, 0) + for placement in widget_placements: + total_region = total_region.union(placement.region) + + widget.virtual_size = total_region.size + log(widget, total_region, widget.virtual_size) + for sub_widget, sub_region, sub_order, sub_clip in widget_placements: sub_region += region.origin sub_clip = sub_clip.intersection(clip) - self.add_widget(console, sub_widget, sub_region, sub_order, sub_clip) + # sub_clip = (sub_clip + region.origin).intersection(clip) + # sub_clip = sub_clip + region.origin + self.add_widget(sub_widget, sub_region, sub_order, sub_clip) diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py index 8616c1fea..0027b5391 100644 --- a/src/textual/layouts/dock.py +++ b/src/textual/layouts/dock.py @@ -9,7 +9,7 @@ from rich.console import Console from .._layout_resolve import layout_resolve from ..geometry import Offset, Region, Size -from ..layout import Layout +from ..layout import Layout, WidgetPlacement from ..layout_map import LayoutMap if sys.version_info >= (3, 8): @@ -48,18 +48,15 @@ class DockLayout(Layout): for dock in self.docks: yield from dock.widgets - def generate_map( - self, console: Console, size: Size, viewport: Region, scroll: Offset - ) -> LayoutMap: + def arrange( + self, size: Size, viewport: Region, scroll: Offset + ) -> Iterable[WidgetPlacement]: map: LayoutMap = LayoutMap(size) width, height = size layout_region = Region(0, 0, width, height) layers: dict[int, Region] = defaultdict(lambda: layout_region) - def add_widget(widget: Widget, region: Region, order: tuple[int, ...]): - map.add_widget(console, widget, region, order, viewport) - for index, dock in enumerate(self.docks): dock_options = [ DockOptions( @@ -87,7 +84,9 @@ class DockLayout(Layout): if not layout_size: break total += layout_size - add_widget(widget, Region(x, render_y, width, layout_size), order) + yield WidgetPlacement( + widget, Region(x, render_y, width, layout_size), order, viewport + ) render_y += layout_size remaining = max(0, remaining - layout_size) region = Region(x, y + total, width, height - total) - scroll @@ -104,10 +103,11 @@ class DockLayout(Layout): if not layout_size: break total += layout_size - add_widget( + yield WidgetPlacement( widget, Region(x, render_y - layout_size, width, layout_size), order, + viewport, ) render_y -= layout_size remaining = max(0, remaining - layout_size) @@ -125,7 +125,12 @@ class DockLayout(Layout): if not layout_size: break total += layout_size - add_widget(widget, Region(render_x, y, layout_size, height), order) + yield WidgetPlacement( + widget, + Region(render_x, y, layout_size, height), + order, + viewport, + ) render_x += layout_size remaining = max(0, remaining - layout_size) region = Region(x + total, y, width - total, height) - scroll @@ -142,10 +147,11 @@ class DockLayout(Layout): if not layout_size: break total += layout_size - add_widget( + yield WidgetPlacement( widget, Region(render_x - layout_size, y, layout_size, height), order, + viewport, ) render_x -= layout_size remaining = max(0, remaining - layout_size) diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index 25a7056a9..c0f03908f 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -12,7 +12,7 @@ from rich.console import Console from .._layout_resolve import layout_resolve from ..geometry import Size, Offset, Region -from ..layout import Layout +from ..layout import Layout, WidgetPlacement from ..layout_map import LayoutMap from ..widget import Widget @@ -263,9 +263,9 @@ class GridLayout(Layout): def get_widgets(self) -> Iterable[Widget]: return self.widgets.keys() - def generate_map( - self, console: Console, size: Size, viewport: Region, scroll: Offset - ) -> LayoutMap: + def arrange( + self, size: Size, viewport: Region, scroll: Offset + ) -> Iterable[WidgetPlacement]: """Generate a map that associates widgets with their location on screen. Args: @@ -276,7 +276,6 @@ class GridLayout(Layout): Returns: dict[Widget, OrderedRegion]: [description] """ - map: LayoutMap = LayoutMap(size) width, height = size def resolve( @@ -327,17 +326,6 @@ class GridLayout(Layout): return names, tracks, len(spans), max_size - def add_widget(widget: Widget, region: Region, order: tuple[int, int]): - region -= scroll - map.add_widget(console, widget, region, order, viewport) - # region = region + widget.layout_offset - # map[widget] = RenderRegion(region, order, offset) - # if isinstance(widget, View): - # sub_map = widget.layout.generate_map( - # region.width, region.height, region.origin + offset - # ) - # map.update(sub_map) - container = Size(width - self.column_gutter * 2, height - self.row_gutter * 2) column_names, column_tracks, column_count, column_size = resolve_tracks( [ @@ -390,7 +378,9 @@ class GridLayout(Layout): self.column_align, self.row_align, ) - add_widget(widget, region + gutter, (0, order)) + yield WidgetPlacement( + widget, region + gutter - scroll, (0, order), viewport + ) order += 1 # Widgets with no area assigned. @@ -422,7 +412,9 @@ class GridLayout(Layout): self.column_align, self.row_align, ) - add_widget(widget, region + gutter, (0, order)) + yield WidgetPlacement( + widget, region + gutter - scroll, (0, order), viewport + ) order += 1 return map diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index d32914481..0b7d2c1a0 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -7,7 +7,7 @@ from rich.measure import Measurement from .. import log from ..geometry import Offset, Region, Size -from ..layout import Layout +from ..layout import Layout, WidgetPlacement from ..layout_map import LayoutMap from ..widget import Widget @@ -38,9 +38,9 @@ class VerticalLayout(Layout): def get_widgets(self) -> Iterable[Widget]: return self._widgets - def generate_map( - self, console: Console, size: Size, viewport: Region, scroll: Offset - ) -> LayoutMap: + def arrange( + self, size: Size, viewport: Region, scroll: Offset + ) -> Iterable[WidgetPlacement]: index = 0 width, height = size gutter_height, gutter_width = self.gutter @@ -52,10 +52,6 @@ class VerticalLayout(Layout): x = gutter_width y = gutter_height - map: LayoutMap = LayoutMap(size) - - def add_widget(widget: Widget, region: Region, clip: Region) -> None: - map.add_widget(console, widget, region, (self.z, index), clip) for widget in self._widgets: if ( @@ -66,11 +62,11 @@ class VerticalLayout(Layout): assert widget.render_cache is not None render_height = widget.render_cache.size.height region = Region(x, y, render_width, render_height) - add_widget(widget, region - scroll, viewport) + yield WidgetPlacement(widget, region - scroll, (self.z, index), viewport) - x, y, width, height = map.contents_region - map.contents_region = Region( - x, y, width + self.gutter[0], height + self.gutter[1] - ) + # 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/view.py b/src/textual/view.py index d10e4f3cf..5bbb6373d 100644 --- a/src/textual/view.py +++ b/src/textual/view.py @@ -118,34 +118,42 @@ class View(Widget): self.refresh() async def refresh_layout(self) -> None: - await self.layout.mount_all(self) - if not self.is_root_view: - await self.app.view.refresh_layout() - return + try: + await self.layout.mount_all(self) + if not self.is_root_view: + await self.app.view.refresh_layout() + return - if not self.size: - return + if not self.size: + return - width, height = self.console.size - hidden, shown, resized = self.layout.reflow( - self.console, width, height, self.scroll - ) + # if self.layout._layout_map is not None: + # self.log(self.layout._layout_map.widgets) - assert self.layout.map is not None - self.virtual_size = self.layout.map.virtual_size + width, height = self.console.size + hidden, shown, resized = self.layout.reflow( + self.console, width, height, self.scroll + ) - for widget in hidden: - widget.post_message_no_wait(events.Hide(self)) - for widget in shown: - widget.post_message_no_wait(events.Show(self)) + assert self.layout.map is not None + self.virtual_size = self.layout.map.virtual_size - send_resize = shown - send_resize.update(resized) + for widget in hidden: + widget.post_message_no_wait(events.Hide(self)) + for widget in shown: + widget.post_message_no_wait(events.Show(self)) - for widget, region, unclipped_region in self.layout: - widget._update_size(unclipped_region.size) - if widget in send_resize: - widget.post_message_no_wait(events.Resize(self, unclipped_region.size)) + send_resize = shown + send_resize.update(resized) + + for widget, region, unclipped_region in self.layout: + widget._update_size(unclipped_region.size) + if widget in send_resize: + widget.post_message_no_wait( + events.Resize(self, unclipped_region.size) + ) + except: + self.app.panic() async def on_resize(self, event: events.Resize) -> None: self._update_size(event.size) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index cf99ae554..8cb0cc405 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -10,6 +10,7 @@ import rich.repr from logging import getLogger from .. import events +from ..geometry import Offset from ..widget import Reactive, Widget log = getLogger("rich") diff --git a/src/textual/widgets/_tree_control.py b/src/textual/widgets/_tree_control.py index 5d98d76d9..0a0367684 100644 --- a/src/textual/widgets/_tree_control.py +++ b/src/textual/widgets/_tree_control.py @@ -1,9 +1,9 @@ from __future__ import annotations -from functools import lru_cache from typing import Generic, Iterator, NewType, TypeVar +import rich.repr from rich.console import RenderableType from rich.text import Text, TextType from rich.tree import Tree @@ -24,6 +24,7 @@ NodeID = NewType("NodeID", int) NodeDataType = TypeVar("NodeDataType") +@rich.repr.auto class TreeNode(Generic[NodeDataType]): def __init__( self, @@ -46,6 +47,11 @@ class TreeNode(Generic[NodeDataType]): self._tree.expanded = False self.children: list[TreeNode] = [] + def __rich_repr__(self) -> rich.repr.Result: + yield "id", self.id + yield "label", self.label + yield "data", self.data + @property def control(self) -> TreeControl: return self._control @@ -154,11 +160,15 @@ class TreeNode(Generic[NodeDataType]): return self._control.render_node(self) +@rich.repr.auto class TreeClick(Generic[NodeDataType], Message, bubble=True): def __init__(self, sender: MessageTarget, node: TreeNode[NodeDataType]) -> None: self.node = node super().__init__(sender) + def __rich_repr__(self) -> rich.repr.Result: + yield "node", self.node + class TreeControl(Generic[NodeDataType], Widget): def __init__(