python (3.11.7)
       1  from abc import ABC, abstractmethod
       2  from itertools import islice
       3  from operator import itemgetter
       4  from threading import RLock
       5  from typing import (
       6      TYPE_CHECKING,
       7      Dict,
       8      Iterable,
       9      List,
      10      NamedTuple,
      11      Optional,
      12      Sequence,
      13      Tuple,
      14      Union,
      15  )
      16  
      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
      27  
      28  if TYPE_CHECKING:
      29      from pip._vendor.rich.tree import Tree
      30  
      31  
      32  class ESC[4;38;5;81mLayoutRender(ESC[4;38;5;149mNamedTuple):
      33      """An individual layout render."""
      34  
      35      region: Region
      36      render: List[List[Segment]]
      37  
      38  
      39  RegionMap = Dict["Layout", Region]
      40  RenderMap = Dict["Layout", LayoutRender]
      41  
      42  
      43  class ESC[4;38;5;81mLayoutError(ESC[4;38;5;149mException):
      44      """Layout related error."""
      45  
      46  
      47  class ESC[4;38;5;81mNoSplitter(ESC[4;38;5;149mLayoutError):
      48      """Requested splitter does not exist."""
      49  
      50  
      51  class ESC[4;38;5;81m_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 = layout
      58          self.style = style
      59  
      60      def __rich_console__(
      61          self, console: Console, options: ConsoleOptions
      62      ) -> RenderResult:
      63          width = options.max_width
      64          height = options.height or options.size.height
      65          layout = self.layout
      66          title = (
      67              f"{layout.name!r} ({width} x {height})"
      68              if layout.name
      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  
      80  class ESC[4;38;5;81mSplitter(ESC[4;38;5;149mABC):
      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  
     101  class ESC[4;38;5;81mRowSplitter(ESC[4;38;5;149mSplitter):
     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: 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  
     121  class ESC[4;38;5;81mColumnSplitter(ESC[4;38;5;149mSplitter):
     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: 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
     142  class ESC[4;38;5;81mLayout:
     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 = 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  
     231              icon = layout.splitter.get_tree_icon()
     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:
     354              append_layout_region(pop())
     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  
     396      def __rich_console__(
     397          self, console: Console, options: ConsoleOptions
     398      ) -> RenderResult:
     399          with self._lock:
     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)]
     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  
     419  if __name__ == "__main__":
     420      from pip._vendor.rich.console import Console
     421  
     422      console = Console()
     423      layout = Layout()
     424  
     425      layout.split_column(
     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)