codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import typing4 5from .columns import Columns6from .constants import BOX_SYMBOLS, Align, WHSettings7from .divider import Divider8from .pile import Pile9from .solid_fill import SolidFill10from .text import Text11from .widget_decoration import WidgetDecoration, delegate_to_widget_mixin12 13if typing.TYPE_CHECKING:14 from typing_extensions import Literal15 16 from .widget import Widget17 18WrappedWidget = typing.TypeVar("WrappedWidget")19 20 21class LineBox(WidgetDecoration[WrappedWidget], delegate_to_widget_mixin("_wrapped_widget")):22 Symbols = BOX_SYMBOLS23 24 def __init__(25 self,26 original_widget: WrappedWidget,27 title: str = "",28 title_align: Literal["left", "center", "right"] | Align = Align.CENTER,29 title_attr=None,30 tlcorner: str = BOX_SYMBOLS.LIGHT.TOP_LEFT,31 tline: str = BOX_SYMBOLS.LIGHT.HORIZONTAL,32 lline: str = BOX_SYMBOLS.LIGHT.VERTICAL,33 trcorner: str = BOX_SYMBOLS.LIGHT.TOP_RIGHT,34 blcorner: str = BOX_SYMBOLS.LIGHT.BOTTOM_LEFT,35 rline: str = BOX_SYMBOLS.LIGHT.VERTICAL,36 bline: str = BOX_SYMBOLS.LIGHT.HORIZONTAL,37 brcorner: str = BOX_SYMBOLS.LIGHT.BOTTOM_RIGHT,38 ) -> None:39 """40 Draw a line around original_widget.41 42 Use 'title' to set an initial title text with will be centered43 on top of the box.44 45 Use `title_attr` to apply a specific attribute to the title text.46 47 Use `title_align` to align the title to the 'left', 'right', or 'center'.48 The default is 'center'.49 50 You can also override the widgets used for the lines/corners:51 tline: top line52 bline: bottom line53 lline: left line54 rline: right line55 tlcorner: top left corner56 trcorner: top right corner57 blcorner: bottom left corner58 brcorner: bottom right corner59 60 If empty string is specified for one of the lines/corners, then no character will be output there.61 If no top/bottom/left/right lines - whole lines will be omitted.62 This allows for seamless use of adjoining LineBoxes.63 64 Class attribute `Symbols` can be used as source for standard lines:65 66 >>> print(LineBox(Text("Some text")).render(()))67 ┌─────────┐68 │Some text│69 └─────────┘70 >>> print(71 ... LineBox(72 ... Text("Some text"),73 ... tlcorner=LineBox.Symbols.LIGHT.TOP_LEFT_ROUNDED,74 ... trcorner=LineBox.Symbols.LIGHT.TOP_RIGHT_ROUNDED,75 ... blcorner=LineBox.Symbols.LIGHT.BOTTOM_LEFT_ROUNDED,76 ... brcorner=LineBox.Symbols.LIGHT.BOTTOM_RIGHT_ROUNDED,77 ... ).render(())78 ... )79 ╭─────────╮80 │Some text│81 ╰─────────╯82 >>> print(83 ... LineBox(84 ... Text("Some text"),85 ... tline=LineBox.Symbols.HEAVY.HORIZONTAL,86 ... bline=LineBox.Symbols.HEAVY.HORIZONTAL,87 ... lline=LineBox.Symbols.HEAVY.VERTICAL,88 ... rline=LineBox.Symbols.HEAVY.VERTICAL,89 ... tlcorner=LineBox.Symbols.HEAVY.TOP_LEFT,90 ... trcorner=LineBox.Symbols.HEAVY.TOP_RIGHT,91 ... blcorner=LineBox.Symbols.HEAVY.BOTTOM_LEFT,92 ... brcorner=LineBox.Symbols.HEAVY.BOTTOM_RIGHT,93 ... ).render(())94 ... )95 ┏━━━━━━━━━┓96 ┃Some text┃97 ┗━━━━━━━━━┛98 99 To make Table constructions, some lineboxes need to be drawn without sides100 and T or CROSS symbols used for corners of cells.101 """102 103 w_lline = SolidFill(lline)104 w_rline = SolidFill(rline)105 106 w_tlcorner, w_tline, w_trcorner = Text(tlcorner), Divider(tline), Text(trcorner)107 w_blcorner, w_bline, w_brcorner = Text(blcorner), Divider(bline), Text(brcorner)108 109 if not tline and title:110 raise ValueError("Cannot have a title when tline is empty string")111 112 if title_attr:113 self.title_widget = Text((title_attr, self.format_title(title)))114 else:115 self.title_widget = Text(self.format_title(title))116 117 if tline:118 if title_align not in {Align.LEFT, Align.CENTER, Align.RIGHT}:119 raise ValueError('title_align must be one of "left", "right", or "center"')120 if title_align == Align.LEFT:121 tline_widgets = [(WHSettings.PACK, self.title_widget), w_tline]122 else:123 tline_widgets = [w_tline, (WHSettings.PACK, self.title_widget)]124 if title_align == Align.CENTER:125 tline_widgets.append(w_tline)126 127 self.tline_widget = Columns(tline_widgets)128 top = Columns(129 (130 (int(bool(tlcorner and lline)), w_tlcorner),131 self.tline_widget,132 (int(bool(trcorner and rline)), w_trcorner),133 )134 )135 136 else:137 self.tline_widget = None138 top = None139 140 # Note: We need to define a fixed first widget (even if it's 0 width) so that the other141 # widgets have something to anchor onto142 middle = Columns(143 ((int(bool(lline)), w_lline), original_widget, (int(bool(rline)), w_rline)),144 box_columns=[0, 2],145 focus_column=original_widget,146 )147 148 if bline:149 bottom = Columns(150 (151 (int(bool(blcorner and lline)), w_blcorner),152 w_bline,153 (int(bool(brcorner and rline)), w_brcorner),154 )155 )156 else:157 bottom = None158 159 pile_widgets = []160 if top:161 pile_widgets.append((WHSettings.PACK, top))162 pile_widgets.append(middle)163 if bottom:164 pile_widgets.append((WHSettings.PACK, bottom))165 166 self._wrapped_widget = Pile(pile_widgets, focus_item=middle)167 168 super().__init__(original_widget)169 170 @property171 def original_widget(self) -> WrappedWidget:172 return super().original_widget173 174 @original_widget.setter175 def original_widget(self, original_widget: WrappedWidget) -> None:176 v_index = int(bool(self.tline_widget)) # we care only about top177 h_index = 1 # constant178 middle: Columns = typing.cast("Columns", self._wrapped_widget[v_index])179 _old_widget, options = middle.contents[h_index]180 middle.contents[h_index] = (original_widget, options)181 WidgetDecoration.original_widget.fset(self, original_widget) # pylint: disable=no-member182 183 @property184 def _w(self) -> Pile:185 return self._wrapped_widget186 187 def format_title(self, text: str) -> str:188 if text:189 return f" {text} "190 191 return ""192 193 def set_title(self, text: str) -> None:194 if not self.tline_widget:195 raise ValueError("Cannot set title when tline is unset")196 self.title_widget.set_text(self.format_title(text))197 self.tline_widget._invalidate()198 199 @property200 def focus(self) -> Widget | None:201 """LineBox is partially container.202 203 While focus position is a bit hacky204 (formally it's not container and only position 0 available),205 focus widget is always provided by original widget.206 """207 return self._original_widget.focus208 