1from operator import itemgetter
2from typing import Iterable, NamedTuple, TYPE_CHECKING
3
4from . import errors
5from .protocol import is_renderable
6
7if TYPE_CHECKING:
8    from .console import Console, ConsoleOptions, RenderableType
9
10
11class Measurement(NamedTuple):
12    """Stores the minimum and maximum widths (in characters) required to render an object."""
13
14    minimum: int
15    """Minimum number of cells required to render."""
16    maximum: int
17    """Maximum number of cells required to render."""
18
19    @property
20    def span(self) -> int:
21        """Get difference between maximum and minimum."""
22        return self.maximum - self.minimum
23
24    def normalize(self) -> "Measurement":
25        """Get measurement that ensures that minimum <= maximum and minimum >= 0
26
27        Returns:
28            Measurement: A normalized measurement.
29        """
30        minimum, maximum = self
31        minimum = min(max(0, minimum), maximum)
32        return Measurement(max(0, minimum), max(0, max(minimum, maximum)))
33
34    def with_maximum(self, width: int) -> "Measurement":
35        """Get a RenderableWith where the widths are <= width.
36
37        Args:
38            width (int): Maximum desired width.
39
40        Returns:
41            Measurement: New Measurement object.
42        """
43        minimum, maximum = self
44        return Measurement(min(minimum, width), min(maximum, width))
45
46    def with_minimum(self, width: int) -> "Measurement":
47        """Get a RenderableWith where the widths are >= width.
48
49        Args:
50            width (int): Minimum desired width.
51
52        Returns:
53            Measurement: New Measurement object.
54        """
55        minimum, maximum = self
56        width = max(0, width)
57        return Measurement(max(minimum, width), max(maximum, width))
58
59    def clamp(self, min_width: int = None, max_width: int = None) -> "Measurement":
60        """Clamp a measurement within the specified range.
61
62        Args:
63            min_width (int): Minimum desired width, or ``None`` for no minimum. Defaults to None.
64            max_width (int): Maximum desired width, or ``None`` for no maximum. Defaults to None.
65
66        Returns:
67            Measurement: New Measurement object.
68        """
69        measurement = self
70        if min_width is not None:
71            measurement = measurement.with_minimum(min_width)
72        if max_width is not None:
73            measurement = measurement.with_maximum(max_width)
74        return measurement
75
76    @classmethod
77    def get(
78        cls, console: "Console", options: "ConsoleOptions", renderable: "RenderableType"
79    ) -> "Measurement":
80        """Get a measurement for a renderable.
81
82        Args:
83            console (~rich.console.Console): Console instance.
84            options (~rich.console.ConsoleOptions): Console options.
85            renderable (RenderableType): An object that may be rendered with Rich.
86
87        Raises:
88            errors.NotRenderableError: If the object is not renderable.
89
90        Returns:
91            Measurement: Measurement object containing range of character widths required to render the object.
92        """
93        _max_width = options.max_width
94        if _max_width < 1:
95            return Measurement(0, 0)
96        if isinstance(renderable, str):
97            renderable = console.render_str(renderable, markup=options.markup)
98        if hasattr(renderable, "__rich__"):
99            renderable = renderable.__rich__()  # type: ignore
100        if is_renderable(renderable):
101            get_console_width = getattr(renderable, "__rich_measure__", None)
102            if get_console_width is not None:
103                render_width = (
104                    get_console_width(console, options)
105                    .normalize()
106                    .with_maximum(_max_width)
107                )
108                if render_width.maximum < 1:
109                    return Measurement(0, 0)
110                return render_width.normalize()
111            else:
112                return Measurement(0, _max_width)
113        else:
114            raise errors.NotRenderableError(
115                f"Unable to get render width for {renderable!r}; "
116                "a str, Segment, or object with __rich_console__ method is required"
117            )
118
119
120def measure_renderables(
121    console: "Console",
122    options: "ConsoleOptions",
123    renderables: Iterable["RenderableType"],
124) -> "Measurement":
125    """Get a measurement that would fit a number of renderables.
126
127    Args:
128        console (~rich.console.Console): Console instance.
129        renderables (Iterable[RenderableType]): One or more renderable objects.
130        max_width (int): The maximum width available.
131
132    Returns:
133        Measurement: Measurement object containing range of character widths required to
134        contain all given renderables.
135    """
136    if not renderables:
137        return Measurement(0, 0)
138    get_measurement = Measurement.get
139    measurements = [
140        get_measurement(console, options, renderable) for renderable in renderables
141    ]
142    measured_width = Measurement(
143        max(measurements, key=itemgetter(0)).minimum,
144        max(measurements, key=itemgetter(1)).maximum,
145    )
146    return measured_width
147