1 from abc
import ABC
, abstractmethod
2 from itertools
import islice
3 from operator
import itemgetter
4 from threading
import RLock
17 from ._ratio
import ratio_resolve
18 from .align
import Align
19 from .console
import Console
, ConsoleOptions
, RenderableType
, RenderResult
20 from .highlighter
import ReprHighlighter
21 from .panel
import Panel
22 from .pretty
import Pretty
23 from .region
import Region
24 from .repr import Result
, rich_repr
25 from .segment
import Segment
26 from .style
import StyleType
29 from pip
._vendor
.rich
.tree
import Tree
32 class LayoutRender(NamedTuple
):
33 """An individual layout render."""
36 render
: List
[List
[Segment
]]
39 RegionMap
= Dict
["Layout", Region
]
40 RenderMap
= Dict
["Layout", LayoutRender
]
43 class LayoutError(Exception):
44 """Layout related error."""
47 class NoSplitter(LayoutError
):
48 """Requested splitter does not exist."""
52 """An internal renderable used as a Layout placeholder."""
54 highlighter
= ReprHighlighter()
56 def __init__(self
, layout
: "Layout", style
: StyleType
= "") -> None:
61 self
, console
: Console
, options
: ConsoleOptions
63 width
= options
.max_width
64 height
= options
.height
or options
.size
.height
67 f
"{layout.name!r} ({width} x {height})"
69 else f
"({width} x {height})"
72 Align
.center(Pretty(layout
), vertical
="middle"),
74 title
=self
.highlighter(title
),
81 """Base class for a splitter."""
86 def get_tree_icon(self
) -> str:
87 """Get the icon (emoji) used in layout.tree"""
91 self
, children
: Sequence
["Layout"], region
: Region
92 ) -> Iterable
[Tuple
["Layout", Region
]]:
93 """Divide a region amongst several child layouts.
96 children (Sequence(Layout)): A number of child layouts.
97 region (Region): A rectangular region to divide.
101 class RowSplitter(Splitter
):
102 """Split a layout region in to rows."""
106 def get_tree_icon(self
) -> str:
107 return "[layout.tree.row]⬌"
110 self
, children
: Sequence
["Layout"], region
: Region
111 ) -> Iterable
[Tuple
["Layout", Region
]]:
112 x
, y
, width
, height
= region
113 render_widths
= ratio_resolve(width
, children
)
116 for child
, child_width
in zip(children
, render_widths
):
117 yield child
, _Region(x
+ offset
, y
, child_width
, height
)
118 offset
+= child_width
121 class ColumnSplitter(Splitter
):
122 """Split a layout region in to columns."""
126 def get_tree_icon(self
) -> str:
127 return "[layout.tree.column]⬍"
130 self
, children
: Sequence
["Layout"], region
: Region
131 ) -> Iterable
[Tuple
["Layout", Region
]]:
132 x
, y
, width
, height
= region
133 render_heights
= ratio_resolve(height
, children
)
136 for child
, child_height
in zip(children
, render_heights
):
137 yield child
, _Region(x
, y
+ offset
, width
, child_height
)
138 offset
+= child_height
143 """A renderable to divide a fixed height in to rows or columns.
146 renderable (RenderableType, optional): Renderable content, or None for placeholder. Defaults to None.
147 name (str, optional): Optional identifier for Layout. Defaults to None.
148 size (int, optional): Optional fixed size of layout. Defaults to None.
149 minimum_size (int, optional): Minimum size of layout. Defaults to 1.
150 ratio (int, optional): Optional ratio for flexible layout. Defaults to 1.
151 visible (bool, optional): Visibility of layout. Defaults to True.
154 splitters
= {"row": RowSplitter, "column": ColumnSplitter}
158 renderable
: Optional
[RenderableType
] = None,
160 name
: Optional
[str] = None,
161 size
: Optional
[int] = None,
162 minimum_size
: int = 1,
164 visible
: bool = True,
166 self
._renderable
= renderable
or _Placeholder(self
)
168 self
.minimum_size
= minimum_size
171 self
.visible
= visible
172 self
.splitter
: Splitter
= self
.splitters
["column"]()
173 self
._children
: List
[Layout
] = []
174 self
._render
_map
: RenderMap
= {}
177 def __rich_repr__(self
) -> Result
:
178 yield "name", self
.name
, None
179 yield "size", self
.size
, None
180 yield "minimum_size", self
.minimum_size
, 1
181 yield "ratio", self
.ratio
, 1
184 def renderable(self
) -> RenderableType
:
185 """Layout renderable."""
186 return self
if self
._children
else self
._renderable
189 def children(self
) -> List
["Layout"]:
190 """Gets (visible) layout children."""
191 return [child
for child
in self
._children
if child
.visible
]
194 def map(self
) -> RenderMap
:
195 """Get a map of the last render."""
196 return self
._render
_map
198 def get(self
, name
: str) -> Optional
["Layout"]:
199 """Get a named layout, or None if it doesn't exist.
202 name (str): Name of layout.
205 Optional[Layout]: Layout instance or None if no layout was found.
207 if self
.name
== name
:
210 for child
in self
._children
:
211 named_layout
= child
.get(name
)
212 if named_layout
is not None:
216 def __getitem__(self
, name
: str) -> "Layout":
217 layout
= self
.get(name
)
219 raise KeyError(f
"No layout with name {name!r}")
223 def tree(self
) -> "Tree":
224 """Get a tree renderable to show layout structure."""
225 from pip
._vendor
.rich
.styled
import Styled
226 from pip
._vendor
.rich
.table
import Table
227 from pip
._vendor
.rich
.tree
import Tree
229 def summary(layout
: "Layout") -> Table
:
231 icon
= layout
.splitter
.get_tree_icon()
233 table
= Table
.grid(padding
=(0, 1, 0, 0))
235 text
: RenderableType
= (
236 Pretty(layout
) if layout
.visible
else Styled(Pretty(layout
), "dim")
238 table
.add_row(icon
, text
)
245 guide_style
=f
"layout.tree.{layout.splitter.name}",
249 def recurse(tree
: "Tree", layout
: "Layout") -> None:
250 for child
in layout
._children
:
254 guide_style
=f
"layout.tree.{child.splitter.name}",
264 *layouts
: Union
["Layout", RenderableType
],
265 splitter
: Union
[Splitter
, str] = "column",
267 """Split the layout in to multiple sub-layouts.
270 *layouts (Layout): Positional arguments should be (sub) Layout instances.
271 splitter (Union[Splitter, str]): Splitter instance or name of splitter.
274 layout
if isinstance(layout
, Layout
) else Layout(layout
)
275 for layout
in layouts
280 if isinstance(splitter
, Splitter
)
281 else self
.splitters
[splitter
]()
284 raise NoSplitter(f
"No splitter called {splitter!r}")
285 self
._children
[:] = _layouts
287 def add_split(self
, *layouts
: Union
["Layout", RenderableType
]) -> None:
288 """Add a new layout(s) to existing split.
291 *layouts (Union[Layout, RenderableType]): Positional arguments should be renderables or (sub) Layout instances.
295 layout
if isinstance(layout
, Layout
) else Layout(layout
)
296 for layout
in layouts
298 self
._children
.extend(_layouts
)
300 def split_row(self
, *layouts
: Union
["Layout", RenderableType
]) -> None:
301 """Split the layout in to a row (layouts side by side).
304 *layouts (Layout): Positional arguments should be (sub) Layout instances.
306 self
.split(*layouts
, splitter
="row")
308 def split_column(self
, *layouts
: Union
["Layout", RenderableType
]) -> None:
309 """Split the layout in to a column (layouts stacked on top of each other).
312 *layouts (Layout): Positional arguments should be (sub) Layout instances.
314 self
.split(*layouts
, splitter
="column")
316 def unsplit(self
) -> None:
317 """Reset splits to initial state."""
318 del self
._children
[:]
320 def update(self
, renderable
: RenderableType
) -> None:
321 """Update renderable.
324 renderable (RenderableType): New renderable object.
327 self
._renderable
= renderable
329 def refresh_screen(self
, console
: "Console", layout_name
: str) -> None:
330 """Refresh a sub-layout.
333 console (Console): Console instance where Layout is to be rendered.
334 layout_name (str): Name of layout.
337 layout
= self
[layout_name
]
338 region
, _lines
= self
._render
_map
[layout
]
339 (x
, y
, width
, height
) = region
340 lines
= console
.render_lines(
341 layout
, console
.options
.update_dimensions(width
, height
)
343 self
._render
_map
[layout
] = LayoutRender(region
, lines
)
344 console
.update_screen_lines(lines
, x
, y
)
346 def _make_region_map(self
, width
: int, height
: int) -> RegionMap
:
347 """Create a dict that maps layout on to Region."""
348 stack
: List
[Tuple
[Layout
, Region
]] = [(self
, Region(0, 0, width
, height
))]
351 layout_regions
: List
[Tuple
[Layout
, Region
]] = []
352 append_layout_region
= layout_regions
.append
354 append_layout_region(pop())
355 layout
, region
= layout_regions
[-1]
356 children
= layout
.children
358 for child_and_region
in layout
.splitter
.divide(children
, region
):
359 push(child_and_region
)
363 for layout
, region
in sorted(layout_regions
, key
=itemgetter(1))
367 def render(self
, console
: Console
, options
: ConsoleOptions
) -> RenderMap
:
368 """Render the sub_layouts.
371 console (Console): Console instance.
372 options (ConsoleOptions): Console options.
375 RenderMap: A dict that maps Layout on to a tuple of Region, lines
377 render_width
= options
.max_width
378 render_height
= options
.height
or console
.height
379 region_map
= self
._make
_region
_map
(render_width
, render_height
)
382 for layout
, region
in region_map
.items()
383 if not layout
.children
385 render_map
: Dict
["Layout", "LayoutRender"] = {}
386 render_lines
= console
.render_lines
387 update_dimensions
= options
.update_dimensions
389 for layout
, region
in layout_regions
:
390 lines
= render_lines(
391 layout
.renderable
, update_dimensions(region
.width
, region
.height
)
393 render_map
[layout
] = LayoutRender(region
, lines
)
396 def __rich_console__(
397 self
, console
: Console
, options
: ConsoleOptions
400 width
= options
.max_width
or console
.width
401 height
= options
.height
or console
.height
402 render_map
= self
.render(console
, options
.update_dimensions(width
, height
))
403 self
._render
_map
= render_map
404 layout_lines
: List
[List
[Segment
]] = [[] for _
in range(height
)]
406 for (region
, lines
) in render_map
.values():
407 _x
, y
, _layout_width
, layout_height
= region
408 for row
, line
in zip(
409 _islice(layout_lines
, y
, y
+ layout_height
), lines
413 new_line
= Segment
.line()
414 for layout_row
in layout_lines
:
415 yield from layout_row
419 if __name__
== "__main__":
420 from pip
._vendor
.rich
.console
import Console
426 Layout(name
="header", size
=3),
427 Layout(ratio
=1, name
="main"),
428 Layout(size
=10, name
="footer"),
431 layout
["main"].split_row(Layout(name
="side"), Layout(name
="body", ratio
=2))
433 layout
["body"].split_row(Layout(name
="content", ratio
=2), Layout(name
="s2"))
435 layout
["s2"].split_column(
436 Layout(name
="top"), Layout(name
="middle"), Layout(name
="bottom")
439 layout
["side"].split_column(Layout(layout
.tree
, name
="left1"), Layout(name
="left2"))
441 layout
["content"].update("foo")
443 console
.print(layout
)