panel.py 8.4 KB

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