CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
layout.py443 linesDownload Raw Back to rich
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 
Aluode/PerceptionLabPortable · CoolFace