123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- 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 .region import Region
- from .repr import Result, rich_repr
- 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,
- ) -> 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.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)
|