You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

318 line
11 KiB

  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): 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. border_style = style + console.get_style(self.border_style)
  139. width = (
  140. options.max_width
  141. if self.width is None
  142. else min(options.max_width, self.width)
  143. )
  144. safe_box: bool = console.safe_box if self.safe_box is None else self.safe_box
  145. box = self.box.substitute(options, safe=safe_box)
  146. def align_text(
  147. text: Text, width: int, align: str, character: str, style: Style
  148. ) -> Text:
  149. """Gets new aligned text.
  150. Args:
  151. text (Text): Title or subtitle text.
  152. width (int): Desired width.
  153. align (str): Alignment.
  154. character (str): Character for alignment.
  155. style (Style): Border style
  156. Returns:
  157. Text: New text instance
  158. """
  159. text = text.copy()
  160. text.truncate(width)
  161. excess_space = width - cell_len(text.plain)
  162. if text.style:
  163. text.stylize(console.get_style(text.style))
  164. if excess_space:
  165. if align == "left":
  166. return Text.assemble(
  167. text,
  168. (character * excess_space, style),
  169. no_wrap=True,
  170. end="",
  171. )
  172. elif align == "center":
  173. left = excess_space // 2
  174. return Text.assemble(
  175. (character * left, style),
  176. text,
  177. (character * (excess_space - left), style),
  178. no_wrap=True,
  179. end="",
  180. )
  181. else:
  182. return Text.assemble(
  183. (character * excess_space, style),
  184. text,
  185. no_wrap=True,
  186. end="",
  187. )
  188. return text
  189. title_text = self._title
  190. if title_text is not None:
  191. title_text.stylize_before(border_style)
  192. child_width = (
  193. width - 2
  194. if self.expand
  195. else console.measure(
  196. renderable, options=options.update_width(width - 2)
  197. ).maximum
  198. )
  199. child_height = self.height or options.height or None
  200. if child_height:
  201. child_height -= 2
  202. if title_text is not None:
  203. child_width = min(
  204. options.max_width - 2, max(child_width, title_text.cell_len + 2)
  205. )
  206. width = child_width + 2
  207. child_options = options.update(
  208. width=child_width, height=child_height, highlight=self.highlight
  209. )
  210. lines = console.render_lines(renderable, child_options, style=style)
  211. line_start = Segment(box.mid_left, border_style)
  212. line_end = Segment(f"{box.mid_right}", border_style)
  213. new_line = Segment.line()
  214. if title_text is None or width <= 4:
  215. yield Segment(box.get_top([width - 2]), border_style)
  216. else:
  217. title_text = align_text(
  218. title_text,
  219. width - 4,
  220. self.title_align,
  221. box.top,
  222. border_style,
  223. )
  224. yield Segment(box.top_left + box.top, border_style)
  225. yield from console.render(title_text, child_options.update_width(width - 4))
  226. yield Segment(box.top + box.top_right, border_style)
  227. yield new_line
  228. for line in lines:
  229. yield line_start
  230. yield from line
  231. yield line_end
  232. yield new_line
  233. subtitle_text = self._subtitle
  234. if subtitle_text is not None:
  235. subtitle_text.stylize_before(border_style)
  236. if subtitle_text is None or width <= 4:
  237. yield Segment(box.get_bottom([width - 2]), border_style)
  238. else:
  239. subtitle_text = align_text(
  240. subtitle_text,
  241. width - 4,
  242. self.subtitle_align,
  243. box.bottom,
  244. border_style,
  245. )
  246. yield Segment(box.bottom_left + box.bottom, border_style)
  247. yield from console.render(
  248. subtitle_text, child_options.update_width(width - 4)
  249. )
  250. yield Segment(box.bottom + box.bottom_right, border_style)
  251. yield new_line
  252. def __rich_measure__(
  253. self, console: "Console", options: "ConsoleOptions"
  254. ) -> "Measurement":
  255. _title = self._title
  256. _, right, _, left = Padding.unpack(self.padding)
  257. padding = left + right
  258. renderables = [self.renderable, _title] if _title else [self.renderable]
  259. if self.width is None:
  260. width = (
  261. measure_renderables(
  262. console,
  263. options.update_width(options.max_width - padding - 2),
  264. renderables,
  265. ).maximum
  266. + padding
  267. + 2
  268. )
  269. else:
  270. width = self.width
  271. return Measurement(width, width)
  272. if __name__ == "__main__": # pragma: no cover
  273. from .console import Console
  274. c = Console()
  275. from .box import DOUBLE, ROUNDED
  276. from .padding import Padding
  277. p = Panel(
  278. "Hello, World!",
  279. title="rich.Panel",
  280. style="white on blue",
  281. box=DOUBLE,
  282. padding=1,
  283. )
  284. c.print()
  285. c.print(p)