Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
padding.py576 linesDownload Raw Back to widget
1from __future__ import annotations2 3import typing4import warnings5 6from urwid.canvas import CompositeCanvas, SolidCanvas7from urwid.split_repr import remove_defaults8from urwid.util import int_scale9 10from .constants import (11    RELATIVE_100,12    Align,13    Sizing,14    WHSettings,15    normalize_align,16    normalize_width,17    simplify_align,18    simplify_width,19)20from .widget_decoration import WidgetDecoration, WidgetError, WidgetWarning21 22if typing.TYPE_CHECKING:23    from collections.abc import Iterator24 25    from typing_extensions import Literal26 27WrappedWidget = typing.TypeVar("WrappedWidget")28 29 30class PaddingError(WidgetError):31    """Padding related errors."""32 33 34class PaddingWarning(WidgetWarning):35    """Padding related warnings."""36 37 38class Padding(WidgetDecoration[WrappedWidget], typing.Generic[WrappedWidget]):39    def __init__(40        self,41        w: WrappedWidget,42        align: (43            Literal["left", "center", "right"]44            | Align45            | tuple[Literal["relative", WHSettings.RELATIVE, "fixed left", "fixed right"], int]46        ) = Align.LEFT,47        width: (48            int49            | Literal["pack", "clip", WHSettings.PACK, WHSettings.CLIP]50            | tuple[Literal["relative", WHSettings.RELATIVE, "fixed left", "fixed right"], int]51        ) = RELATIVE_100,52        min_width: int | None = None,53        left: int = 0,54        right: int = 0,55    ) -> None:56        """57        :param w: a box, flow or fixed widget to pad on the left and/or right58            this widget is stored as self.original_widget59        :type w: Widget60 61        :param align: one of: ``'left'``, ``'center'``, ``'right'``62            (``'relative'``, *percentage* 0=left 100=right)63 64        :param width: one of:65 66            *given width*67              integer number of columns for self.original_widget68 69            ``'pack'``70              try to pack self.original_widget to its ideal size71 72            (``'relative'``, *percentage of total width*)73              make width depend on the container's width74 75            ``'clip'``76              to enable clipping mode for a fixed widget77 78        :param min_width: the minimum number of columns for79            self.original_widget or ``None``80        :type min_width: int | None81 82        :param left: a fixed number of columns to pad on the left83        :type left: int84 85        :param right: a fixed number of columns to pad on the right86        :type right: int87 88        Clipping Mode: (width= ``'clip'``)89        In clipping mode this padding widget will behave as a flow90        widget and self.original_widget will be treated as a fixed widget.91        self.original_widget will be clipped to fit the available number of columns.92        For example if align is ``'left'`` then self.original_widget may be clipped on the right.93 94        Pack Mode: (width= ``'pack'``)95        In pack mode is supported FIXED operation if it is supported by the original widget.96 97        >>> from urwid import Divider, Text, BigText, FontRegistry98        >>> from urwid.util import set_temporary_encoding99        >>> size = (7,)100        >>> def pr(w):101        ...     with set_temporary_encoding("utf-8"):102        ...         for t in w.render(size).text:103        ...             print(f"|{t.decode('utf-8')}|")104        >>> pr(Padding(Text("Head"), ("relative", 20), "pack"))105        | Head  |106        >>> pr(Padding(Divider("-"), left=2, right=1))107        |  ---- |108        >>> pr(Padding(Divider("*"), "center", 3))109        |  ***  |110        >>> p = Padding(Text("1234"), "left", 2, None, 1, 1)111        >>> p112        <Padding fixed/flow widget <Text fixed/flow widget '1234'> left=1 right=1 width=2>113        >>> pr(p)  # align against left114        | 12    |115        | 34    |116        >>> p.align = "right"117        >>> pr(p)  # align against right118        |    12 |119        |    34 |120        >>> pr(Padding(Text("hi\\nthere"), "right", "pack"))  # pack text first121        |  hi   |122        |  there|123        >>> pr(Padding(BigText("1,2,3", FontRegistry["Thin 3x3"]()), width="clip"))124        | ┐  ┌─┐|125        | │  ┌─┘|126        | ┴ ,└─ |127        """128        super().__init__(w)129 130        # convert obsolete parameters 'fixed left' and 'fixed right':131        if isinstance(align, tuple) and align[0] in {"fixed left", "fixed right"}:132            if align[0] == "fixed left":133                left = align[1]134                align = Align.LEFT135            else:136                right = align[1]137                align = Align.RIGHT138        if isinstance(width, tuple) and width[0] in {"fixed left", "fixed right"}:139            if width[0] == "fixed left":140                left = width[1]141            else:142                right = width[1]143            width = RELATIVE_100144 145        # convert old clipping mode width=None to width='clip'146        if width is None:147            width = WHSettings.CLIP148 149        self.left = left150        self.right = right151        self._align_type, self._align_amount = normalize_align(align, PaddingError)152        self._width_type, self._width_amount = normalize_width(width, PaddingError)153        self.min_width = min_width154 155    def sizing(self) -> frozenset[Sizing]:156        """Widget sizing.157 158        Rules:159        * width == CLIP: only FLOW is supported, and wrapped widget should support FIXED160        * width == GIVEN: FIXED is supported, and wrapped widget should support FLOW161        * All other cases: use sizing of target widget162        """163        if self._width_type == WHSettings.CLIP:164            return frozenset((Sizing.FLOW,))165 166        sizing = set(self.original_widget.sizing())167        if self._width_type == WHSettings.GIVEN:168            if Sizing.FLOW in sizing:169                sizing.add(Sizing.FIXED)170 171            elif Sizing.BOX not in sizing:172                warnings.warn(173                    f"WHSettings.GIVEN expect BOX or FLOW widget to be used, but received {self.original_widget}",174                    PaddingWarning,175                    stacklevel=3,176                )177 178        return frozenset(sizing)179 180    def _repr_attrs(self) -> dict[str, typing.Any]:181        attrs = {182            **super()._repr_attrs(),183            "align": self.align,184            "width": self.width,185            "left": self.left,186            "right": self.right,187            "min_width": self.min_width,188        }189        return remove_defaults(attrs, Padding.__init__)190 191    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:192        yield "w", self.original_widget193        yield "align", self.align194        yield "width", self.width195        yield "min_width", self.min_width196        yield "left", self.left197        yield "right", self.right198 199    @property200    def align(201        self,202    ) -> Literal["left", "center", "right"] | Align | tuple[Literal["relative", WHSettings.RELATIVE], int]:203        """204        Return the padding alignment setting.205        """206        return simplify_align(self._align_type, self._align_amount)207 208    @align.setter209    def align(210        self, align: Literal["left", "center", "right"] | Align | tuple[Literal["relative", WHSettings.RELATIVE], int]211    ) -> None:212        """213        Set the padding alignment.214        """215        self._align_type, self._align_amount = normalize_align(align, PaddingError)216        self._invalidate()217 218    @property219    def width(220        self,221    ) -> (222        Literal["clip", "pack", WHSettings.CLIP, WHSettings.PACK]223        | int224        | tuple[Literal["relative", WHSettings.RELATIVE], int]225    ):226        """227        Return the padding width.228        """229        return simplify_width(self._width_type, self._width_amount)230 231    @width.setter232    def width(233        self,234        width: (235            Literal["clip", "pack", WHSettings.CLIP, WHSettings.PACK]236            | int237            | tuple[Literal["relative", WHSettings.RELATIVE], int]238        ),239    ) -> None:240        """241        Set the padding width.242        """243        self._width_type, self._width_amount = normalize_width(width, PaddingError)244        self._invalidate()245 246    def pack(247        self,248        size: tuple[()] | tuple[int] | tuple[int, int] = (),249        focus: bool = False,250    ) -> tuple[int, int]:251        if size:252            return super().pack(size, focus)253        if self._width_type == WHSettings.CLIP:254            raise PaddingError("WHSettings.CLIP makes Padding FLOW-only widget")255 256        expand = self.left + self.right257        w_sizing = self.original_widget.sizing()258 259        if self._width_type == WHSettings.GIVEN:260            if Sizing.FLOW not in w_sizing:261                warnings.warn(262                    f"WHSettings.GIVEN expect FLOW widget to be used for FIXED pack/render, "263                    f"but received {self.original_widget}",264                    PaddingWarning,265                    stacklevel=3,266                )267 268            return (269                max(self._width_amount, self.min_width or 1) + expand,270                self.original_widget.rows((self._width_amount,), focus),271            )272 273        if Sizing.FIXED not in w_sizing:274            warnings.warn(275                f"Padded widget should support FIXED sizing for FIXED render, but received {self.original_widget}",276                PaddingWarning,277                stacklevel=3,278            )279        width, height = self.original_widget.pack(size, focus)280 281        if self._width_type == WHSettings.PACK:282            return max(width, self.min_width or 1) + expand, height283 284        if self._width_type == WHSettings.RELATIVE:285            return max(int(width * 100 / self._width_amount + 0.5), self.min_width or 1) + expand, height286 287        raise PaddingError(f"Unexpected width type: {self._width_type.upper()})")288 289    def render(290        self,291        size: tuple[()] | tuple[int] | tuple[int, int],292        focus: bool = False,293    ) -> CompositeCanvas:294        left, right = self.padding_values(size, focus)295 296        if self._width_type == WHSettings.CLIP:297            canv = self._original_widget.render((), focus)298        elif size:299            maxcol = size[0] - (left + right)300            if self._width_type == WHSettings.GIVEN and maxcol < self._width_amount:301                warnings.warn(302                    f"{self}.render(size={size}, focus={focus}): too narrow size ({maxcol!r} < {self._width_amount!r})",303                    PaddingWarning,304                    stacklevel=3,305                )306            canv = self._original_widget.render((maxcol, *size[1:]), focus)307        elif self._width_type == WHSettings.GIVEN:308            canv = self._original_widget.render((self._width_amount, *size[1:]), focus)309        else:310            canv = self._original_widget.render((), focus)311 312        if canv.cols() == 0:313            canv = SolidCanvas(" ", size[0], canv.rows())314            canv = CompositeCanvas(canv)315            canv.set_depends([self._original_widget])316            return canv317 318        canv = CompositeCanvas(canv)319        canv.set_depends([self._original_widget])320        if left != 0 or right != 0:321            canv.pad_trim_left_right(left, right)322 323        return canv324 325    def padding_values(326        self,327        size: tuple[()] | tuple[int] | tuple[int, int],328        focus: bool,329    ) -> tuple[int, int]:330        """Return the number of columns to pad on the left and right.331 332        Override this method to define custom padding behaviour."""333        if self._width_type == WHSettings.CLIP:334            width, _ignore = self._original_widget.pack((), focus=focus)335            if not size:336                raise PaddingError("WHSettings.CLIP makes Padding FLOW-only widget")337            return calculate_left_right_padding(338                size[0],339                self._align_type,340                self._align_amount,341                WHSettings.CLIP,342                width,343                None,344                self.left,345                self.right,346            )347 348        if self._width_type == WHSettings.PACK:349            if size:350                maxcol = size[0]351                maxwidth = max(maxcol - self.left - self.right, self.min_width or 0)352                (width, _ignore) = self._original_widget.pack((maxwidth,), focus=focus)353            else:354                (width, _ignore) = self._original_widget.pack((), focus=focus)355                maxcol = width + self.left + self.right356 357            return calculate_left_right_padding(358                maxcol,359                self._align_type,360                self._align_amount,361                WHSettings.GIVEN,362                width,363                self.min_width,364                self.left,365                self.right,366            )367 368        if size:369            maxcol = size[0]370        elif self._width_type == WHSettings.GIVEN:371            maxcol = self._width_amount + self.left + self.right372        else:373            maxcol = (374                max(self._original_widget.pack((), focus=focus)[0] * 100 // self._width_amount, self.min_width or 1)375                + self.left376                + self.right377            )378 379        return calculate_left_right_padding(380            maxcol,381            self._align_type,382            self._align_amount,383            self._width_type,384            self._width_amount,385            self.min_width,386            self.left,387            self.right,388        )389 390    def rows(self, size: tuple[int], focus: bool = False) -> int:391        """Return the rows needed for self.original_widget."""392        (maxcol,) = size393        left, right = self.padding_values(size, focus)394        if self._width_type == WHSettings.PACK:395            _pcols, prows = self._original_widget.pack((maxcol - left - right,), focus)396            return prows397        if self._width_type == WHSettings.CLIP:398            _fcols, frows = self._original_widget.pack((), focus)399            return frows400        return self._original_widget.rows((maxcol - left - right,), focus=focus)401 402    def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None:403        """Pass keypress to self._original_widget."""404        left, right = self.padding_values(size, True)405        if size:406            maxvals = (size[0] - left - right, *size[1:])407            return self._original_widget.keypress(maxvals, key)408        return self._original_widget.keypress((), key)409 410    def get_cursor_coords(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> tuple[int, int] | None:411        """Return the (x,y) coordinates of cursor within self._original_widget."""412        if not hasattr(self._original_widget, "get_cursor_coords"):413            return None414 415        left, right = self.padding_values(size, True)416        if size:417            maxvals = (size[0] - left - right, *size[1:])418            if maxvals[0] == 0:419                return None420        else:421            maxvals = ()422 423        if (coords := self._original_widget.get_cursor_coords(maxvals)) is not None:424            x, y = coords425            return x + left, y426 427        return None428 429    def move_cursor_to_coords(430        self,431        size: tuple[()] | tuple[int] | tuple[int, int],432        x: int,433        y: int,434    ) -> bool:435        """Set the cursor position with (x,y) coordinates of self._original_widget.436 437        Returns True if move succeeded, False otherwise.438        """439        if not hasattr(self._original_widget, "move_cursor_to_coords"):440            return True441 442        left, right = self.padding_values(size, True)443        if size:444            maxcol = size[0]445            maxvals = (maxcol - left - right, *size[1:])446        else:447            maxcol = self.pack((), True)[0]448            maxvals = ()449 450        if isinstance(x, int):451            if x < left:452                x = left453            elif x >= maxcol - right:454                x = maxcol - right - 1455            x -= left456 457        return self._original_widget.move_cursor_to_coords(maxvals, x, y)458 459    def mouse_event(460        self,461        size: tuple[()] | tuple[int] | tuple[int, int],462        event: str,463        button: int,464        col: int,465        row: int,466        focus: bool,467    ) -> bool | None:468        """Send mouse event if position is within self._original_widget."""469        if not hasattr(self._original_widget, "mouse_event"):470            return False471 472        left, right = self.padding_values(size, focus)473        if size:474            maxcol = size[0]475            if col < left or col >= maxcol - right:476                return False477            maxvals = (maxcol - left - right, *size[1:])478        else:479            maxvals = ()480 481        return self._original_widget.mouse_event(maxvals, event, button, col - left, row, focus)482 483    def get_pref_col(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> int | None:484        """Return the preferred column from self._original_widget, or None."""485        if not hasattr(self._original_widget, "get_pref_col"):486            return None487 488        left, right = self.padding_values(size, True)489        if size:490            maxvals = (size[0] - left - right, *size[1:])491        else:492            maxvals = ()493 494        x = self._original_widget.get_pref_col(maxvals)495        if isinstance(x, int):496            return x + left497        return x498 499 500def calculate_left_right_padding(501    maxcol: int,502    align_type: Literal["left", "center", "right"] | Align,503    align_amount: int,504    width_type: Literal["fixed", "relative", "clip", "given", WHSettings.RELATIVE, WHSettings.CLIP, WHSettings.GIVEN],505    width_amount: int,506    min_width: int | None,507    left: int,508    right: int,509) -> tuple[int, int]:510    """511    Return the amount of padding (or clipping) on the left and512    right part of maxcol columns to satisfy the following:513 514    align_type -- 'left', 'center', 'right', 'relative'515    align_amount -- a percentage when align_type=='relative'516    width_type -- 'fixed', 'relative', 'clip'517    width_amount -- a percentage when width_type=='relative'518        otherwise equal to the width of the widget519    min_width -- a desired minimum width for the widget or None520    left -- a fixed number of columns to pad on the left521    right -- a fixed number of columns to pad on the right522 523    >>> clrp = calculate_left_right_padding524    >>> clrp(15, "left", 0, "given", 10, None, 2, 0)525    (2, 3)526    >>> clrp(15, "relative", 0, "given", 10, None, 2, 0)527    (2, 3)528    >>> clrp(15, "relative", 100, "given", 10, None, 2, 0)529    (5, 0)530    >>> clrp(15, "center", 0, "given", 4, None, 2, 0)531    (6, 5)532    >>> clrp(15, "left", 0, "clip", 18, None, 0, 0)533    (0, -3)534    >>> clrp(15, "right", 0, "clip", 18, None, 0, -1)535    (-2, -1)536    >>> clrp(15, "center", 0, "given", 18, None, 2, 0)537    (0, 0)538    >>> clrp(20, "left", 0, "relative", 60, None, 0, 0)539    (0, 8)540    >>> clrp(20, "relative", 30, "relative", 60, None, 0, 0)541    (2, 6)542    >>> clrp(20, "relative", 30, "relative", 60, 14, 0, 0)543    (2, 4)544    """545    if width_type == WHSettings.RELATIVE:546        maxwidth = max(maxcol - left - right, 0)547        width = int(maxwidth * width_amount / 100 + 0.5)548        if min_width is not None:549            width = max(width, min_width)550    else:551        width = width_amount552 553    align = {Align.LEFT: 0, Align.CENTER: 50, Align.RIGHT: 100}.get(align_type, align_amount)554 555    # add the remainder of left/right the padding556    padding = maxcol - width - left - right557    right += int_scale(100 - align, 101, padding + 1)558    left = maxcol - width - right559 560    # reduce padding if we are clipping an edge561    if right < 0 < left:562        shift = min(left, -right)563        left -= shift564        right += shift565    elif left < 0 < right:566        shift = min(right, -left)567        right -= shift568        left += shift569 570    # only clip if width_type == 'clip'571    if width_type != WHSettings.CLIP and (left < 0 or right < 0):572        left = max(left, 0)573        right = max(right, 0)574 575    return left, right576 
codekingpro/portable-devtools · Team Ai