5454from textual ._debug import get_caller_file_and_line
5555from textual ._dispatch_key import dispatch_key
5656from textual ._easing import DEFAULT_SCROLL_EASING
57+ from textual ._extrema import Extrema
5758from textual ._styles_cache import StylesCache
5859from textual ._types import AnimationLevel
5960from textual .actions import SkipAction
@@ -504,6 +505,8 @@ def __init__(
504505 """Time of last scroll."""
505506 self ._user_scroll_interrupt : bool = False
506507 """Has the user interrupted a scroll to end?"""
508+ self ._extrema = Extrema ()
509+ """Optional minimum and maximum values for width and height."""
507510
508511 @property
509512 def is_mounted (self ) -> bool :
@@ -1528,12 +1531,16 @@ def _get_box_model(
15281531 # Container minus padding and border
15291532 content_container = container - gutter .totals
15301533
1534+ extrema = self ._extrema = self ._resolve_extrema (
1535+ container , viewport , width_fraction , height_fraction
1536+ )
1537+ min_width , max_width , min_height , max_height = extrema
1538+
15311539 if styles .width is None :
15321540 # No width specified, fill available space
15331541 content_width = Fraction (content_container .width - margin .width )
15341542 elif is_auto_width :
15351543 # When width is auto, we want enough space to always fit the content
1536-
15371544 content_width = Fraction (
15381545 self .get_content_width (content_container - margin .totals , viewport )
15391546 )
@@ -1555,28 +1562,17 @@ def _get_box_model(
15551562 if is_border_box :
15561563 content_width -= gutter .width
15571564
1558- if styles . min_width is not None :
1565+ if min_width is not None :
15591566 # Restrict to minimum width, if set
1560- min_width = styles .min_width .resolve (
1561- container - margin .totals , viewport , width_fraction
1562- )
1563- if is_border_box :
1564- min_width -= gutter .width
15651567 content_width = max (content_width , min_width , Fraction (0 ))
15661568
1567- if styles . max_width is not None and not (
1569+ if max_width is not None and not (
15681570 container .width == 0
15691571 and not styles .max_width .is_cells
15701572 and self ._parent is not None
15711573 and self ._parent .styles .is_auto_width
15721574 ):
15731575 # Restrict to maximum width, if set
1574- max_width = styles .max_width .resolve (
1575- container - margin .totals , viewport , width_fraction
1576- )
1577- if is_border_box :
1578- max_width -= gutter .width
1579-
15801576 content_width = min (content_width , max_width )
15811577
15821578 content_width = max (Fraction (0 ), content_width )
@@ -1614,28 +1610,16 @@ def _get_box_model(
16141610 if is_border_box :
16151611 content_height -= gutter .height
16161612
1617- if styles . min_height is not None :
1613+ if min_height is not None :
16181614 # Restrict to minimum height, if set
1619- min_height = styles .min_height .resolve (
1620- container - margin .totals , viewport , height_fraction
1621- )
1622- if is_border_box :
1623- min_height -= gutter .height
16241615 content_height = max (content_height , min_height , Fraction (0 ))
16251616
1626- if styles . max_height is not None and not (
1617+ if max_height is not None and not (
16271618 container .height == 0
16281619 and not styles .max_height .is_cells
16291620 and self ._parent is not None
16301621 and self ._parent .styles .is_auto_height
16311622 ):
1632- # Restrict maximum height, if set
1633- max_height = styles .max_height .resolve (
1634- container - margin .totals , viewport , height_fraction
1635- )
1636-
1637- if is_border_box :
1638- max_height -= gutter .height
16391623 content_height = min (content_height , max_height )
16401624
16411625 content_height = max (Fraction (0 ), content_height )
@@ -2202,6 +2186,63 @@ def is_on_screen(self) -> bool:
22022186 return False
22032187 return True
22042188
2189+ def _resolve_extrema (
2190+ self ,
2191+ container : Size ,
2192+ viewport : Size ,
2193+ width_fraction : Fraction ,
2194+ height_fraction : Fraction ,
2195+ ) -> Extrema :
2196+ """Resolve minimum and maximum values for width and height.
2197+
2198+ Args:
2199+ container: Size of outer widget.
2200+ viewport: Viewport size.
2201+ width_fraction: Size of 1fr width.
2202+ height_fraction: Size of 1fr height.
2203+
2204+ Returns:
2205+ Extrema object.
2206+ """
2207+
2208+ min_width : Fraction | None = None
2209+ max_width : Fraction | None = None
2210+ min_height : Fraction | None = None
2211+ max_height : Fraction | None = None
2212+
2213+ styles = self .styles
2214+ container -= styles .margin .totals
2215+ if styles .box_sizing == "border-box" :
2216+ gutter_width , gutter_height = styles .gutter .totals
2217+ else :
2218+ gutter_width = gutter_height = 0
2219+
2220+ if styles .min_width is not None :
2221+ min_width = (
2222+ styles .min_width .resolve (container , viewport , width_fraction )
2223+ - gutter_width
2224+ )
2225+
2226+ if styles .max_width is not None :
2227+ max_width = (
2228+ styles .max_width .resolve (container , viewport , width_fraction )
2229+ - gutter_width
2230+ )
2231+ if styles .min_height is not None :
2232+ min_height = (
2233+ styles .min_height .resolve (container , viewport , height_fraction )
2234+ - gutter_height
2235+ )
2236+
2237+ if styles .max_height is not None :
2238+ max_height = (
2239+ styles .max_height .resolve (container , viewport , height_fraction )
2240+ - gutter_height
2241+ )
2242+
2243+ extrema = Extrema (min_width , max_width , min_height , max_height )
2244+ return extrema
2245+
22052246 def animate (
22062247 self ,
22072248 attribute : str ,
0 commit comments