panel.py 10 KB

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