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