| from abc import ABC, abstractmethod |
| from itertools import islice |
| from operator import itemgetter |
| from threading import RLock |
| from typing import ( |
| TYPE_CHECKING, |
| Dict, |
| Iterable, |
| List, |
| NamedTuple, |
| Optional, |
| Sequence, |
| Tuple, |
| Union, |
| ) |
| |
| from ._ratio import ratio_resolve |
| from .align import Align |
| from .console import Console, ConsoleOptions, RenderableType, RenderResult |
| from .highlighter import ReprHighlighter |
| from .panel import Panel |
| from .pretty import Pretty |
| from .repr import rich_repr, Result |
| from .region import Region |
| from .segment import Segment |
| from .style import StyleType |
| |
| if TYPE_CHECKING: |
| from pip._vendor.rich.tree import Tree |
| |
| |
| class LayoutRender(NamedTuple): |
| """An individual layout render.""" |
| |
| region: Region |
| render: List[List[Segment]] |
| |
| |
| RegionMap = Dict["Layout", Region] |
| RenderMap = Dict["Layout", LayoutRender] |
| |
| |
| class LayoutError(Exception): |
| """Layout related error.""" |
| |
| |
| class NoSplitter(LayoutError): |
| """Requested splitter does not exist.""" |
| |
| |
| class _Placeholder: |
| """An internal renderable used as a Layout placeholder.""" |
| |
| highlighter = ReprHighlighter() |
| |
| def __init__(self, layout: "Layout", style: StyleType = "") -> None: |
| self.layout = layout |
| self.style = style |
| |
| def __rich_console__( |
| self, console: Console, options: ConsoleOptions |
| ) -> RenderResult: |
| width = options.max_width |
| height = options.height or options.size.height |
| layout = self.layout |
| title = ( |
| f"{layout.name!r} ({width} x {height})" |
| if layout.name |
| else f"({width} x {height})" |
| ) |
| yield Panel( |
| Align.center(Pretty(layout), vertical="middle"), |
| style=self.style, |
| title=self.highlighter(title), |
| border_style="blue", |
| height=height, |
| ) |
| |
| |
| class Splitter(ABC): |
| """Base class for a splitter.""" |
| |
| name: str = "" |
| |
| @abstractmethod |
| def get_tree_icon(self) -> str: |
| """Get the icon (emoji) used in layout.tree""" |
| |
| @abstractmethod |
| def divide( |
| self, children: Sequence["Layout"], region: Region |
| ) -> Iterable[Tuple["Layout", Region]]: |
| """Divide a region amongst several child layouts. |
| |
| Args: |
| children (Sequence(Layout)): A number of child layouts. |
| region (Region): A rectangular region to divide. |
| """ |
| |
| |
| class RowSplitter(Splitter): |
| """Split a layout region in to rows.""" |
| |
| name = "row" |
| |
| def get_tree_icon(self) -> str: |
| return "[layout.tree.row]⬌" |
| |
| def divide( |
| self, children: Sequence["Layout"], region: Region |
| ) -> Iterable[Tuple["Layout", Region]]: |
| x, y, width, height = region |
| render_widths = ratio_resolve(width, children) |
| offset = 0 |
| _Region = Region |
| for child, child_width in zip(children, render_widths): |
| yield child, _Region(x + offset, y, child_width, height) |
| offset += child_width |
| |
| |
| class ColumnSplitter(Splitter): |
| """Split a layout region in to columns.""" |
| |
| name = "column" |
| |
| def get_tree_icon(self) -> str: |
| return "[layout.tree.column]⬍" |
| |
| def divide( |
| self, children: Sequence["Layout"], region: Region |
| ) -> Iterable[Tuple["Layout", Region]]: |
| x, y, width, height = region |
| render_heights = ratio_resolve(height, children) |
| offset = 0 |
| _Region = Region |
| for child, child_height in zip(children, render_heights): |
| yield child, _Region(x, y + offset, width, child_height) |
| offset += child_height |
| |
| |
| @rich_repr |
| class Layout: |
| """A renderable to divide a fixed height in to rows or columns. |
| |
| Args: |
| renderable (RenderableType, optional): Renderable content, or None for placeholder. Defaults to None. |
| name (str, optional): Optional identifier for Layout. Defaults to None. |
| size (int, optional): Optional fixed size of layout. Defaults to None. |
| minimum_size (int, optional): Minimum size of layout. Defaults to 1. |
| ratio (int, optional): Optional ratio for flexible layout. Defaults to 1. |
| visible (bool, optional): Visibility of layout. Defaults to True. |
| """ |
| |
| splitters = {"row": RowSplitter, "column": ColumnSplitter} |
| |
| def __init__( |
| self, |
| renderable: Optional[RenderableType] = None, |
| *, |
| name: Optional[str] = None, |
| size: Optional[int] = None, |
| minimum_size: int = 1, |
| ratio: int = 1, |
| visible: bool = True, |
| height: Optional[int] = None, |
| ) -> None: |
| self._renderable = renderable or _Placeholder(self) |
| self.size = size |
| self.minimum_size = minimum_size |
| self.ratio = ratio |
| self.name = name |
| self.visible = visible |
| self.height = height |
| self.splitter: Splitter = self.splitters["column"]() |
| self._children: List[Layout] = [] |
| self._render_map: RenderMap = {} |
| self._lock = RLock() |
| |
| def __rich_repr__(self) -> Result: |
| yield "name", self.name, None |
| yield "size", self.size, None |
| yield "minimum_size", self.minimum_size, 1 |
| yield "ratio", self.ratio, 1 |
| |
| @property |
| def renderable(self) -> RenderableType: |
| """Layout renderable.""" |
| return self if self._children else self._renderable |
| |
| @property |
| def children(self) -> List["Layout"]: |
| """Gets (visible) layout children.""" |
| return [child for child in self._children if child.visible] |
| |
| @property |
| def map(self) -> RenderMap: |
| """Get a map of the last render.""" |
| return self._render_map |
| |
| def get(self, name: str) -> Optional["Layout"]: |
| """Get a named layout, or None if it doesn't exist. |
| |
| Args: |
| name (str): Name of layout. |
| |
| Returns: |
| Optional[Layout]: Layout instance or None if no layout was found. |
| """ |
| if self.name == name: |
| return self |
| else: |
| for child in self._children: |
| named_layout = child.get(name) |
| if named_layout is not None: |
| return named_layout |
| return None |
| |
| def __getitem__(self, name: str) -> "Layout": |
| layout = self.get(name) |
| if layout is None: |
| raise KeyError(f"No layout with name {name!r}") |
| return layout |
| |
| @property |
| def tree(self) -> "Tree": |
| """Get a tree renderable to show layout structure.""" |
| from pip._vendor.rich.styled import Styled |
| from pip._vendor.rich.table import Table |
| from pip._vendor.rich.tree import Tree |
| |
| def summary(layout: "Layout") -> Table: |
| |
| icon = layout.splitter.get_tree_icon() |
| |
| table = Table.grid(padding=(0, 1, 0, 0)) |
| |
| text: RenderableType = ( |
| Pretty(layout) if layout.visible else Styled(Pretty(layout), "dim") |
| ) |
| table.add_row(icon, text) |
| _summary = table |
| return _summary |
| |
| layout = self |
| tree = Tree( |
| summary(layout), |
| guide_style=f"layout.tree.{layout.splitter.name}", |
| highlight=True, |
| ) |
| |
| def recurse(tree: "Tree", layout: "Layout") -> None: |
| for child in layout._children: |
| recurse( |
| tree.add( |
| summary(child), |
| guide_style=f"layout.tree.{child.splitter.name}", |
| ), |
| child, |
| ) |
| |
| recurse(tree, self) |
| return tree |
| |
| def split( |
| self, |
| *layouts: Union["Layout", RenderableType], |
| splitter: Union[Splitter, str] = "column", |
| ) -> None: |
| """Split the layout in to multiple sub-layouts. |
| |
| Args: |
| *layouts (Layout): Positional arguments should be (sub) Layout instances. |
| splitter (Union[Splitter, str]): Splitter instance or name of splitter. |
| """ |
| _layouts = [ |
| layout if isinstance(layout, Layout) else Layout(layout) |
| for layout in layouts |
| ] |
| try: |
| self.splitter = ( |
| splitter |
| if isinstance(splitter, Splitter) |
| else self.splitters[splitter]() |
| ) |
| except KeyError: |
| raise NoSplitter(f"No splitter called {splitter!r}") |
| self._children[:] = _layouts |
| |
| def add_split(self, *layouts: Union["Layout", RenderableType]) -> None: |
| """Add a new layout(s) to existing split. |
| |
| Args: |
| *layouts (Union[Layout, RenderableType]): Positional arguments should be renderables or (sub) Layout instances. |
| |
| """ |
| _layouts = ( |
| layout if isinstance(layout, Layout) else Layout(layout) |
| for layout in layouts |
| ) |
| self._children.extend(_layouts) |
| |
| def split_row(self, *layouts: Union["Layout", RenderableType]) -> None: |
| """Split the layout in to a row (layouts side by side). |
| |
| Args: |
| *layouts (Layout): Positional arguments should be (sub) Layout instances. |
| """ |
| self.split(*layouts, splitter="row") |
| |
| def split_column(self, *layouts: Union["Layout", RenderableType]) -> None: |
| """Split the layout in to a column (layouts stacked on top of each other). |
| |
| Args: |
| *layouts (Layout): Positional arguments should be (sub) Layout instances. |
| """ |
| self.split(*layouts, splitter="column") |
| |
| def unsplit(self) -> None: |
| """Reset splits to initial state.""" |
| del self._children[:] |
| |
| def update(self, renderable: RenderableType) -> None: |
| """Update renderable. |
| |
| Args: |
| renderable (RenderableType): New renderable object. |
| """ |
| with self._lock: |
| self._renderable = renderable |
| |
| def refresh_screen(self, console: "Console", layout_name: str) -> None: |
| """Refresh a sub-layout. |
| |
| Args: |
| console (Console): Console instance where Layout is to be rendered. |
| layout_name (str): Name of layout. |
| """ |
| with self._lock: |
| layout = self[layout_name] |
| region, _lines = self._render_map[layout] |
| (x, y, width, height) = region |
| lines = console.render_lines( |
| layout, console.options.update_dimensions(width, height) |
| ) |
| self._render_map[layout] = LayoutRender(region, lines) |
| console.update_screen_lines(lines, x, y) |
| |
| def _make_region_map(self, width: int, height: int) -> RegionMap: |
| """Create a dict that maps layout on to Region.""" |
| stack: List[Tuple[Layout, Region]] = [(self, Region(0, 0, width, height))] |
| push = stack.append |
| pop = stack.pop |
| layout_regions: List[Tuple[Layout, Region]] = [] |
| append_layout_region = layout_regions.append |
| while stack: |
| append_layout_region(pop()) |
| layout, region = layout_regions[-1] |
| children = layout.children |
| if children: |
| for child_and_region in layout.splitter.divide(children, region): |
| push(child_and_region) |
| |
| region_map = { |
| layout: region |
| for layout, region in sorted(layout_regions, key=itemgetter(1)) |
| } |
| return region_map |
| |
| def render(self, console: Console, options: ConsoleOptions) -> RenderMap: |
| """Render the sub_layouts. |
| |
| Args: |
| console (Console): Console instance. |
| options (ConsoleOptions): Console options. |
| |
| Returns: |
| RenderMap: A dict that maps Layout on to a tuple of Region, lines |
| """ |
| render_width = options.max_width |
| render_height = options.height or console.height |
| region_map = self._make_region_map(render_width, render_height) |
| layout_regions = [ |
| (layout, region) |
| for layout, region in region_map.items() |
| if not layout.children |
| ] |
| render_map: Dict["Layout", "LayoutRender"] = {} |
| render_lines = console.render_lines |
| update_dimensions = options.update_dimensions |
| |
| for layout, region in layout_regions: |
| lines = render_lines( |
| layout.renderable, update_dimensions(region.width, region.height) |
| ) |
| render_map[layout] = LayoutRender(region, lines) |
| return render_map |
| |
| def __rich_console__( |
| self, console: Console, options: ConsoleOptions |
| ) -> RenderResult: |
| with self._lock: |
| width = options.max_width or console.width |
| height = options.height or console.height |
| render_map = self.render(console, options.update_dimensions(width, height)) |
| self._render_map = render_map |
| layout_lines: List[List[Segment]] = [[] for _ in range(height)] |
| _islice = islice |
| for (region, lines) in render_map.values(): |
| _x, y, _layout_width, layout_height = region |
| for row, line in zip( |
| _islice(layout_lines, y, y + layout_height), lines |
| ): |
| row.extend(line) |
| |
| new_line = Segment.line() |
| for layout_row in layout_lines: |
| yield from layout_row |
| yield new_line |
| |
| |
| if __name__ == "__main__": |
| from pip._vendor.rich.console import Console |
| |
| console = Console() |
| layout = Layout() |
| |
| layout.split_column( |
| Layout(name="header", size=3), |
| Layout(ratio=1, name="main"), |
| Layout(size=10, name="footer"), |
| ) |
| |
| layout["main"].split_row(Layout(name="side"), Layout(name="body", ratio=2)) |
| |
| layout["body"].split_row(Layout(name="content", ratio=2), Layout(name="s2")) |
| |
| layout["s2"].split_column( |
| Layout(name="top"), Layout(name="middle"), Layout(name="bottom") |
| ) |
| |
| layout["side"].split_column(Layout(layout.tree, name="left1"), Layout(name="left2")) |
| |
| layout["content"].update("foo") |
| |
| console.print(layout) |