Let us walk on the 3-isogeny graph
Loading...
Searching...
No Matches
layout.py
Go to the documentation of this file.
1from abc import ABC, abstractmethod
2from itertools import islice
3from operator import itemgetter
4from threading import RLock
5from typing import (
6 TYPE_CHECKING,
7 Dict,
8 Iterable,
9 List,
10 NamedTuple,
11 Optional,
12 Sequence,
13 Tuple,
14 Union,
15)
16
17from ._ratio import ratio_resolve
18from .align import Align
19from .console import Console, ConsoleOptions, RenderableType, RenderResult
20from .highlighter import ReprHighlighter
21from .panel import Panel
22from .pretty import Pretty
23from .region import Region
24from .repr import Result, rich_repr
25from .segment import Segment
26from .style import StyleType
27
28if TYPE_CHECKING:
29 from pip._vendor.rich.tree import Tree
30
31
32class LayoutRender(NamedTuple):
33 """An individual layout render."""
34
35 region: Region
36 render: List[List[Segment]]
37
38
39RegionMap = Dict["Layout", Region]
40RenderMap = Dict["Layout", LayoutRender]
41
42
43class LayoutError(Exception):
44 """Layout related error."""
45
46
47class NoSplitter(LayoutError):
48 """Requested splitter does not exist."""
49
50
52 """An internal renderable used as a Layout placeholder."""
53
54 highlighter = ReprHighlighter()
55
56 def __init__(self, layout: "Layout", style: StyleType = "") -> None:
57 self.layout = layout
58 self.style = style
59
61 self, console: Console, options: ConsoleOptions
62 ) -> RenderResult:
63 width = options.max_width
65 layout = self.layout
66 title = (
67 f"{layout.name!r} ({width} x {height})"
69 else f"({width} x {height})"
70 )
71 yield Panel(
72 Align.center(Pretty(layout), vertical="middle"),
73 style=self.style,
74 title=self.highlighter(title),
75 border_style="blue",
76 height=height,
77 )
78
79
80class Splitter(ABC):
81 """Base class for a splitter."""
82
83 name: str = ""
84
85 @abstractmethod
86 def get_tree_icon(self) -> str:
87 """Get the icon (emoji) used in layout.tree"""
88
89 @abstractmethod
90 def divide(
91 self, children: Sequence["Layout"], region: Region
92 ) -> Iterable[Tuple["Layout", Region]]:
93 """Divide a region amongst several child layouts.
94
95 Args:
96 children (Sequence(Layout)): A number of child layouts.
97 region (Region): A rectangular region to divide.
98 """
99
100
102 """Split a layout region in to rows."""
103
104 name = "row"
105
106 def get_tree_icon(self) -> str:
107 return "[layout.tree.row]⬌"
108
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)
114 offset = 0
115 _Region = Region
116 for child, child_width in zip(children, render_widths):
117 yield child, _Region(x + offset, y, child_width, height)
118 offset += child_width
119
120
122 """Split a layout region in to columns."""
123
124 name = "column"
125
126 def get_tree_icon(self) -> str:
127 return "[layout.tree.column]⬍"
128
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)
134 offset = 0
135 _Region = Region
136 for child, child_height in zip(children, render_heights):
137 yield child, _Region(x, y + offset, width, child_height)
138 offset += child_height
139
140
141@rich_repr
142class Layout:
143 """A renderable to divide a fixed height in to rows or columns.
144
145 Args:
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.
152 """
153
154 splitters = {"row": RowSplitter, "column": ColumnSplitter}
155
157 self,
158 renderable: Optional[RenderableType] = None,
159 *,
160 name: Optional[str] = None,
161 size: Optional[int] = None,
162 minimum_size: int = 1,
163 ratio: int = 1,
164 visible: bool = True,
165 ) -> None:
166 self._renderable = renderable or _Placeholder(self)
167 self.size = size
168 self.minimum_size = minimum_size
169 self.ratio = ratio
170 self.name = name
171 self.visible = visible
172 self.splitter: Splitter = self.splitters["column"]()
173 self._children: List[Layout] = []
174 self._render_map: RenderMap = {}
175 self._lock = RLock()
176
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
182
183 @property
184 def renderable(self) -> RenderableType:
185 """Layout renderable."""
186 return self if self._children else self._renderable
187
188 @property
189 def children(self) -> List["Layout"]:
190 """Gets (visible) layout children."""
191 return [child for child in self._children if child.visible]
192
193 @property
194 def map(self) -> RenderMap:
195 """Get a map of the last render."""
196 return self._render_map
197
198 def get(self, name: str) -> Optional["Layout"]:
199 """Get a named layout, or None if it doesn't exist.
200
201 Args:
202 name (str): Name of layout.
203
204 Returns:
205 Optional[Layout]: Layout instance or None if no layout was found.
206 """
207 if self.name == name:
208 return self
209 else:
210 for child in self._children:
211 named_layout = child.get(name)
212 if named_layout is not None:
213 return named_layout
214 return None
215
216 def __getitem__(self, name: str) -> "Layout":
217 layout = self.get(name)
218 if layout is None:
219 raise KeyError(f"No layout with name {name!r}")
220 return layout
221
222 @property
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
228
229 def summary(layout: "Layout") -> Table:
230
232
233 table = Table.grid(padding=(0, 1, 0, 0))
234
235 text: RenderableType = (
236 Pretty(layout) if layout.visible else Styled(Pretty(layout), "dim")
237 )
238 table.add_row(icon, text)
239 _summary = table
240 return _summary
241
242 layout = self
243 tree = Tree(
244 summary(layout),
245 guide_style=f"layout.tree.{layout.splitter.name}",
246 highlight=True,
247 )
248
249 def recurse(tree: "Tree", layout: "Layout") -> None:
250 for child in layout._children:
251 recurse(
252 tree.add(
253 summary(child),
254 guide_style=f"layout.tree.{child.splitter.name}",
255 ),
256 child,
257 )
258
259 recurse(tree, self)
260 return tree
261
262 def split(
263 self,
264 *layouts: Union["Layout", RenderableType],
265 splitter: Union[Splitter, str] = "column",
266 ) -> None:
267 """Split the layout in to multiple sub-layouts.
268
269 Args:
270 *layouts (Layout): Positional arguments should be (sub) Layout instances.
271 splitter (Union[Splitter, str]): Splitter instance or name of splitter.
272 """
273 _layouts = [
274 layout if isinstance(layout, Layout) else Layout(layout)
275 for layout in layouts
276 ]
277 try:
278 self.splitter = (
279 splitter
280 if isinstance(splitter, Splitter)
281 else self.splitters[splitter]()
282 )
283 except KeyError:
284 raise NoSplitter(f"No splitter called {splitter!r}")
285 self._children[:] = _layouts
286
287 def add_split(self, *layouts: Union["Layout", RenderableType]) -> None:
288 """Add a new layout(s) to existing split.
289
290 Args:
291 *layouts (Union[Layout, RenderableType]): Positional arguments should be renderables or (sub) Layout instances.
292
293 """
294 _layouts = (
295 layout if isinstance(layout, Layout) else Layout(layout)
296 for layout in layouts
297 )
298 self._children.extend(_layouts)
299
300 def split_row(self, *layouts: Union["Layout", RenderableType]) -> None:
301 """Split the layout in to a row (layouts side by side).
302
303 Args:
304 *layouts (Layout): Positional arguments should be (sub) Layout instances.
305 """
306 self.split(*layouts, splitter="row")
307
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).
310
311 Args:
312 *layouts (Layout): Positional arguments should be (sub) Layout instances.
313 """
314 self.split(*layouts, splitter="column")
315
316 def unsplit(self) -> None:
317 """Reset splits to initial state."""
318 del self._children[:]
319
320 def update(self, renderable: RenderableType) -> None:
321 """Update renderable.
322
323 Args:
324 renderable (RenderableType): New renderable object.
325 """
326 with self._lock:
327 self._renderable = renderable
328
329 def refresh_screen(self, console: "Console", layout_name: str) -> None:
330 """Refresh a sub-layout.
331
332 Args:
333 console (Console): Console instance where Layout is to be rendered.
334 layout_name (str): Name of layout.
335 """
336 with self._lock:
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)
342 )
343 self._render_map[layout] = LayoutRender(region, lines)
344 console.update_screen_lines(lines, x, y)
345
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))]
349 push = stack.append
350 pop = stack.pop
351 layout_regions: List[Tuple[Layout, Region]] = []
352 append_layout_region = layout_regions.append
353 while stack:
355 layout, region = layout_regions[-1]
356 children = layout.children
357 if children:
358 for child_and_region in layout.splitter.divide(children, region):
359 push(child_and_region)
360
361 region_map = {
362 layout: region
363 for layout, region in sorted(layout_regions, key=itemgetter(1))
364 }
365 return region_map
366
367 def render(self, console: Console, options: ConsoleOptions) -> RenderMap:
368 """Render the sub_layouts.
369
370 Args:
371 console (Console): Console instance.
372 options (ConsoleOptions): Console options.
373
374 Returns:
375 RenderMap: A dict that maps Layout on to a tuple of Region, lines
376 """
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)
380 layout_regions = [
381 (layout, region)
382 for layout, region in region_map.items()
383 if not layout.children
384 ]
385 render_map: Dict["Layout", "LayoutRender"] = {}
386 render_lines = console.render_lines
387 update_dimensions = options.update_dimensions
388
389 for layout, region in layout_regions:
390 lines = render_lines(
391 layout.renderable, update_dimensions(region.width, region.height)
392 )
393 render_map[layout] = LayoutRender(region, lines)
394 return render_map
395
397 self, console: Console, options: ConsoleOptions
398 ) -> RenderResult:
399 with self._lock:
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)]
405 _islice = islice
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
410 ):
411 row.extend(line)
412
413 new_line = Segment.line()
414 for layout_row in layout_lines:
415 yield from layout_row
416 yield new_line
417
418
419if __name__ == "__main__":
420 from pip._vendor.rich.console import Console
421
422 console = Console()
423 layout = Layout()
424
426 Layout(name="header", size=3),
427 Layout(ratio=1, name="main"),
428 Layout(size=10, name="footer"),
429 )
430
431 layout["main"].split_row(Layout(name="side"), Layout(name="body", ratio=2))
432
433 layout["body"].split_row(Layout(name="content", ratio=2), Layout(name="s2"))
434
435 layout["s2"].split_column(
436 Layout(name="top"), Layout(name="middle"), Layout(name="bottom")
437 )
438
439 layout["side"].split_column(Layout(layout.tree, name="left1"), Layout(name="left2"))
440
441 layout["content"].update("foo")
442
443 console.print(layout)
Iterable[Tuple["Layout", Region]] divide(self, Sequence["Layout"] children, Region region)
Definition layout.py:131
None refresh_screen(self, "Console" console, str layout_name)
Definition layout.py:329
RenderMap render(self, Console console, ConsoleOptions options)
Definition layout.py:367
None add_split(self, *Union["Layout", RenderableType] layouts)
Definition layout.py:287
None split_row(self, *Union["Layout", RenderableType] layouts)
Definition layout.py:300
RenderableType renderable(self)
Definition layout.py:184
RenderResult __rich_console__(self, Console console, ConsoleOptions options)
Definition layout.py:398
"Layout" __getitem__(self, str name)
Definition layout.py:216
None split_column(self, *Union["Layout", RenderableType] layouts)
Definition layout.py:308
None __init__(self, Optional[RenderableType] renderable=None, *Optional[str] name=None, Optional[int] size=None, int minimum_size=1, int ratio=1, bool visible=True)
Definition layout.py:165
None update(self, RenderableType renderable)
Definition layout.py:320
None split(self, *Union["Layout", RenderableType] layouts, Union[Splitter, str] splitter="column")
Definition layout.py:266
Optional["Layout"] get(self, str name)
Definition layout.py:198
RegionMap _make_region_map(self, int width, int height)
Definition layout.py:346
List["Layout"] children(self)
Definition layout.py:189
Iterable[Tuple["Layout", Region]] divide(self, Sequence["Layout"] children, Region region)
Definition layout.py:111
Iterable[Tuple["Layout", Region]] divide(self, Sequence["Layout"] children, Region region)
Definition layout.py:92
None __init__(self, "Layout" layout, StyleType style="")
Definition layout.py:56
RenderResult __rich_console__(self, Console console, ConsoleOptions options)
Definition layout.py:62
for i