195 lines
6.7 KiB
Python
195 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import typing
|
|
|
|
from .columns import Columns
|
|
from .constants import BOX_SYMBOLS, Align, WHSettings
|
|
from .divider import Divider
|
|
from .pile import Pile
|
|
from .solid_fill import SolidFill
|
|
from .text import Text
|
|
from .widget_decoration import WidgetDecoration, delegate_to_widget_mixin
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from typing_extensions import Literal
|
|
|
|
from .widget import Widget
|
|
|
|
WrappedWidget = typing.TypeVar("WrappedWidget")
|
|
|
|
|
|
class LineBox(WidgetDecoration[WrappedWidget], delegate_to_widget_mixin("_wrapped_widget")):
|
|
Symbols = BOX_SYMBOLS
|
|
|
|
def __init__(
|
|
self,
|
|
original_widget: WrappedWidget,
|
|
title: str = "",
|
|
title_align: Literal["left", "center", "right"] | Align = Align.CENTER,
|
|
title_attr=None,
|
|
tlcorner: str = BOX_SYMBOLS.LIGHT.TOP_LEFT,
|
|
tline: str = BOX_SYMBOLS.LIGHT.HORIZONTAL,
|
|
lline: str = BOX_SYMBOLS.LIGHT.VERTICAL,
|
|
trcorner: str = BOX_SYMBOLS.LIGHT.TOP_RIGHT,
|
|
blcorner: str = BOX_SYMBOLS.LIGHT.BOTTOM_LEFT,
|
|
rline: str = BOX_SYMBOLS.LIGHT.VERTICAL,
|
|
bline: str = BOX_SYMBOLS.LIGHT.HORIZONTAL,
|
|
brcorner: str = BOX_SYMBOLS.LIGHT.BOTTOM_RIGHT,
|
|
) -> None:
|
|
"""
|
|
Draw a line around original_widget.
|
|
|
|
Use 'title' to set an initial title text with will be centered
|
|
on top of the box.
|
|
|
|
Use `title_attr` to apply a specific attribute to the title text.
|
|
|
|
Use `title_align` to align the title to the 'left', 'right', or 'center'.
|
|
The default is 'center'.
|
|
|
|
You can also override the widgets used for the lines/corners:
|
|
tline: top line
|
|
bline: bottom line
|
|
lline: left line
|
|
rline: right line
|
|
tlcorner: top left corner
|
|
trcorner: top right corner
|
|
blcorner: bottom left corner
|
|
brcorner: bottom right corner
|
|
|
|
If empty string is specified for one of the lines/corners, then no character will be output there.
|
|
If no top/bottom/left/right lines - whole lines will be omitted.
|
|
This allows for seamless use of adjoining LineBoxes.
|
|
|
|
Class attribute `Symbols` can be used as source for standard lines:
|
|
|
|
>>> print(LineBox(Text("Some text")).render(()))
|
|
┌─────────┐
|
|
│Some text│
|
|
└─────────┘
|
|
>>> print(
|
|
... LineBox(
|
|
... Text("Some text"),
|
|
... tlcorner=LineBox.Symbols.LIGHT.TOP_LEFT_ROUNDED,
|
|
... trcorner=LineBox.Symbols.LIGHT.TOP_RIGHT_ROUNDED,
|
|
... blcorner=LineBox.Symbols.LIGHT.BOTTOM_LEFT_ROUNDED,
|
|
... brcorner=LineBox.Symbols.LIGHT.BOTTOM_RIGHT_ROUNDED,
|
|
... ).render(())
|
|
... )
|
|
╭─────────╮
|
|
│Some text│
|
|
╰─────────╯
|
|
>>> print(
|
|
... LineBox(
|
|
... Text("Some text"),
|
|
... tline=LineBox.Symbols.HEAVY.HORIZONTAL,
|
|
... bline=LineBox.Symbols.HEAVY.HORIZONTAL,
|
|
... lline=LineBox.Symbols.HEAVY.VERTICAL,
|
|
... rline=LineBox.Symbols.HEAVY.VERTICAL,
|
|
... tlcorner=LineBox.Symbols.HEAVY.TOP_LEFT,
|
|
... trcorner=LineBox.Symbols.HEAVY.TOP_RIGHT,
|
|
... blcorner=LineBox.Symbols.HEAVY.BOTTOM_LEFT,
|
|
... brcorner=LineBox.Symbols.HEAVY.BOTTOM_RIGHT,
|
|
... ).render(())
|
|
... )
|
|
┏━━━━━━━━━┓
|
|
┃Some text┃
|
|
┗━━━━━━━━━┛
|
|
|
|
To make Table constructions, some lineboxes need to be drawn without sides
|
|
and T or CROSS symbols used for corners of cells.
|
|
"""
|
|
|
|
w_lline = SolidFill(lline)
|
|
w_rline = SolidFill(rline)
|
|
|
|
w_tlcorner, w_tline, w_trcorner = Text(tlcorner), Divider(tline), Text(trcorner)
|
|
w_blcorner, w_bline, w_brcorner = Text(blcorner), Divider(bline), Text(brcorner)
|
|
|
|
if not tline and title:
|
|
raise ValueError("Cannot have a title when tline is empty string")
|
|
|
|
if title_attr:
|
|
self.title_widget = Text((title_attr, self.format_title(title)))
|
|
else:
|
|
self.title_widget = Text(self.format_title(title))
|
|
|
|
if tline:
|
|
if title_align not in {Align.LEFT, Align.CENTER, Align.RIGHT}:
|
|
raise ValueError('title_align must be one of "left", "right", or "center"')
|
|
if title_align == Align.LEFT:
|
|
tline_widgets = [(WHSettings.PACK, self.title_widget), w_tline]
|
|
else:
|
|
tline_widgets = [w_tline, (WHSettings.PACK, self.title_widget)]
|
|
if title_align == Align.CENTER:
|
|
tline_widgets.append(w_tline)
|
|
|
|
self.tline_widget = Columns(tline_widgets)
|
|
top = Columns(
|
|
(
|
|
(int(bool(tlcorner and lline)), w_tlcorner),
|
|
self.tline_widget,
|
|
(int(bool(trcorner and rline)), w_trcorner),
|
|
)
|
|
)
|
|
|
|
else:
|
|
self.tline_widget = None
|
|
top = None
|
|
|
|
# Note: We need to define a fixed first widget (even if it's 0 width) so that the other
|
|
# widgets have something to anchor onto
|
|
middle = Columns(
|
|
((int(bool(lline)), w_lline), original_widget, (int(bool(rline)), w_rline)),
|
|
box_columns=[0, 2],
|
|
focus_column=original_widget,
|
|
)
|
|
|
|
if bline:
|
|
bottom = Columns(
|
|
(
|
|
(int(bool(blcorner and lline)), w_blcorner),
|
|
w_bline,
|
|
(int(bool(brcorner and rline)), w_brcorner),
|
|
)
|
|
)
|
|
else:
|
|
bottom = None
|
|
|
|
pile_widgets = []
|
|
if top:
|
|
pile_widgets.append((WHSettings.PACK, top))
|
|
pile_widgets.append(middle)
|
|
if bottom:
|
|
pile_widgets.append((WHSettings.PACK, bottom))
|
|
|
|
self._wrapped_widget = Pile(pile_widgets, focus_item=middle)
|
|
|
|
super().__init__(original_widget)
|
|
|
|
@property
|
|
def _w(self) -> Pile:
|
|
return self._wrapped_widget
|
|
|
|
def format_title(self, text: str) -> str:
|
|
if text:
|
|
return f" {text} "
|
|
|
|
return ""
|
|
|
|
def set_title(self, text: str) -> None:
|
|
if not self.tline_widget:
|
|
raise ValueError("Cannot set title when tline is unset")
|
|
self.title_widget.set_text(self.format_title(text))
|
|
self.tline_widget._invalidate()
|
|
|
|
@property
|
|
def focus(self) -> Widget | None:
|
|
"""LineBox is partially container.
|
|
|
|
While focus position is a bit hacky
|
|
(formally it's not container and only position 0 available),
|
|
focus widget is always provided by original widget.
|
|
"""
|
|
return self._original_widget.focus
|