python (3.11.7)
       1  from typing import TYPE_CHECKING, Optional
       2  
       3  from .align import AlignMethod
       4  from .box import ROUNDED, Box
       5  from .cells import cell_len
       6  from .jupyter import JupyterMixin
       7  from .measure import Measurement, measure_renderables
       8  from .padding import Padding, PaddingDimensions
       9  from .segment import Segment
      10  from .style import Style, StyleType
      11  from .text import Text, TextType
      12  
      13  if TYPE_CHECKING:
      14      from .console import Console, ConsoleOptions, RenderableType, RenderResult
      15  
      16  
      17  class ESC[4;38;5;81mPanel(ESC[4;38;5;149mJupyterMixin):
      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          def align_text(
     154              text: Text, width: int, align: str, character: str, style: Style
     155          ) -> Text:
     156              """Gets new aligned text.
     157  
     158              Args:
     159                  text (Text): Title or subtitle text.
     160                  width (int): Desired width.
     161                  align (str): Alignment.
     162                  character (str): Character for alignment.
     163                  style (Style): Border style
     164  
     165              Returns:
     166                  Text: New text instance
     167              """
     168              text = text.copy()
     169              text.truncate(width)
     170              excess_space = width - cell_len(text.plain)
     171              if excess_space:
     172                  if align == "left":
     173                      return Text.assemble(
     174                          text,
     175                          (character * excess_space, style),
     176                          no_wrap=True,
     177                          end="",
     178                      )
     179                  elif align == "center":
     180                      left = excess_space // 2
     181                      return Text.assemble(
     182                          (character * left, style),
     183                          text,
     184                          (character * (excess_space - left), style),
     185                          no_wrap=True,
     186                          end="",
     187                      )
     188                  else:
     189                      return Text.assemble(
     190                          (character * excess_space, style),
     191                          text,
     192                          no_wrap=True,
     193                          end="",
     194                      )
     195              return text
     196  
     197          title_text = self._title
     198          if title_text is not None:
     199              title_text.stylize_before(border_style)
     200  
     201          child_width = (
     202              width - 2
     203              if self.expand
     204              else console.measure(
     205                  renderable, options=options.update_width(width - 2)
     206              ).maximum
     207          )
     208          child_height = self.height or options.height or None
     209          if child_height:
     210              child_height -= 2
     211          if title_text is not None:
     212              child_width = min(
     213                  options.max_width - 2, max(child_width, title_text.cell_len + 2)
     214              )
     215  
     216          width = child_width + 2
     217          child_options = options.update(
     218              width=child_width, height=child_height, highlight=self.highlight
     219          )
     220          lines = console.render_lines(renderable, child_options, style=style)
     221  
     222          line_start = Segment(box.mid_left, border_style)
     223          line_end = Segment(f"{box.mid_right}", border_style)
     224          new_line = Segment.line()
     225          if title_text is None or width <= 4:
     226              yield Segment(box.get_top([width - 2]), border_style)
     227          else:
     228              title_text = align_text(
     229                  title_text,
     230                  width - 4,
     231                  self.title_align,
     232                  box.top,
     233                  border_style,
     234              )
     235              yield Segment(box.top_left + box.top, border_style)
     236              yield from console.render(title_text, child_options.update_width(width - 4))
     237              yield Segment(box.top + box.top_right, border_style)
     238  
     239          yield new_line
     240          for line in lines:
     241              yield line_start
     242              yield from line
     243              yield line_end
     244              yield new_line
     245  
     246          subtitle_text = self._subtitle
     247          if subtitle_text is not None:
     248              subtitle_text.stylize_before(border_style)
     249  
     250          if subtitle_text is None or width <= 4:
     251              yield Segment(box.get_bottom([width - 2]), border_style)
     252          else:
     253              subtitle_text = align_text(
     254                  subtitle_text,
     255                  width - 4,
     256                  self.subtitle_align,
     257                  box.bottom,
     258                  border_style,
     259              )
     260              yield Segment(box.bottom_left + box.bottom, border_style)
     261              yield from console.render(
     262                  subtitle_text, child_options.update_width(width - 4)
     263              )
     264              yield Segment(box.bottom + box.bottom_right, border_style)
     265  
     266          yield new_line
     267  
     268      def __rich_measure__(
     269          self, console: "Console", options: "ConsoleOptions"
     270      ) -> "Measurement":
     271          _title = self._title
     272          _, right, _, left = Padding.unpack(self.padding)
     273          padding = left + right
     274          renderables = [self.renderable, _title] if _title else [self.renderable]
     275  
     276          if self.width is None:
     277              width = (
     278                  measure_renderables(
     279                      console,
     280                      options.update_width(options.max_width - padding - 2),
     281                      renderables,
     282                  ).maximum
     283                  + padding
     284                  + 2
     285              )
     286          else:
     287              width = self.width
     288          return Measurement(width, width)
     289  
     290  
     291  if __name__ == "__main__":  # pragma: no cover
     292      from .console import Console
     293  
     294      c = Console()
     295  
     296      from .box import DOUBLE, ROUNDED
     297      from .padding import Padding
     298  
     299      p = Panel(
     300          "Hello, World!",
     301          title="rich.Panel",
     302          style="white on blue",
     303          box=DOUBLE,
     304          padding=1,
     305      )
     306  
     307      c.print()
     308      c.print(p)