Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
canvas.py1393 linesDownload Raw Back to urwid
1# Urwid canvas class and functions2#    Copyright (C) 2004-2011  Ian Ward3#4#    This library is free software; you can redistribute it and/or5#    modify it under the terms of the GNU Lesser General Public6#    License as published by the Free Software Foundation; either7#    version 2.1 of the License, or (at your option) any later version.8#9#    This library is distributed in the hope that it will be useful,10#    but WITHOUT ANY WARRANTY; without even the implied warranty of11#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU12#    Lesser General Public License for more details.13#14#    You should have received a copy of the GNU Lesser General Public15#    License along with this library; if not, write to the Free Software16#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA17#18# Urwid web site: https://urwid.org/19 20 21from __future__ import annotations22 23import contextlib24import dataclasses25import typing26import weakref27from contextlib import suppress28 29from urwid.str_util import calc_text_pos, calc_width30from urwid.text_layout import LayoutSegment, trim_line31from urwid.util import (32    apply_target_encoding,33    get_encoding,34    rle_append_modify,35    rle_join_modify,36    rle_len,37    rle_product,38    trim_text_attr_cs,39)40 41if typing.TYPE_CHECKING:42    from collections.abc import Hashable, Iterable, Iterator, Sequence43 44    from typing_extensions import Literal45 46    from .widget import Widget47 48 49class CanvasCache:50    """51    Cache for rendered canvases.  Automatically populated and52    accessed by Widget render() MetaClass magic, cleared by53    Widget._invalidate().54 55    Stores weakrefs to the canvas objects, so an external class56    must maintain a reference for this cache to be effective.57    At present the Screen classes store the last topmost canvas58    after redrawing the screen, keeping the canvases from being59    garbage collected.60 61    _widgets[widget] = {(wcls, size, focus): weakref.ref(canvas), ...}62    _refs[weakref.ref(canvas)] = (widget, wcls, size, focus)63    _deps[widget} = [dependent_widget, ...]64    """65 66    _widgets: typing.ClassVar[67        dict[68            Widget,69            dict[70                tuple[type[Widget], tuple[int, int] | tuple[int] | tuple[()], bool],71                weakref.ReferenceType,72            ],73        ]74    ] = {}75    _refs: typing.ClassVar[76        dict[77            weakref.ReferenceType,78            tuple[Widget, type[Widget], tuple[int, int] | tuple[int] | tuple[()], bool],79        ]80    ] = {}81    _deps: typing.ClassVar[dict[Widget, list[Widget]]] = {}82    hits = 083    fetches = 084    cleanups = 085 86    @classmethod87    def store(cls, wcls: type[Widget], canvas: Canvas) -> None:88        """89        Store a weakref to canvas in the cache.90 91        wcls -- widget class that contains render() function92        canvas -- rendered canvas with widget_info (widget, size, focus)93        """94        if not canvas.cacheable:95            return96 97        if not canvas.widget_info:98            raise TypeError("Can't store canvas without widget_info")99        widget, size, focus = canvas.widget_info100 101        def walk_depends(canv: Canvas) -> list[Widget]:102            """103            Collect all child widgets for determining who we104            depend on.105            """106            # FIXME: is this recursion necessary?  The cache invalidating might work with only one level.107            depends = []108            for _x, _y, c, _pos in canv.children:109                if c.widget_info:110                    depends.append(c.widget_info[0])111                elif hasattr(c, "children"):112                    depends.extend(walk_depends(c))113            return depends114 115        # use explicit depends_on if available from the canvas116        depends_on = getattr(canvas, "depends_on", None)117        if depends_on is None and hasattr(canvas, "children"):118            depends_on = walk_depends(canvas)119        if depends_on:120            for w in depends_on:121                if w not in cls._widgets:122                    return123            for w in depends_on:124                cls._deps.setdefault(w, []).append(widget)125 126        ref = weakref.ref(canvas, cls.cleanup)127        cls._refs[ref] = (widget, wcls, size, focus)128        cls._widgets.setdefault(widget, {})[wcls, size, focus] = ref129 130    @classmethod131    def fetch(cls, widget: Widget, wcls: type[Widget], size, focus: bool) -> Canvas | None:132        """133        Return the cached canvas or None.134 135        widget -- widget object requested136        wcls -- widget class that contains render() function137        size, focus -- render() parameters138        """139        cls.fetches += 1  # collect stats140 141        sizes = cls._widgets.get(widget, None)142        if not sizes:143            return None144        ref = sizes.get((wcls, size, focus), None)145        if not ref:146            return None147        canv = ref()148        if canv:149            cls.hits += 1  # more stats150        return canv151 152    @classmethod153    def invalidate(cls, widget: Widget) -> None:154        """155        Remove all canvases cached for widget.156        """157        with contextlib.suppress(KeyError):158            for ref in cls._widgets[widget].values():159                with suppress(KeyError):160                    del cls._refs[ref]161            del cls._widgets[widget]162 163        if widget not in cls._deps:164            return165        dependants = cls._deps.get(widget, [])166        with suppress(KeyError):167            del cls._deps[widget]168        for w in dependants:169            cls.invalidate(w)170 171    @classmethod172    def cleanup(cls, ref: weakref.ReferenceType) -> None:173        cls.cleanups += 1  # collect stats174 175        w = cls._refs.get(ref, None)176        del cls._refs[ref]177        if not w:178            return179        widget, wcls, size, focus = w180        sizes = cls._widgets.get(widget, None)181        if not sizes:182            return183        with suppress(KeyError):184            del sizes[wcls, size, focus]185        if not sizes:186            with contextlib.suppress(KeyError):187                del cls._widgets[widget]188                del cls._deps[widget]189 190    @classmethod191    def clear(cls) -> None:192        """193        Empty the cache.194        """195        cls._widgets = {}196        cls._refs = {}197        cls._deps = {}198 199 200class CanvasError(Exception):201    pass202 203 204class Canvas:205    """206    base class for canvases207    """208 209    cacheable = True210 211    _finalized_error = CanvasError(212        "This canvas has been finalized. Use CompositeCanvas to wrap this canvas if you need to make changes."213    )214 215    def __init__(self) -> None:216        """Base Canvas class"""217        self._widget_info: tuple[Widget, tuple[[]] | tuple[int] | tuple[int, int], bool] | None = None218        self.coords: dict[str, tuple[int, int, tuple[Widget, int, int]] | tuple[int, int, None]] = {}219        self.shortcuts: dict[str, str] = {}220 221    def finalize(222        self,223        widget: Widget,224        size: tuple[()] | tuple[int] | tuple[int, int],225        focus: bool,226    ) -> None:227        """228        Mark this canvas as finalized (should not be any future229        changes to its content). This is required before caching230        the canvas.  This happens automatically after a widget's231        'render call returns the canvas thanks to some metaclass232        magic.233 234        widget -- widget that rendered this canvas235        size -- size parameter passed to widget's render method236        focus -- focus parameter passed to widget's render method237        """238        if self.widget_info:239            raise self._finalized_error240        self._widget_info = widget, size, focus241 242    @property243    def widget_info(self) -> tuple[Widget, tuple[[]] | tuple[int] | tuple[int, int], bool] | None:244        return self._widget_info245 246    @property247    def text(self) -> list[bytes]:248        """249        Return the text content of the canvas as a list of strings, one for each row.250        """251        return [b"".join([text for (attr, cs, text) in row]) for row in self.content()]252 253    @property254    def decoded_text(self) -> Sequence[str]:255        """Decoded text content of the canvas as a sequence of strings, one for each row."""256        encoding = get_encoding()257        return tuple(line.decode(encoding) for line in self.text)258 259    def content(260        self,261        trim_left: int = 0,262        trim_top: int = 0,263        cols: int | None = None,264        rows: int | None = None,265        attr=None,266    ) -> Iterator[list[tuple[object, Literal["0", "U"] | None, bytes]]]:267        raise NotImplementedError()268 269    def cols(self) -> int:270        raise NotImplementedError()271 272    def rows(self) -> int:273        raise NotImplementedError()274 275    def content_delta(self, other: Canvas):276        raise NotImplementedError()277 278    def get_cursor(self) -> tuple[int, int] | None:279        if c := self.coords.get("cursor", None):280            return c[:2]  # trim off data part281 282        return None283 284    def set_cursor(self, c: tuple[int, int] | None) -> None:285        if self.widget_info and self.cacheable:286            raise self._finalized_error287        if c is None:288            with suppress(KeyError):289                del self.coords["cursor"]290            return291        self.coords["cursor"] = (*c, None)  # data part292 293    cursor = property(get_cursor, set_cursor)294 295    def get_pop_up(self) -> tuple[int, int, tuple[Widget, int, int]] | None:296        if c := self.coords.get("pop up", None):297            return c298 299        return None300 301    def set_pop_up(self, w: Widget, left: int, top: int, overlay_width: int, overlay_height: int) -> None:302        """303        This method adds pop-up information to the canvas.  This information304        is intercepted by a PopUpTarget widget higher in the chain to305        display a pop-up at the given (left, top) position relative to the306        current canvas.307 308        :param w: widget to use for the pop-up309        :type w: widget310        :param left: x position for left edge of pop-up >= 0311        :type left: int312        :param top: y position for top edge of pop-up >= 0313        :type top: int314        :param overlay_width: width of overlay in screen columns > 0315        :type overlay_width: int316        :param overlay_height: height of overlay in screen rows > 0317        :type overlay_height: int318        """319        if self.widget_info and self.cacheable:320            raise self._finalized_error321 322        self.coords["pop up"] = (left, top, (w, overlay_width, overlay_height))323 324    def translate_coords(self, dx: int, dy: int) -> dict[str, tuple[int, int, tuple[Widget, int, int]]]:325        """326        Return coords shifted by (dx, dy).327        """328        d = {}329        for name, (x, y, data) in self.coords.items():330            d[name] = (x + dx, y + dy, data)331        return d332 333    def __repr__(self) -> str:334        extra = [""]335        with contextlib.suppress(BaseException):336            extra.append(f"cols={self.cols()}")337 338        with contextlib.suppress(BaseException):339            extra.append(f"rows={self.rows()}")340 341        if self.cursor:342            extra.append(f"cursor={self.cursor}")343 344        return f"<{self.__class__.__name__} finalized={bool(self.widget_info)}{' '.join(extra)} at 0x{id(self):X}>"345 346    def __str__(self) -> str:347        with contextlib.suppress(BaseException):348            return "\n".join(self.decoded_text)349 350        return repr(self)351 352 353class TextCanvas(Canvas):354    """355    class for storing rendered text and attributes356    """357 358    def __init__(359        self,360        text: list[bytes] | None = None,361        attr: list[list[tuple[Hashable | None, int]]] | None = None,362        cs: list[list[tuple[Literal["0", "U"] | None, int]]] | None = None,363        cursor: tuple[int, int] | None = None,364        maxcol: int | None = None,365        check_width: bool = True,366    ) -> None:367        """368        text -- list of strings, one for each line369        attr -- list of run length encoded attributes for text370        cs -- list of run length encoded character set for text371        cursor -- (x,y) of cursor or None372        maxcol -- screen columns taken by this canvas373        check_width -- check and fix width of all lines in text374        """375        super().__init__()376        if text is None:377            text = []378 379        if check_width:380            widths = []381            for t in text:382                if not isinstance(t, bytes):383                    raise CanvasError(384                        "Canvas text must be plain strings encoded in the screen's encoding",385                        repr(text),386                    )387                widths.append(calc_width(t, 0, len(t)))388        else:389            if not isinstance(maxcol, int):390                raise TypeError(maxcol)391            widths = [maxcol] * len(text)392 393        if maxcol is None:394            if widths:395                # find maxcol ourselves396                maxcol = max(widths)397            else:398                maxcol = 0399 400        if attr is None:401            attr = [[] for _ in range(len(text))]402        if cs is None:403            cs = [[] for _ in range(len(text))]404 405        # pad text and attr to maxcol406        for i in range(len(text)):407            w = widths[i]408            if w > maxcol:409                raise CanvasError(410                    f"Canvas text is wider than the maxcol specified:\n"411                    f"maxcol={maxcol!r}\n"412                    f"widths={widths!r}\n"413                    f"text={text!r}\n"414                    f"urwid target encoding={get_encoding()}"415                )416            if w < maxcol:417                text[i] += b"".rjust(maxcol - w)418            a_gap = len(text[i]) - rle_len(attr[i])419            if a_gap < 0:420                raise CanvasError(f"Attribute extends beyond text \n{text[i]!r}\n{attr[i]!r}")421            if a_gap:422                rle_append_modify(attr[i], (None, a_gap))423 424            cs_gap = len(text[i]) - rle_len(cs[i])425            if cs_gap < 0:426                raise CanvasError(f"Character Set extends beyond text \n{text[i]!r}\n{cs[i]!r}")427            if cs_gap:428                rle_append_modify(cs[i], (None, cs_gap))429 430        self._attr = attr431        self._cs = cs432        self.cursor = cursor433        self._text = text434        self._maxcol = maxcol435 436    def rows(self) -> int:437        """Return the number of rows in this canvas."""438        return len(self._text)439 440    def cols(self) -> int:441        """Return the screen column width of this canvas."""442        return self._maxcol443 444    def translated_coords(self, dx: int, dy: int) -> tuple[int, int] | None:445        """446        Return cursor coords shifted by (dx, dy), or None if there447        is no cursor.448        """449        if self.cursor:450            x, y = self.cursor451            return x + dx, y + dy452        return None453 454    def content(455        self,456        trim_left: int = 0,457        trim_top: int = 0,458        cols: int | None = 0,459        rows: int | None = 0,460        attr=None,461    ) -> Iterator[tuple[object, Literal["0", "U"] | None, bytes]]:462        """463        Return the canvas content as a list of rows where each row464        is a list of (attr, cs, text) tuples.465 466        trim_left, trim_top, cols, rows may be set by467        CompositeCanvas when rendering a partially obscured468        canvas.469        """470        maxcol, maxrow = self.cols(), self.rows()471        if not cols:472            cols = maxcol - trim_left473        if not rows:474            rows = maxrow - trim_top475 476        if not ((0 <= trim_left < maxcol) and (cols > 0 and trim_left + cols <= maxcol)):477            raise ValueError(trim_left)478        if not ((0 <= trim_top < maxrow) and (rows > 0 and trim_top + rows <= maxrow)):479            raise ValueError(trim_top)480 481        if trim_top or rows < maxrow:482            text_attr_cs = zip(483                self._text[trim_top : trim_top + rows],484                self._attr[trim_top : trim_top + rows],485                self._cs[trim_top : trim_top + rows],486            )487        else:488            text_attr_cs = zip(self._text, self._attr, self._cs)489 490        for text, a_row, cs_row in text_attr_cs:491            if trim_left or cols < self._maxcol:492                text, a_row, cs_row = trim_text_attr_cs(  # noqa: PLW2901493                    text,494                    a_row,495                    cs_row,496                    trim_left,497                    trim_left + cols,498                )499            attr_cs = rle_product(a_row, cs_row)500            i = 0501            row = []502            for (a, cs), run in attr_cs:503                if attr and a in attr:504                    a = attr[a]  # noqa: PLW2901505                row.append((a, cs, text[i : i + run]))506                i += run507            yield row508 509    def content_delta(self, other: Canvas):510        """511        Return the differences between other and this canvas.512 513        If other is the same object as self this will return no514        differences, otherwise this is the same as calling515        content().516        """517        if other is self:518            return [self.cols()] * self.rows()519        return self.content()520 521 522class BlankCanvas(Canvas):523    """524    a canvas with nothing on it, only works as part of a composite canvas525    since it doesn't know its own size526    """527 528    def content(529        self,530        trim_left: int = 0,531        trim_top: int = 0,532        cols: int | None = 0,533        rows: int | None = 0,534        attr=None,535    ) -> Iterator[list[tuple[object, Literal["0", "U"] | None, bytes]]]:536        """537        return (cols, rows) of spaces with default attributes.538        """539        def_attr = None540        if attr and None in attr:541            def_attr = attr[None]542        line = [(def_attr, None, b"".rjust(cols))]543        for _ in range(rows):544            yield line545 546    def cols(self) -> typing.NoReturn:547        raise NotImplementedError("BlankCanvas doesn't know its own size!")548 549    def rows(self) -> typing.NoReturn:550        raise NotImplementedError("BlankCanvas doesn't know its own size!")551 552    def content_delta(self, other: Canvas) -> typing.NoReturn:553        raise NotImplementedError("BlankCanvas doesn't know its own size!")554 555 556blank_canvas = BlankCanvas()557 558 559class SolidCanvas(Canvas):560    """561    A canvas filled completely with a single character.562    """563 564    def __init__(self, fill_char: str | bytes, cols: int, rows: int) -> None:565        super().__init__()566        end, col = calc_text_pos(fill_char, 0, len(fill_char), 1)567        if col != 1:568            raise ValueError(f"Invalid fill_char: {fill_char!r}")569        self._text, cs = apply_target_encoding(fill_char[:end])570        self._cs = cs[0][0]571        self.size = cols, rows572        self.cursor = None573 574    def cols(self) -> int:575        return self.size[0]576 577    def rows(self) -> int:578        return self.size[1]579 580    def content(581        self,582        trim_left: int = 0,583        trim_top: int = 0,584        cols: int | None = None,585        rows: int | None = None,586        attr=None,587    ) -> Iterator[list[tuple[object, Literal["0", "U"] | None, bytes]]]:588        if cols is None:589            cols = self.size[0]590        if rows is None:591            rows = self.size[1]592        def_attr = None593        if attr and None in attr:594            def_attr = attr[None]595 596        line = [(def_attr, self._cs, self._text * cols)]597        for _ in range(rows):598            yield line599 600    def content_delta(self, other):601        """602        Return the differences between other and this canvas.603        """604        if other is self:605            return [self.cols()] * self.rows()606        return self.content()607 608 609class CompositeCanvas(Canvas):610    """611    class for storing a combination of canvases612    """613 614    def __init__(self, canv: Canvas = None) -> None:615        """616        canv -- a Canvas object to wrap this CompositeCanvas around.617 618        if canv is a CompositeCanvas, make a copy of its contents619        """620        # a "shard" is a (num_rows, list of cviews) tuple, one for621        # each cview starting in this shard622 623        # a "cview" is a tuple that defines a view of a canvas:624        # (trim_left, trim_top, cols, rows, attr_map, canv)625 626        # a "shard tail" is a list of tuples:627        # (col_gap, done_rows, content_iter, cview)628 629        # tuples that define the unfinished cviews that are part of630        # shards following the first shard.631        super().__init__()632 633        if canv is None:634            self.shards: list[635                tuple[636                    int,637                    list[tuple[int, int, int, int, dict[Hashable | None, Hashable] | None, Canvas]],638                ]639            ] = []640            self.children: list[tuple[int, int, Canvas, typing.Any]] = []641        else:642            if hasattr(canv, "shards"):643                self.shards = canv.shards644            else:645                self.shards = [(canv.rows(), [(0, 0, canv.cols(), canv.rows(), None, canv)])]646            self.children = [(0, 0, canv, None)]647            self.coords.update(canv.coords)648            for shortcut in canv.shortcuts:649                self.shortcuts[shortcut] = "wrap"650 651    def __repr__(self) -> str:652        extra = [""]653        with contextlib.suppress(BaseException):654            extra.append(f"cols={self.cols()}")655 656        with contextlib.suppress(BaseException):657            extra.append(f"rows={self.rows()}")658 659        if self.cursor:660            extra.append(f"cursor={self.cursor}")661        if self.children:662            extra.append(f"children=({', '.join(repr(canv) for _, _, canv, _ in self.children)})")663 664        return f"<{self.__class__.__name__} finalized={bool(self.widget_info)}{' '.join(extra)} at 0x{id(self):X}>"665 666    def rows(self) -> int:667        for r, cv in self.shards:668            if not isinstance(r, int):669                raise TypeError(r, cv)670 671        return sum(r for r, cv in self.shards)672 673    def cols(self) -> int:674        if not self.shards:675            return 0676        cols = sum(cv[2] for cv in self.shards[0][1])677        if not isinstance(cols, int):678            raise TypeError(cols)679        return cols680 681    def content(682        self,683        trim_left: int = 0,684        trim_top: int = 0,685        cols: int | None = None,686        rows: int | None = None,687        attr=None,688    ) -> Iterator[list[tuple[object, Literal["0", "U"] | None, bytes]]]:689        """690        Return the canvas content as a list of rows where each row691        is a list of (attr, cs, text) tuples.692        """693        shard_tail = []694        for num_rows, cviews in self.shards:695            # combine shard and shard tail696            sbody = shard_body(cviews, shard_tail)697 698            # output rows699            for _ in range(num_rows):700                yield shard_body_row(sbody)701 702            # prepare next shard tail703            shard_tail = shard_body_tail(num_rows, sbody)704 705    def content_delta(self, other: Canvas):706        """707        Return the differences between other and this canvas.708        """709        if not hasattr(other, "shards"):710            yield from self.content()711            return712 713        shard_tail = []714        for num_rows, cviews in shards_delta(self.shards, other.shards):715            # combine shard and shard tail716            sbody = shard_body(cviews, shard_tail)717 718            # output rows719            row = []720            for _ in range(num_rows):721                # if whole shard is unchanged, don't keep722                # calling shard_body_row723                if len(row) != 1 or not isinstance(row[0], int):724                    row = shard_body_row(sbody)725                yield row726 727            # prepare next shard tail728            shard_tail = shard_body_tail(num_rows, sbody)729 730    def trim(self, top: int, count: int | None = None) -> None:731        """Trim lines from the top and/or bottom of canvas.732 733        top -- number of lines to remove from top734        count -- number of lines to keep, or None for all the rest735        """736        if top < 0:737            raise ValueError(f"invalid trim amount {top:d}!")738        if top >= self.rows():739            raise ValueError(f"cannot trim {top:d} lines from {self.rows():d}!")740        if self.widget_info:741            raise self._finalized_error742 743        if top:744            self.shards = shards_trim_top(self.shards, top)745 746        if count == 0:747            self.shards = []748        elif count is not None:749            self.shards = shards_trim_rows(self.shards, count)750 751        self.coords = self.translate_coords(0, -top)752 753    def trim_end(self, end: int) -> None:754        """Trim lines from the bottom of the canvas.755 756        end -- number of lines to remove from the end757        """758        if end <= 0:759            raise ValueError(f"invalid trim amount {end:d}!")760        if end > self.rows():761            raise ValueError(f"cannot trim {end:d} lines from {self.rows():d}!")762        if self.widget_info:763            raise self._finalized_error764 765        self.shards = shards_trim_rows(self.shards, self.rows() - end)766 767    def pad_trim_left_right(self, left: int, right: int) -> None:768        """769        Pad or trim this canvas on the left and right770 771        values > 0 indicate screen columns to pad772        values < 0 indicate screen columns to trim773        """774        if self.widget_info:775            raise self._finalized_error776        shards = self.shards777        if left < 0 or right < 0:778            trim_left = max(0, -left)779            cols = self.cols() - trim_left - max(0, -right)780            shards = shards_trim_sides(shards, trim_left, cols)781 782        rows = self.rows()783        if left > 0 or right > 0:784            top_rows, top_cviews = shards[0]785            if left > 0:786                new_top_cviews = [(0, 0, left, rows, None, blank_canvas), *top_cviews]787            else:788                new_top_cviews = top_cviews.copy()789 790            if right > 0:791                new_top_cviews.append((0, 0, right, rows, None, blank_canvas))792            shards = [(top_rows, new_top_cviews), *shards[1:]]793 794        self.coords = self.translate_coords(left, 0)795        self.shards = shards796 797    def pad_trim_top_bottom(self, top: int, bottom: int) -> None:798        """799        Pad or trim this canvas on the top and bottom.800        """801        if self.widget_info:802            raise self._finalized_error803        orig_shards = self.shards804 805        if top < 0 or bottom < 0:806            trim_top = max(0, -top)807            rows = self.rows() - trim_top - max(0, -bottom)808            self.trim(trim_top, rows)809 810        cols = self.cols()811        if top > 0:812            self.shards = [(top, [(0, 0, cols, top, None, blank_canvas)]), *self.shards]813            self.coords = self.translate_coords(0, top)814 815        if bottom > 0:816            if orig_shards is self.shards:817                self.shards = self.shards.copy()818            self.shards.append((bottom, [(0, 0, cols, bottom, None, blank_canvas)]))819 820    def overlay(self, other: CompositeCanvas, left: int, top: int) -> None:821        """Overlay other onto this canvas."""822        if self.widget_info:823            raise self._finalized_error824 825        width = other.cols()826        height = other.rows()827        right = self.cols() - left - width828        bottom = self.rows() - top - height829 830        if right < 0:831            raise ValueError(f"top canvas of overlay not the size expected!{(other.cols(), left, right, width)!r}")832        if bottom < 0:833            raise ValueError(f"top canvas of overlay not the size expected!{(other.rows(), top, bottom, height)!r}")834 835        shards = self.shards836        top_shards = []837        side_shards = self.shards838        bottom_shards = []839        if top:840            side_shards = shards_trim_top(shards, top)841            top_shards = shards_trim_rows(shards, top)842        if bottom:843            bottom_shards = shards_trim_top(side_shards, height)844            side_shards = shards_trim_rows(side_shards, height)845 846        left_shards = []847        right_shards = []848        if left > 0:849            left_shards = [shards_trim_sides(side_shards, 0, left)]850        if right > 0:851            right_shards = [shards_trim_sides(side_shards, max(0, left + width), right)]852 853        if not self.rows():854            middle_shards = []855        elif left or right:856            middle_shards = shards_join((*left_shards, other.shards, *right_shards))857        else:858            middle_shards = other.shards859 860        self.shards = top_shards + middle_shards + bottom_shards861 862        self.coords.update(other.translate_coords(left, top))863 864    def fill_attr(self, a: Hashable) -> None:865        """866        Apply attribute a to all areas of this canvas with default attribute currently set to None,867        leaving other attributes intact.868        """869        self.fill_attr_apply({None: a})870 871    def fill_attr_apply(self, mapping: dict[Hashable | None, Hashable]) -> None:872        """873        Apply an attribute-mapping dictionary to the canvas.874 875        mapping -- dictionary of original-attribute:new-attribute items876        """877        if self.widget_info:878            raise self._finalized_error879 880        shards = []881        for num_rows, original_cviews in self.shards:882            new_cviews = []883            for cv in original_cviews:884                # cv[4] == attr_map885                if cv[4] is None:886                    new_cviews.append((*cv[:4], mapping, *cv[5:]))887                else:888                    combined = mapping.copy()889                    combined.update([(k, mapping.get(v, v)) for k, v in cv[4].items()])890                    new_cviews.append((*cv[:4], combined, *cv[5:]))891            shards.append((num_rows, new_cviews))892        self.shards = shards893 894    def set_depends(self, widget_list: Sequence[Widget]) -> None:895        """896        Explicitly specify the list of widgets that this canvas897        depends on.  If any of these widgets change this canvas898        will have to be updated.899        """900        if self.widget_info:901            raise self._finalized_error902 903        self.depends_on = widget_list904 905 906def shard_body_row(sbody):907    """908    Return one row, advancing the iterators in sbody.909 910    ** MODIFIES sbody by calling next() on its iterators **911    """912    row = []913    for _done_rows, content_iter, cview in sbody:914        if content_iter:915            row.extend(next(content_iter))916        else:  # noqa: PLR5501  # pylint: disable=else-if-used  # readability917            # need to skip this unchanged canvas918            if row and isinstance(row[-1], int):919                row[-1] += cview[2]920            else:921                row.append(cview[2])922 923    return row924 925 926def shard_body_tail(num_rows: int, sbody):927    """928    Return a new shard tail that follows this shard body.929    """930    shard_tail = []931    col_gap = 0932 933    for done_rows, content_iter, cview in sbody:934        cols, rows = cview[2:4]935        done_rows += num_rows  # noqa: PLW2901936        if done_rows == rows:937            col_gap += cols938            continue939        shard_tail.append((col_gap, done_rows, content_iter, cview))940        col_gap = 0941    return shard_tail942 943 944def shards_delta(shards, other_shards):945    """946    Yield shards1 with cviews that are the same as shards2 having canv = None.947    """948    # pylint: disable=stop-iteration-return949    other_shards_iter = iter(other_shards)950    other_num_rows = other_cviews = None951    done = other_done = 0952    for num_rows, cviews in shards:953        if other_num_rows is None:954            other_num_rows, other_cviews = next(other_shards_iter)955        while other_done < done:956            other_done += other_num_rows957            other_num_rows, other_cviews = next(other_shards_iter)958        if other_done > done:959            yield (num_rows, cviews)960            done += num_rows961            continue962        # top-aligned shards, compare each cview963        yield (num_rows, shard_cviews_delta(cviews, other_cviews))964        other_done += other_num_rows965        other_num_rows = None966        done += num_rows967 968 969def shard_cviews_delta(cviews, other_cviews):970    # pylint: disable=stop-iteration-return971    other_cviews_iter = iter(other_cviews)972    other_cv = None973    cols = other_cols = 0974    for cv in cviews:975        if other_cv is None:976            other_cv = next(other_cviews_iter)977        while other_cols < cols:978            other_cols += other_cv[2]979            other_cv = next(other_cviews_iter)980        if other_cols > cols:981            yield cv982            cols += cv[2]983            continue984        # top-left-aligned cviews, compare them985        if cv[5] is other_cv[5] and cv[:5] == other_cv[:5]:986            yield (*cv[:5], None, *cv[6:])987        else:988            yield cv989        other_cols += other_cv[2]990        other_cv = None991        cols += cv[2]992 993 994def shard_body(cviews, shard_tail, create_iter: bool = True, iter_default=None):995    """996    Return a list of (done_rows, content_iter, cview) tuples for997    this shard and shard tail.998 999    If a canvas in cviews is None (eg. when unchanged from1000    shard_cviews_delta()) or if create_iter is False then no1001    iterator is created for content_iter.1002 1003    iter_default is the value used for content_iter when no iterator1004    is created.1005    """1006    col = 01007    body = []  # build the next shard tail1008    cviews_iter = iter(cviews)1009    for col_gap, done_rows, content_iter, tail_cview in shard_tail:1010        while col_gap:1011            try:1012                cview = next(cviews_iter)1013            except StopIteration:1014                break1015            (trim_left, trim_top, cols, rows, attr_map, canv) = cview[:6]1016            col += cols1017            col_gap -= cols  # noqa: PLW29011018            if col_gap < 0:1019                raise CanvasError("cviews overflow gaps in shard_tail!")1020            if create_iter and canv:1021                new_iter = canv.content(trim_left, trim_top, cols, rows, attr_map)1022            else:1023                new_iter = iter_default1024            body.append((0, new_iter, cview))1025        body.append((done_rows, content_iter, tail_cview))1026    for cview in cviews_iter:1027        (trim_left, trim_top, cols, rows, attr_map, canv) = cview[:6]1028        if create_iter and canv:1029            new_iter = canv.content(trim_left, trim_top, cols, rows, attr_map)1030        else:1031            new_iter = iter_default1032        body.append((0, new_iter, cview))1033    return body1034 1035 1036def shards_trim_top(shards, top: int):1037    """1038    Return shards with top rows removed.1039    """1040    if top <= 0:1041        raise ValueError(top)1042 1043    shard_iter = iter(shards)1044    shard_tail = []1045    # skip over shards that are completely removed1046    for num_rows, cviews in shard_iter:1047        if top < num_rows:1048            break1049        sbody = shard_body(cviews, shard_tail, False)1050        shard_tail = shard_body_tail(num_rows, sbody)1051        top -= num_rows1052    else:1053        raise CanvasError("tried to trim shards out of existence")1054 1055    sbody = shard_body(cviews, shard_tail, False)1056    shard_tail = shard_body_tail(num_rows, sbody)1057    # trim the top of this shard1058    new_sbody = [(0, content_iter, cview_trim_top(cv, done_rows + top)) for done_rows, content_iter, cv in sbody]1059 1060    sbody = new_sbody1061 1062    new_shards = [(num_rows - top, [cv for done_rows, content_iter, cv in sbody])]1063 1064    # write out the rest of the shards1065    new_shards.extend(shard_iter)1066 1067    return new_shards1068 1069 1070def shards_trim_rows(shards, keep_rows: int):1071    """1072    Return the topmost keep_rows rows from shards.1073    """1074    if keep_rows < 0:1075        raise ValueError(keep_rows)1076 1077    new_shards = []1078    done_rows = 01079    for num_rows, cviews in shards:1080        if done_rows >= keep_rows:1081            break1082        new_cviews = []1083        for cv in cviews:1084            if cv[3] + done_rows > keep_rows:1085                new_cviews.append(cview_trim_rows(cv, keep_rows - done_rows))1086            else:1087                new_cviews.append(cv)1088 1089        if num_rows + done_rows > keep_rows:1090            new_shards.append((keep_rows - done_rows, new_cviews))1091        else:1092            new_shards.append((num_rows, new_cviews))1093        done_rows += num_rows1094 1095    return new_shards1096 1097 1098def shards_trim_sides(shards, left: int, cols: int):1099    """1100    Return shards with starting from column left and cols total width.1101    """1102    if left < 0:1103        raise ValueError(left)1104    if cols <= 0:1105        raise ValueError(cols)1106    shard_tail = []1107    new_shards = []1108    right = left + cols1109    for num_rows, cviews in shards:1110        sbody = shard_body(cviews, shard_tail, False)1111        shard_tail = shard_body_tail(num_rows, sbody)1112        new_cviews = []1113        col = 01114        for done_rows, _content_iter, cv in sbody:1115            cv_cols = cv[2]1116            next_col = col + cv_cols1117            if done_rows or next_col <= left or col >= right:1118                col = next_col1119                continue1120            if col < left:1121                cv = cview_trim_left(cv, left - col)  # noqa: PLW29011122                col = left1123            if next_col > right:1124                cv = cview_trim_cols(cv, right - col)  # noqa: PLW29011125            new_cviews.append(cv)1126            col = next_col1127        if not new_cviews:1128            prev_num_rows, prev_cviews = new_shards[-1]1129            new_shards[-1] = (prev_num_rows + num_rows, prev_cviews)1130        else:1131            new_shards.append((num_rows, new_cviews))1132    return new_shards1133 1134 1135def shards_join(shard_lists):1136    """1137    Return the result of joining shard lists horizontally.1138    All shards lists must have the same number of rows.1139    """1140    shards_iters = [iter(sl) for sl in shard_lists]1141    shards_current = [next(i) for i in shards_iters]1142 1143    new_shards = []1144    while True:1145        new_cviews = []1146        num_rows = min(r for r, cv in shards_current)1147 1148        shards_next = []1149        for rows, cviews in shards_current:1150            if cviews:1151                new_cviews.extend(cviews)1152            shards_next.append((rows - num_rows, None))1153 1154        shards_current = shards_next1155        new_shards.append((num_rows, new_cviews))1156 1157        # advance to next shards1158        try:1159            for i in range(len(shards_current)):1160                if shards_current[i][0] > 0:1161                    continue1162                shards_current[i] = next(shards_iters[i])1163        except StopIteration:1164            break1165    return new_shards1166 1167 1168def cview_trim_rows(cv, rows: int):1169    return (*cv[:3], rows, *cv[4:])1170 1171 1172def cview_trim_top(cv, trim: int):1173    return (cv[0], trim + cv[1], cv[2], cv[3] - trim, *cv[4:])1174 1175 1176def cview_trim_left(cv, trim: int):1177    return (cv[0] + trim, cv[1], cv[2] - trim, *cv[3:])1178 1179 1180def cview_trim_cols(cv, cols: int):1181    return (*cv[:2], cols, *cv[3:])1182 1183 1184def CanvasCombine(canvas_info: Iterable[tuple[Canvas, typing.Any, bool]]) -> CompositeCanvas:1185    """Stack canvases in l vertically and return resulting canvas.1186 1187    :param canvas_info: list of (canvas, position, focus) tuples:1188 1189                        position1190                            a value that widget.set_focus will accept or None if not allowed1191                        focus1192                            True if this canvas is the one that would be in focus if the whole widget is in focus1193    """1194    clist = [(CompositeCanvas(c), p, f) for c, p, f in canvas_info]1195 1196    combined_canvas = CompositeCanvas()1197    shards = []1198    children = []1199    row = 01200    focus_index = 0

Showing the first 1,200 of 1393 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai