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