Aluode/PerceptionLabPortable
0
1from abc import ABC, abstractmethod2from itertools import islice3from operator import itemgetter4from threading import RLock5from 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_resolve18from .align import Align19from .console import Console, ConsoleOptions, RenderableType, RenderResult20from .highlighter import ReprHighlighter21from .panel import Panel22from .pretty import Pretty23from .region import Region24from .repr import Result, rich_repr25from .segment import Segment26from .style import StyleType27 28if TYPE_CHECKING:29 from pip._vendor.rich.tree import Tree30 31 32class LayoutRender(NamedTuple):33 """An individual layout render."""34 35 region: Region36 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 51class _Placeholder: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 = layout58 self.style = style59 60 def __rich_console__(61 self, console: Console, options: ConsoleOptions62 ) -> RenderResult:63 width = options.max_width64 height = options.height or options.size.height65 layout = self.layout66 title = (67 f"{layout.name!r} ({width} x {height})"68 if layout.name69 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 @abstractmethod86 def get_tree_icon(self) -> str:87 """Get the icon (emoji) used in layout.tree"""88 89 @abstractmethod90 def divide(91 self, children: Sequence["Layout"], region: Region92 ) -> 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 101class RowSplitter(Splitter):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 109 def divide(110 self, children: Sequence["Layout"], region: Region111 ) -> Iterable[Tuple["Layout", Region]]:112 x, y, width, height = region113 render_widths = ratio_resolve(width, children)114 offset = 0115 _Region = Region116 for child, child_width in zip(children, render_widths):117 yield child, _Region(x + offset, y, child_width, height)118 offset += child_width119 120 121class ColumnSplitter(Splitter):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 129 def divide(130 self, children: Sequence["Layout"], region: Region131 ) -> Iterable[Tuple["Layout", Region]]:132 x, y, width, height = region133 render_heights = ratio_resolve(height, children)134 offset = 0135 _Region = Region136 for child, child_height in zip(children, render_heights):137 yield child, _Region(x, y + offset, width, child_height)138 offset += child_height139 140 141@rich_repr142class 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 156 def __init__(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 = size168 self.minimum_size = minimum_size169 self.ratio = ratio170 self.name = name171 self.visible = visible172 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, None179 yield "size", self.size, None180 yield "minimum_size", self.minimum_size, 1181 yield "ratio", self.ratio, 1182 183 @property184 def renderable(self) -> RenderableType:185 """Layout renderable."""186 return self if self._children else self._renderable187 188 @property189 def children(self) -> List["Layout"]:190 """Gets (visible) layout children."""191 return [child for child in self._children if child.visible]192 193 @property194 def map(self) -> RenderMap:195 """Get a map of the last render."""196 return self._render_map197 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 self209 else:210 for child in self._children:211 named_layout = child.get(name)212 if named_layout is not None:213 return named_layout214 return None215 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 layout221 222 @property223 def tree(self) -> "Tree":224 """Get a tree renderable to show layout structure."""225 from pip._vendor.rich.styled import Styled226 from pip._vendor.rich.table import Table227 from pip._vendor.rich.tree import Tree228 229 def summary(layout: "Layout") -> Table:230 icon = layout.splitter.get_tree_icon()231 232 table = Table.grid(padding=(0, 1, 0, 0))233 234 text: RenderableType = (235 Pretty(layout) if layout.visible else Styled(Pretty(layout), "dim")236 )237 table.add_row(icon, text)238 _summary = table239 return _summary240 241 layout = self242 tree = Tree(243 summary(layout),244 guide_style=f"layout.tree.{layout.splitter.name}",245 highlight=True,246 )247 248 def recurse(tree: "Tree", layout: "Layout") -> None:249 for child in layout._children:250 recurse(251 tree.add(252 summary(child),253 guide_style=f"layout.tree.{child.splitter.name}",254 ),255 child,256 )257 258 recurse(tree, self)259 return tree260 261 def split(262 self,263 *layouts: Union["Layout", RenderableType],264 splitter: Union[Splitter, str] = "column",265 ) -> None:266 """Split the layout in to multiple sub-layouts.267 268 Args:269 *layouts (Layout): Positional arguments should be (sub) Layout instances.270 splitter (Union[Splitter, str]): Splitter instance or name of splitter.271 """272 _layouts = [273 layout if isinstance(layout, Layout) else Layout(layout)274 for layout in layouts275 ]276 try:277 self.splitter = (278 splitter279 if isinstance(splitter, Splitter)280 else self.splitters[splitter]()281 )282 except KeyError:283 raise NoSplitter(f"No splitter called {splitter!r}")284 self._children[:] = _layouts285 286 def add_split(self, *layouts: Union["Layout", RenderableType]) -> None:287 """Add a new layout(s) to existing split.288 289 Args:290 *layouts (Union[Layout, RenderableType]): Positional arguments should be renderables or (sub) Layout instances.291 292 """293 _layouts = (294 layout if isinstance(layout, Layout) else Layout(layout)295 for layout in layouts296 )297 self._children.extend(_layouts)298 299 def split_row(self, *layouts: Union["Layout", RenderableType]) -> None:300 """Split the layout in to a row (layouts side by side).301 302 Args:303 *layouts (Layout): Positional arguments should be (sub) Layout instances.304 """305 self.split(*layouts, splitter="row")306 307 def split_column(self, *layouts: Union["Layout", RenderableType]) -> None:308 """Split the layout in to a column (layouts stacked on top of each other).309 310 Args:311 *layouts (Layout): Positional arguments should be (sub) Layout instances.312 """313 self.split(*layouts, splitter="column")314 315 def unsplit(self) -> None:316 """Reset splits to initial state."""317 del self._children[:]318 319 def update(self, renderable: RenderableType) -> None:320 """Update renderable.321 322 Args:323 renderable (RenderableType): New renderable object.324 """325 with self._lock:326 self._renderable = renderable327 328 def refresh_screen(self, console: "Console", layout_name: str) -> None:329 """Refresh a sub-layout.330 331 Args:332 console (Console): Console instance where Layout is to be rendered.333 layout_name (str): Name of layout.334 """335 with self._lock:336 layout = self[layout_name]337 region, _lines = self._render_map[layout]338 (x, y, width, height) = region339 lines = console.render_lines(340 layout, console.options.update_dimensions(width, height)341 )342 self._render_map[layout] = LayoutRender(region, lines)343 console.update_screen_lines(lines, x, y)344 345 def _make_region_map(self, width: int, height: int) -> RegionMap:346 """Create a dict that maps layout on to Region."""347 stack: List[Tuple[Layout, Region]] = [(self, Region(0, 0, width, height))]348 push = stack.append349 pop = stack.pop350 layout_regions: List[Tuple[Layout, Region]] = []351 append_layout_region = layout_regions.append352 while stack:353 append_layout_region(pop())354 layout, region = layout_regions[-1]355 children = layout.children356 if children:357 for child_and_region in layout.splitter.divide(children, region):358 push(child_and_region)359 360 region_map = {361 layout: region362 for layout, region in sorted(layout_regions, key=itemgetter(1))363 }364 return region_map365 366 def render(self, console: Console, options: ConsoleOptions) -> RenderMap:367 """Render the sub_layouts.368 369 Args:370 console (Console): Console instance.371 options (ConsoleOptions): Console options.372 373 Returns:374 RenderMap: A dict that maps Layout on to a tuple of Region, lines375 """376 render_width = options.max_width377 render_height = options.height or console.height378 region_map = self._make_region_map(render_width, render_height)379 layout_regions = [380 (layout, region)381 for layout, region in region_map.items()382 if not layout.children383 ]384 render_map: Dict["Layout", "LayoutRender"] = {}385 render_lines = console.render_lines386 update_dimensions = options.update_dimensions387 388 for layout, region in layout_regions:389 lines = render_lines(390 layout.renderable, update_dimensions(region.width, region.height)391 )392 render_map[layout] = LayoutRender(region, lines)393 return render_map394 395 def __rich_console__(396 self, console: Console, options: ConsoleOptions397 ) -> RenderResult:398 with self._lock:399 width = options.max_width or console.width400 height = options.height or console.height401 render_map = self.render(console, options.update_dimensions(width, height))402 self._render_map = render_map403 layout_lines: List[List[Segment]] = [[] for _ in range(height)]404 _islice = islice405 for region, lines in render_map.values():406 _x, y, _layout_width, layout_height = region407 for row, line in zip(408 _islice(layout_lines, y, y + layout_height), lines409 ):410 row.extend(line)411 412 new_line = Segment.line()413 for layout_row in layout_lines:414 yield from layout_row415 yield new_line416 417 418if __name__ == "__main__":419 from pip._vendor.rich.console import Console420 421 console = Console()422 layout = Layout()423 424 layout.split_column(425 Layout(name="header", size=3),426 Layout(ratio=1, name="main"),427 Layout(size=10, name="footer"),428 )429 430 layout["main"].split_row(Layout(name="side"), Layout(name="body", ratio=2))431 432 layout["body"].split_row(Layout(name="content", ratio=2), Layout(name="s2"))433 434 layout["s2"].split_column(435 Layout(name="top"), Layout(name="middle"), Layout(name="bottom")436 )437 438 layout["side"].split_column(Layout(layout.tree, name="left1"), Layout(name="left2"))439 440 layout["content"].update("foo")441 442 console.print(layout)443 