mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
refactor layout
This commit is contained in:
@@ -3,11 +3,11 @@ from typing import Any
|
||||
__all__ = ["log", "panic"]
|
||||
|
||||
|
||||
def log(*args: Any, verbosity: int = 0) -> None:
|
||||
def log(*args: Any, verbosity: int = 0, **kwargs) -> None:
|
||||
from ._context import active_app
|
||||
|
||||
app = active_app.get()
|
||||
app.log(*args, verbosity=verbosity)
|
||||
app.log(*args, verbosity=verbosity, **kwargs)
|
||||
|
||||
|
||||
def panic(*args: Any) -> None:
|
||||
|
||||
@@ -124,7 +124,7 @@ class App(MessagePump):
|
||||
def view(self) -> DockView:
|
||||
return self._view_stack[-1]
|
||||
|
||||
def log(self, *args: Any, verbosity: int = 1) -> None:
|
||||
def log(self, *args: Any, verbosity: int = 1, **kwargs) -> None:
|
||||
"""Write to logs.
|
||||
|
||||
Args:
|
||||
@@ -134,6 +134,10 @@ class App(MessagePump):
|
||||
try:
|
||||
if self.log_file and verbosity <= self.log_verbosity:
|
||||
output = f" ".join(str(arg) for arg in args)
|
||||
if kwargs:
|
||||
output += " " + " ".join(
|
||||
f"{key}={value}" for key, value in kwargs.items()
|
||||
)
|
||||
self.log_file.write(output + "\n")
|
||||
self.log_file.flush()
|
||||
except Exception:
|
||||
@@ -452,6 +456,7 @@ class App(MessagePump):
|
||||
async def broker_event(
|
||||
self, event_name: str, event: events.Event, default_namespace: object | None
|
||||
) -> bool:
|
||||
event.stop()
|
||||
try:
|
||||
style = getattr(event, "style")
|
||||
except AttributeError:
|
||||
@@ -471,7 +476,6 @@ class App(MessagePump):
|
||||
return True
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
# self.log("App.on_key")
|
||||
await self.press(event.key)
|
||||
|
||||
async def on_shutdown_request(self, event: events.ShutdownRequest) -> None:
|
||||
|
||||
@@ -52,10 +52,9 @@ class ReflowResult(NamedTuple):
|
||||
|
||||
class WidgetPlacement(NamedTuple):
|
||||
|
||||
widget: Widget
|
||||
region: Region
|
||||
order: tuple[int, ...]
|
||||
clip: Region
|
||||
widget: Widget | None = None
|
||||
order: tuple[int, ...] = ()
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@@ -111,29 +110,15 @@ 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:
|
||||
def reflow(self, view: View, size: Size) -> ReflowResult:
|
||||
self.reset()
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.width = size.width
|
||||
self.height = size.height
|
||||
|
||||
# map = self.arrange(
|
||||
# console,
|
||||
# Size(width, height),
|
||||
# Region(0, 0, width, height),
|
||||
# scroll,
|
||||
# )
|
||||
map = LayoutMap(size)
|
||||
map.add_widget(view, size.region, (), size.region)
|
||||
|
||||
map = self.build_map(console, scroll)
|
||||
self._require_update = False
|
||||
|
||||
old_widgets = set() if self.map is None else set(self.map.keys())
|
||||
@@ -167,9 +152,7 @@ class Layout(ABC):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def arrange(
|
||||
self, size: Size, viewport: Region, scroll: Offset
|
||||
) -> Iterable[WidgetPlacement]:
|
||||
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
|
||||
"""Generate a layout map that defines where on the screen the widgets will be drawn.
|
||||
|
||||
Args:
|
||||
@@ -403,6 +386,4 @@ class Layout(ABC):
|
||||
update_region = region.intersection(clip)
|
||||
update_lines = self.render(console, crop=update_region).lines
|
||||
update = LayoutUpdate(update_lines, update_region)
|
||||
log(update)
|
||||
|
||||
return update
|
||||
|
||||
@@ -19,13 +19,8 @@ class RenderRegion(NamedTuple):
|
||||
class LayoutMap:
|
||||
def __init__(self, size: Size) -> None:
|
||||
self.size = size
|
||||
self.contents_region = Region(0, 0, 0, 0)
|
||||
self.widgets: dict[Widget, RenderRegion] = {}
|
||||
|
||||
@property
|
||||
def virtual_size(self) -> Size:
|
||||
return self.contents_region.size
|
||||
|
||||
def __getitem__(self, widget: Widget) -> RenderRegion:
|
||||
return self.widgets[widget]
|
||||
|
||||
@@ -54,21 +49,20 @@ class LayoutMap:
|
||||
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):
|
||||
widget_placements = list(
|
||||
widget.layout.arrange(region.size, clip, widget.scroll)
|
||||
)
|
||||
total_region = Region(0, 0, 0, 0)
|
||||
for placement in widget_placements:
|
||||
total_region = total_region.union(placement.region)
|
||||
scroll = widget.scroll
|
||||
total_region = region.size.region
|
||||
sub_clip = clip.intersection(region)
|
||||
|
||||
arrangement = widget.layout.arrange(region.size, scroll)
|
||||
for sub_region, sub_widget, sub_order in arrangement:
|
||||
total_region = total_region.union(sub_region)
|
||||
if sub_widget is not None:
|
||||
self.add_widget(
|
||||
sub_widget,
|
||||
sub_region + region.origin - scroll,
|
||||
sub_order,
|
||||
sub_clip,
|
||||
)
|
||||
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)
|
||||
# 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)
|
||||
|
||||
@@ -48,9 +48,7 @@ class DockLayout(Layout):
|
||||
for dock in self.docks:
|
||||
yield from dock.widgets
|
||||
|
||||
def arrange(
|
||||
self, size: Size, viewport: Region, scroll: Offset
|
||||
) -> Iterable[WidgetPlacement]:
|
||||
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
|
||||
|
||||
map: LayoutMap = LayoutMap(size)
|
||||
width, height = size
|
||||
@@ -85,11 +83,11 @@ class DockLayout(Layout):
|
||||
break
|
||||
total += layout_size
|
||||
yield WidgetPlacement(
|
||||
widget, Region(x, render_y, width, layout_size), order, viewport
|
||||
Region(x, render_y, width, layout_size), widget, order
|
||||
)
|
||||
render_y += layout_size
|
||||
remaining = max(0, remaining - layout_size)
|
||||
region = Region(x, y + total, width, height - total) - scroll
|
||||
region = Region(x, y + total, width, height - total)
|
||||
|
||||
elif dock.edge == "bottom":
|
||||
sizes = layout_resolve(height, dock_options)
|
||||
@@ -104,14 +102,13 @@ class DockLayout(Layout):
|
||||
break
|
||||
total += layout_size
|
||||
yield WidgetPlacement(
|
||||
widget,
|
||||
Region(x, render_y - layout_size, width, layout_size),
|
||||
widget,
|
||||
order,
|
||||
viewport,
|
||||
)
|
||||
render_y -= layout_size
|
||||
remaining = max(0, remaining - layout_size)
|
||||
region = Region(x, y, width, height - total) - scroll
|
||||
region = Region(x, y, width, height - total)
|
||||
|
||||
elif dock.edge == "left":
|
||||
sizes = layout_resolve(width, dock_options)
|
||||
@@ -126,14 +123,13 @@ class DockLayout(Layout):
|
||||
break
|
||||
total += layout_size
|
||||
yield WidgetPlacement(
|
||||
widget,
|
||||
Region(render_x, y, layout_size, height),
|
||||
widget,
|
||||
order,
|
||||
viewport,
|
||||
)
|
||||
render_x += layout_size
|
||||
remaining = max(0, remaining - layout_size)
|
||||
region = Region(x + total, y, width - total, height) - scroll
|
||||
region = Region(x + total, y, width - total, height)
|
||||
|
||||
elif dock.edge == "right":
|
||||
sizes = layout_resolve(width, dock_options)
|
||||
@@ -148,14 +144,13 @@ class DockLayout(Layout):
|
||||
break
|
||||
total += layout_size
|
||||
yield WidgetPlacement(
|
||||
widget,
|
||||
Region(render_x - layout_size, y, layout_size, height),
|
||||
widget,
|
||||
order,
|
||||
viewport,
|
||||
)
|
||||
render_x -= layout_size
|
||||
remaining = max(0, remaining - layout_size)
|
||||
region = Region(x, y, width - total, height) - scroll
|
||||
region = Region(x, y, width - total, height)
|
||||
|
||||
layers[dock.z] = region
|
||||
|
||||
|
||||
@@ -263,9 +263,7 @@ class GridLayout(Layout):
|
||||
def get_widgets(self) -> Iterable[Widget]:
|
||||
return self.widgets.keys()
|
||||
|
||||
def arrange(
|
||||
self, size: Size, viewport: Region, scroll: Offset
|
||||
) -> Iterable[WidgetPlacement]:
|
||||
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
|
||||
"""Generate a map that associates widgets with their location on screen.
|
||||
|
||||
Args:
|
||||
@@ -378,9 +376,7 @@ class GridLayout(Layout):
|
||||
self.column_align,
|
||||
self.row_align,
|
||||
)
|
||||
yield WidgetPlacement(
|
||||
widget, region + gutter - scroll, (0, order), viewport
|
||||
)
|
||||
yield WidgetPlacement(region + gutter, widget, (0, order))
|
||||
order += 1
|
||||
|
||||
# Widgets with no area assigned.
|
||||
@@ -412,9 +408,7 @@ class GridLayout(Layout):
|
||||
self.column_align,
|
||||
self.row_align,
|
||||
)
|
||||
yield WidgetPlacement(
|
||||
widget, region + gutter - scroll, (0, order), viewport
|
||||
)
|
||||
yield WidgetPlacement(region + gutter, widget, (0, order))
|
||||
order += 1
|
||||
|
||||
return map
|
||||
|
||||
@@ -38,9 +38,7 @@ class VerticalLayout(Layout):
|
||||
def get_widgets(self) -> Iterable[Widget]:
|
||||
return self._widgets
|
||||
|
||||
def arrange(
|
||||
self, size: Size, viewport: Region, scroll: Offset
|
||||
) -> Iterable[WidgetPlacement]:
|
||||
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
|
||||
index = 0
|
||||
width, height = size
|
||||
gutter_height, gutter_width = self.gutter
|
||||
@@ -53,6 +51,8 @@ class VerticalLayout(Layout):
|
||||
x = gutter_width
|
||||
y = gutter_height
|
||||
|
||||
total_region = size.region
|
||||
|
||||
for widget in self._widgets:
|
||||
if (
|
||||
not widget.render_cache
|
||||
@@ -62,11 +62,13 @@ 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)
|
||||
yield WidgetPlacement(widget, region - scroll, (self.z, index), viewport)
|
||||
yield WidgetPlacement(region, widget, (self.z, index))
|
||||
total_region = total_region.union(region)
|
||||
|
||||
yield WidgetPlacement(total_region)
|
||||
# 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
|
||||
|
||||
@@ -65,8 +65,8 @@ class MessagePump:
|
||||
def is_running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
def log(self, *args) -> None:
|
||||
return self.app.log(*args)
|
||||
def log(self, *args, **kwargs) -> None:
|
||||
return self.app.log(*args, **kwargs)
|
||||
|
||||
def set_parent(self, parent: MessagePump) -> None:
|
||||
self._parent = parent
|
||||
|
||||
@@ -43,20 +43,18 @@ class View(Widget):
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
background: Reactive[str] = Reactive("")
|
||||
scroll_x: Reactive[int] = Reactive(0)
|
||||
scroll_y: Reactive[int] = Reactive(0)
|
||||
virtual_size = Reactive(Size(0, 0))
|
||||
|
||||
async def watch_background(self, value: str) -> None:
|
||||
self.layout.background = value
|
||||
self.app.refresh()
|
||||
|
||||
scroll_x: Reactive[int] = Reactive(0)
|
||||
scroll_y: Reactive[int] = Reactive(0)
|
||||
|
||||
@property
|
||||
def scroll(self) -> Offset:
|
||||
return Offset(self.scroll_x, self.scroll_y)
|
||||
|
||||
virtual_size: Reactive[Size] = Reactive(Size(0, 0))
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
@@ -127,16 +125,10 @@ class View(Widget):
|
||||
if not self.size:
|
||||
return
|
||||
|
||||
# if self.layout._layout_map is not None:
|
||||
# self.log(self.layout._layout_map.widgets)
|
||||
|
||||
width, height = self.console.size
|
||||
hidden, shown, resized = self.layout.reflow(
|
||||
self.console, width, height, self.scroll
|
||||
)
|
||||
hidden, shown, resized = self.layout.reflow(self, Size(*self.console.size))
|
||||
|
||||
assert self.layout.map is not None
|
||||
self.virtual_size = self.layout.map.virtual_size
|
||||
# self.virtual_size = self.layout.map.virtual_size
|
||||
|
||||
for widget in hidden:
|
||||
widget.post_message_no_wait(events.Hide(self))
|
||||
|
||||
@@ -173,16 +173,16 @@ class ScrollView(View):
|
||||
self.animate("x", self.target_x, duration=1, easing="out_cubic")
|
||||
self.animate("y", self.target_y, duration=1, easing="out_cubic")
|
||||
|
||||
async def handle_scroll_up(self, message: Message) -> None:
|
||||
async def handle_scroll_up(self) -> None:
|
||||
self.page_up()
|
||||
|
||||
async def handle_scroll_down(self, message: Message) -> None:
|
||||
async def handle_scroll_down(self) -> None:
|
||||
self.page_down()
|
||||
|
||||
async def handle_scroll_left(self, message: Message) -> None:
|
||||
async def handle_scroll_left(self) -> None:
|
||||
self.page_left()
|
||||
|
||||
async def handle_scroll_right(self, message: Message) -> None:
|
||||
async def handle_scroll_right(self) -> None:
|
||||
self.page_right()
|
||||
|
||||
async def handle_scroll_to(self, message: ScrollTo) -> None:
|
||||
@@ -193,11 +193,10 @@ class ScrollView(View):
|
||||
self.animate("x", self.target_x, speed=150, easing="out_cubic")
|
||||
self.animate("y", self.target_y, speed=150, easing="out_cubic")
|
||||
|
||||
def handle_window_change(self) -> None:
|
||||
virtual_width, virtual_height = self.virtual_size
|
||||
def handle_window_change(self, message) -> None:
|
||||
virtual_width, virtual_height = self.window.virtual_size
|
||||
width, height = self.size
|
||||
|
||||
self.log(self.virtual_size, self.size)
|
||||
self.x = self.validate_x(self.x)
|
||||
self.y = self.validate_y(self.y)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user