313 lines
10 KiB
Python
313 lines
10 KiB
Python
|
from typing import TYPE_CHECKING, Optional
|
||
|
|
||
|
from .align import AlignMethod
|
||
|
from .box import ROUNDED, Box
|
||
|
from .cells import cell_len
|
||
|
from .jupyter import JupyterMixin
|
||
|
from .measure import Measurement, measure_renderables
|
||
|
from .padding import Padding, PaddingDimensions
|
||
|
from .segment import Segment
|
||
|
from .style import Style, StyleType
|
||
|
from .text import Text, TextType
|
||
|
|
||
|
if TYPE_CHECKING:
|
||
|
from .console import Console, ConsoleOptions, RenderableType, RenderResult
|
||
|
|
||
|
|
||
|
class Panel(JupyterMixin):
|
||
|
"""A console renderable that draws a border around its contents.
|
||
|
|
||
|
Example:
|
||
|
>>> console.print(Panel("Hello, World!"))
|
||
|
|
||
|
Args:
|
||
|
renderable (RenderableType): A console renderable object.
|
||
|
box (Box, optional): A Box instance that defines the look of the border (see :ref:`appendix_box`.
|
||
|
Defaults to box.ROUNDED.
|
||
|
safe_box (bool, optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
|
||
|
expand (bool, optional): If True the panel will stretch to fill the console
|
||
|
width, otherwise it will be sized to fit the contents. Defaults to True.
|
||
|
style (str, optional): The style of the panel (border and contents). Defaults to "none".
|
||
|
border_style (str, optional): The style of the border. Defaults to "none".
|
||
|
width (Optional[int], optional): Optional width of panel. Defaults to None to auto-detect.
|
||
|
height (Optional[int], optional): Optional height of panel. Defaults to None to auto-detect.
|
||
|
padding (Optional[PaddingDimensions]): Optional padding around renderable. Defaults to 0.
|
||
|
highlight (bool, optional): Enable automatic highlighting of panel title (if str). Defaults to False.
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
renderable: "RenderableType",
|
||
|
box: Box = ROUNDED,
|
||
|
*,
|
||
|
title: Optional[TextType] = None,
|
||
|
title_align: AlignMethod = "center",
|
||
|
subtitle: Optional[TextType] = None,
|
||
|
subtitle_align: AlignMethod = "center",
|
||
|
safe_box: Optional[bool] = None,
|
||
|
expand: bool = True,
|
||
|
style: StyleType = "none",
|
||
|
border_style: StyleType = "none",
|
||
|
width: Optional[int] = None,
|
||
|
height: Optional[int] = None,
|
||
|
padding: PaddingDimensions = (0, 1),
|
||
|
highlight: bool = False,
|
||
|
) -> None:
|
||
|
self.renderable = renderable
|
||
|
self.box = box
|
||
|
self.title = title
|
||
|
self.title_align: AlignMethod = title_align
|
||
|
self.subtitle = subtitle
|
||
|
self.subtitle_align = subtitle_align
|
||
|
self.safe_box = safe_box
|
||
|
self.expand = expand
|
||
|
self.style = style
|
||
|
self.border_style = border_style
|
||
|
self.width = width
|
||
|
self.height = height
|
||
|
self.padding = padding
|
||
|
self.highlight = highlight
|
||
|
|
||
|
@classmethod
|
||
|
def fit(
|
||
|
cls,
|
||
|
renderable: "RenderableType",
|
||
|
box: Box = ROUNDED,
|
||
|
*,
|
||
|
title: Optional[TextType] = None,
|
||
|
title_align: AlignMethod = "center",
|
||
|
subtitle: Optional[TextType] = None,
|
||
|
subtitle_align: AlignMethod = "center",
|
||
|
safe_box: Optional[bool] = None,
|
||
|
style: StyleType = "none",
|
||
|
border_style: StyleType = "none",
|
||
|
width: Optional[int] = None,
|
||
|
height: Optional[int] = None,
|
||
|
padding: PaddingDimensions = (0, 1),
|
||
|
highlight: bool = False,
|
||
|
) -> "Panel":
|
||
|
"""An alternative constructor that sets expand=False."""
|
||
|
return cls(
|
||
|
renderable,
|
||
|
box,
|
||
|
title=title,
|
||
|
title_align=title_align,
|
||
|
subtitle=subtitle,
|
||
|
subtitle_align=subtitle_align,
|
||
|
safe_box=safe_box,
|
||
|
style=style,
|
||
|
border_style=border_style,
|
||
|
width=width,
|
||
|
height=height,
|
||
|
padding=padding,
|
||
|
highlight=highlight,
|
||
|
expand=False,
|
||
|
)
|
||
|
|
||
|
@property
|
||
|
def _title(self) -> Optional[Text]:
|
||
|
if self.title:
|
||
|
title_text = (
|
||
|
Text.from_markup(self.title)
|
||
|
if isinstance(self.title, str)
|
||
|
else self.title.copy()
|
||
|
)
|
||
|
title_text.end = ""
|
||
|
title_text.plain = title_text.plain.replace("\n", " ")
|
||
|
title_text.no_wrap = True
|
||
|
title_text.expand_tabs()
|
||
|
title_text.pad(1)
|
||
|
return title_text
|
||
|
return None
|
||
|
|
||
|
@property
|
||
|
def _subtitle(self) -> Optional[Text]:
|
||
|
if self.subtitle:
|
||
|
subtitle_text = (
|
||
|
Text.from_markup(self.subtitle)
|
||
|
if isinstance(self.subtitle, str)
|
||
|
else self.subtitle.copy()
|
||
|
)
|
||
|
subtitle_text.end = ""
|
||
|
subtitle_text.plain = subtitle_text.plain.replace("\n", " ")
|
||
|
subtitle_text.no_wrap = True
|
||
|
subtitle_text.expand_tabs()
|
||
|
subtitle_text.pad(1)
|
||
|
return subtitle_text
|
||
|
return None
|
||
|
|
||
|
def __rich_console__(
|
||
|
self, console: "Console", options: "ConsoleOptions"
|
||
|
) -> "RenderResult":
|
||
|
_padding = Padding.unpack(self.padding)
|
||
|
renderable = (
|
||
|
Padding(self.renderable, _padding) if any(_padding) else self.renderable
|
||
|
)
|
||
|
style = console.get_style(self.style)
|
||
|
border_style = style + console.get_style(self.border_style)
|
||
|
width = (
|
||
|
options.max_width
|
||
|
if self.width is None
|
||
|
else min(options.max_width, self.width)
|
||
|
)
|
||
|
|
||
|
safe_box: bool = console.safe_box if self.safe_box is None else self.safe_box
|
||
|
box = self.box.substitute(options, safe=safe_box)
|
||
|
|
||
|
def align_text(
|
||
|
text: Text, width: int, align: str, character: str, style: Style
|
||
|
) -> Text:
|
||
|
"""Gets new aligned text.
|
||
|
|
||
|
Args:
|
||
|
text (Text): Title or subtitle text.
|
||
|
width (int): Desired width.
|
||
|
align (str): Alignment.
|
||
|
character (str): Character for alignment.
|
||
|
style (Style): Border style
|
||
|
|
||
|
Returns:
|
||
|
Text: New text instance
|
||
|
"""
|
||
|
text = text.copy()
|
||
|
text.truncate(width)
|
||
|
excess_space = width - cell_len(text.plain)
|
||
|
if excess_space:
|
||
|
if align == "left":
|
||
|
return Text.assemble(
|
||
|
text,
|
||
|
(character * excess_space, style),
|
||
|
no_wrap=True,
|
||
|
end="",
|
||
|
)
|
||
|
elif align == "center":
|
||
|
left = excess_space // 2
|
||
|
return Text.assemble(
|
||
|
(character * left, style),
|
||
|
text,
|
||
|
(character * (excess_space - left), style),
|
||
|
no_wrap=True,
|
||
|
end="",
|
||
|
)
|
||
|
else:
|
||
|
return Text.assemble(
|
||
|
(character * excess_space, style),
|
||
|
text,
|
||
|
no_wrap=True,
|
||
|
end="",
|
||
|
)
|
||
|
return text
|
||
|
|
||
|
title_text = self._title
|
||
|
if title_text is not None:
|
||
|
title_text.stylize_before(border_style)
|
||
|
|
||
|
child_width = (
|
||
|
width - 2
|
||
|
if self.expand
|
||
|
else console.measure(
|
||
|
renderable, options=options.update_width(width - 2)
|
||
|
).maximum
|
||
|
)
|
||
|
child_height = self.height or options.height or None
|
||
|
if child_height:
|
||
|
child_height -= 2
|
||
|
if title_text is not None:
|
||
|
child_width = min(
|
||
|
options.max_width - 2, max(child_width, title_text.cell_len + 2)
|
||
|
)
|
||
|
|
||
|
width = child_width + 2
|
||
|
child_options = options.update(
|
||
|
width=child_width, height=child_height, highlight=self.highlight
|
||
|
)
|
||
|
lines = console.render_lines(renderable, child_options, style=style)
|
||
|
|
||
|
line_start = Segment(box.mid_left, border_style)
|
||
|
line_end = Segment(f"{box.mid_right}", border_style)
|
||
|
new_line = Segment.line()
|
||
|
if title_text is None or width <= 4:
|
||
|
yield Segment(box.get_top([width - 2]), border_style)
|
||
|
else:
|
||
|
title_text = align_text(
|
||
|
title_text,
|
||
|
width - 4,
|
||
|
self.title_align,
|
||
|
box.top,
|
||
|
border_style,
|
||
|
)
|
||
|
yield Segment(box.top_left + box.top, border_style)
|
||
|
yield from console.render(title_text, child_options.update_width(width - 4))
|
||
|
yield Segment(box.top + box.top_right, border_style)
|
||
|
|
||
|
yield new_line
|
||
|
for line in lines:
|
||
|
yield line_start
|
||
|
yield from line
|
||
|
yield line_end
|
||
|
yield new_line
|
||
|
|
||
|
subtitle_text = self._subtitle
|
||
|
if subtitle_text is not None:
|
||
|
subtitle_text.stylize_before(border_style)
|
||
|
|
||
|
if subtitle_text is None or width <= 4:
|
||
|
yield Segment(box.get_bottom([width - 2]), border_style)
|
||
|
else:
|
||
|
subtitle_text = align_text(
|
||
|
subtitle_text,
|
||
|
width - 4,
|
||
|
self.subtitle_align,
|
||
|
box.bottom,
|
||
|
border_style,
|
||
|
)
|
||
|
yield Segment(box.bottom_left + box.bottom, border_style)
|
||
|
yield from console.render(
|
||
|
subtitle_text, child_options.update_width(width - 4)
|
||
|
)
|
||
|
yield Segment(box.bottom + box.bottom_right, border_style)
|
||
|
|
||
|
yield new_line
|
||
|
|
||
|
def __rich_measure__(
|
||
|
self, console: "Console", options: "ConsoleOptions"
|
||
|
) -> "Measurement":
|
||
|
_title = self._title
|
||
|
_, right, _, left = Padding.unpack(self.padding)
|
||
|
padding = left + right
|
||
|
renderables = [self.renderable, _title] if _title else [self.renderable]
|
||
|
|
||
|
if self.width is None:
|
||
|
width = (
|
||
|
measure_renderables(
|
||
|
console,
|
||
|
options.update_width(options.max_width - padding - 2),
|
||
|
renderables,
|
||
|
).maximum
|
||
|
+ padding
|
||
|
+ 2
|
||
|
)
|
||
|
else:
|
||
|
width = self.width
|
||
|
return Measurement(width, width)
|
||
|
|
||
|
|
||
|
if __name__ == "__main__": # pragma: no cover
|
||
|
from .console import Console
|
||
|
|
||
|
c = Console()
|
||
|
|
||
|
from .box import DOUBLE, ROUNDED
|
||
|
from .padding import Padding
|
||
|
|
||
|
p = Panel(
|
||
|
"Hello, World!",
|
||
|
title="rich.Panel",
|
||
|
style="white on blue",
|
||
|
box=DOUBLE,
|
||
|
padding=1,
|
||
|
)
|
||
|
|
||
|
c.print()
|
||
|
c.print(p)
|