panel.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. from typing import TYPE_CHECKING, Optional
  2. from .align import AlignMethod
  3. from .box import ROUNDED, Box
  4. from .cells import cell_len
  5. from .jupyter import JupyterMixin
  6. from .measure import Measurement, measure_renderables
  7. from .padding import Padding, PaddingDimensions
  8. from .segment import Segment
  9. from .style import Style, StyleType
  10. from .text import Text, TextType
  11. if TYPE_CHECKING:
  12. from .console import Console, ConsoleOptions, RenderableType, RenderResult
  13. class Panel(JupyterMixin):
  14. """A console renderable that draws a border around its contents.
  15. Example:
  16. >>> console.print(Panel("Hello, World!"))
  17. Args:
  18. renderable (RenderableType): A console renderable object.
  19. box (Box, optional): A Box instance that defines the look of the border (see :ref:`appendix_box`. Defaults to box.ROUNDED.
  20. title (Optional[TextType], optional): Optional title displayed in panel header. Defaults to None.
  21. title_align (AlignMethod, optional): Alignment of title. Defaults to "center".
  22. subtitle (Optional[TextType], optional): Optional subtitle displayed in panel footer. Defaults to None.
  23. subtitle_align (AlignMethod, optional): Alignment of subtitle. Defaults to "center".
  24. safe_box (bool, optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
  25. 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.
  26. style (str, optional): The style of the panel (border and contents). Defaults to "none".
  27. border_style (str, optional): The style of the border. Defaults to "none".
  28. width (Optional[int], optional): Optional width of panel. Defaults to None to auto-detect.
  29. height (Optional[int], optional): Optional height of panel. Defaults to None to auto-detect.
  30. padding (Optional[PaddingDimensions]): Optional padding around renderable. Defaults to 0.
  31. highlight (bool, optional): Enable automatic highlighting of panel title (if str). Defaults to False.
  32. """
  33. def __init__(
  34. self,
  35. renderable: "RenderableType",
  36. box: Box = ROUNDED,
  37. *,
  38. title: Optional[TextType] = None,
  39. title_align: AlignMethod = "center",
  40. subtitle: Optional[TextType] = None,
  41. subtitle_align: AlignMethod = "center",
  42. safe_box: Optional[bool] = None,
  43. expand: bool = True,
  44. style: StyleType = "none",
  45. border_style: StyleType = "none",
  46. width: Optional[int] = None,
  47. height: Optional[int] = None,
  48. padding: PaddingDimensions = (0, 1),
  49. highlight: bool = False,
  50. ) -> None:
  51. self.renderable = renderable
  52. self.box = box
  53. self.title = title
  54. self.title_align: AlignMethod = title_align
  55. self.subtitle = subtitle
  56. self.subtitle_align = subtitle_align
  57. self.safe_box = safe_box
  58. self.expand = expand
  59. self.style = style
  60. self.border_style = border_style
  61. self.width = width
  62. self.height = height
  63. self.padding = padding
  64. self.highlight = highlight
  65. @classmethod
  66. def fit(
  67. cls,
  68. renderable: "RenderableType",
  69. box: Box = ROUNDED,
  70. *,
  71. title: Optional[TextType] = None,
  72. title_align: AlignMethod = "center",
  73. subtitle: Optional[TextType] = None,
  74. subtitle_align: AlignMethod = "center",
  75. safe_box: Optional[bool] = None,
  76. style: StyleType = "none",
  77. border_style: StyleType = "none",
  78. width: Optional[int] = None,
  79. height: Optional[int] = None,
  80. padding: PaddingDimensions = (0, 1),
  81. highlight: bool = False,
  82. ) -> "Panel":
  83. """An alternative constructor that sets expand=False."""
  84. return cls(
  85. renderable,
  86. box,
  87. title=title,
  88. title_align=title_align,
  89. subtitle=subtitle,
  90. subtitle_align=subtitle_align,
  91. safe_box=safe_box,
  92. style=style,
  93. border_style=border_style,
  94. width=width,
  95. height=height,
  96. padding=padding,
  97. highlight=highlight,
  98. expand=False,
  99. )
  100. @property
  101. def _title(self) -> Optional[Text]:
  102. if self.title:
  103. title_text = (
  104. Text.from_markup(self.title)
  105. if isinstance(self.title, str)
  106. else self.title.copy()
  107. )
  108. title_text.end = ""
  109. title_text.plain = title_text.plain.replace("\n", " ")
  110. title_text.no_wrap = True
  111. title_text.expand_tabs()
  112. title_text.pad(1)
  113. return title_text
  114. return None
  115. @property
  116. def _subtitle(self) -> Optional[Text]:
  117. if self.subtitle:
  118. subtitle_text = (
  119. Text.from_markup(self.subtitle)
  120. if isinstance(self.subtitle, str)
  121. else self.subtitle.copy()
  122. )
  123. subtitle_text.end = ""
  124. subtitle_text.plain = subtitle_text.plain.replace("\n", " ")
  125. subtitle_text.no_wrap = True
  126. subtitle_text.expand_tabs()
  127. subtitle_text.pad(1)
  128. return subtitle_text
  129. return None
  130. def __rich_console__(
  131. self, console: "Console", options: "ConsoleOptions"
  132. ) -> "RenderResult":
  133. _padding = Padding.unpack(self.padding)
  134. renderable = (
  135. Padding(self.renderable, _padding) if any(_padding) else self.renderable
  136. )
  137. style = console.get_style(self.style)
  138. partial_border_style = console.get_style(self.border_style)
  139. border_style = style + partial_border_style
  140. width = (
  141. options.max_width
  142. if self.width is None
  143. else min(options.max_width, self.width)
  144. )
  145. safe_box: bool = console.safe_box if self.safe_box is None else self.safe_box
  146. box = self.box.substitute(options, safe=safe_box)
  147. def align_text(
  148. text: Text, width: int, align: str, character: str, style: Style
  149. ) -> Text:
  150. """Gets new aligned text.
  151. Args:
  152. text (Text): Title or subtitle text.
  153. width (int): Desired width.
  154. align (str): Alignment.
  155. character (str): Character for alignment.
  156. style (Style): Border style
  157. Returns:
  158. Text: New text instance
  159. """
  160. text = text.copy()
  161. text.truncate(width)
  162. excess_space = width - cell_len(text.plain)
  163. if text.style:
  164. text.stylize(console.get_style(text.style))
  165. if excess_space:
  166. if align == "left":
  167. return Text.assemble(
  168. text,
  169. (character * excess_space, style),
  170. no_wrap=True,
  171. end="",
  172. )
  173. elif align == "center":
  174. left = excess_space // 2
  175. return Text.assemble(
  176. (character * left, style),
  177. text,
  178. (character * (excess_space - left), style),
  179. no_wrap=True,
  180. end="",
  181. )
  182. else:
  183. return Text.assemble(
  184. (character * excess_space, style),
  185. text,
  186. no_wrap=True,
  187. end="",
  188. )
  189. return text
  190. title_text = self._title
  191. if title_text is not None:
  192. title_text.stylize_before(partial_border_style)
  193. child_width = (
  194. width - 2
  195. if self.expand
  196. else console.measure(
  197. renderable, options=options.update_width(width - 2)
  198. ).maximum
  199. )
  200. child_height = self.height or options.height or None
  201. if child_height:
  202. child_height -= 2
  203. if title_text is not None:
  204. child_width = min(
  205. options.max_width - 2, max(child_width, title_text.cell_len + 2)
  206. )
  207. width = child_width + 2
  208. child_options = options.update(
  209. width=child_width, height=child_height, highlight=self.highlight
  210. )
  211. lines = console.render_lines(renderable, child_options, style=style)
  212. line_start = Segment(box.mid_left, border_style)
  213. line_end = Segment(f"{box.mid_right}", border_style)
  214. new_line = Segment.line()
  215. if title_text is None or width <= 4:
  216. yield Segment(box.get_top([width - 2]), border_style)
  217. else:
  218. title_text = align_text(
  219. title_text,
  220. width - 4,
  221. self.title_align,
  222. box.top,
  223. border_style,
  224. )
  225. yield Segment(box.top_left + box.top, border_style)
  226. yield from console.render(title_text, child_options.update_width(width - 4))
  227. yield Segment(box.top + box.top_right, border_style)
  228. yield new_line
  229. for line in lines:
  230. yield line_start
  231. yield from line
  232. yield line_end
  233. yield new_line
  234. subtitle_text = self._subtitle
  235. if subtitle_text is not None:
  236. subtitle_text.stylize_before(partial_border_style)
  237. if subtitle_text is None or width <= 4:
  238. yield Segment(box.get_bottom([width - 2]), border_style)
  239. else:
  240. subtitle_text = align_text(
  241. subtitle_text,
  242. width - 4,
  243. self.subtitle_align,
  244. box.bottom,
  245. border_style,
  246. )
  247. yield Segment(box.bottom_left + box.bottom, border_style)
  248. yield from console.render(
  249. subtitle_text, child_options.update_width(width - 4)
  250. )
  251. yield Segment(box.bottom + box.bottom_right, border_style)
  252. yield new_line
  253. def __rich_measure__(
  254. self, console: "Console", options: "ConsoleOptions"
  255. ) -> "Measurement":
  256. _title = self._title
  257. _, right, _, left = Padding.unpack(self.padding)
  258. padding = left + right
  259. renderables = [self.renderable, _title] if _title else [self.renderable]
  260. if self.width is None:
  261. width = (
  262. measure_renderables(
  263. console,
  264. options.update_width(options.max_width - padding - 2),
  265. renderables,
  266. ).maximum
  267. + padding
  268. + 2
  269. )
  270. else:
  271. width = self.width
  272. return Measurement(width, width)
  273. if __name__ == "__main__": # pragma: no cover
  274. from .console import Console
  275. c = Console()
  276. from .box import DOUBLE, ROUNDED
  277. from .padding import Padding
  278. p = Panel(
  279. "Hello, World!",
  280. title="rich.Panel",
  281. style="white on blue",
  282. box=DOUBLE,
  283. padding=1,
  284. )
  285. c.print()
  286. c.print(p)