Expanding fr (#2221)

* forced fr to expand

* margin size

* remove comment

* missing snapshot

* snapshot tests

* changelog

* optimize

* snapshot fix

* snapshot update

* snapshot and fixes

* docstrings [skip ci]
This commit is contained in:
Will McGugan
2023-04-06 17:30:32 +01:00
committed by GitHub
parent c76667be5f
commit 44367a7422
14 changed files with 859 additions and 417 deletions

View File

@@ -15,9 +15,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Allowed border_title and border_subtitle to accept Text objects
- Added additional line around titles
- When a container is auto, relative dimensions in children stretch the container. https://github.com/Textualize/textual/pull/2221
### Fixed
- Fixed margin not being respected when width or height is "auto" https://github.com/Textualize/textual/issues/2220
- Fixed issue which prevent scroll_visible from working https://github.com/Textualize/textual/issues/2181
## [0.18.0] - 2023-04-04

View File

@@ -1,9 +1,9 @@
Screen {
align: center middle;
padding: 1;
}
#buttons {
margin-top: 1;
height: 3;
width: auto;
}
@@ -12,7 +12,7 @@ ContentSwitcher {
background: $panel;
border: round $primary;
width: 90%;
height: 80%;
height: 1fr;
}
DataTable {

View File

@@ -136,4 +136,5 @@ class Layout(ABC):
# Use a height of zero to ignore relative heights
arrangement = widget._arrange(Size(width, 0))
height = arrangement.total_region.bottom
return height

View File

@@ -33,7 +33,6 @@ def resolve(
Returns:
List of (<OFFSET>, <LENGTH>)
"""
resolved: list[tuple[Scalar, Fraction | None]] = [
(
(scalar, None)
@@ -84,6 +83,7 @@ def resolve_box_models(
widgets: list[Widget],
size: Size,
parent_size: Size,
margin: Size,
dimension: Literal["width", "height"] = "width",
) -> list[BoxModel]:
"""Resolve box models for a list of dimensions
@@ -93,14 +93,19 @@ def resolve_box_models(
widgets: Widgets in resolve.
size: Size of container.
parent_size: Size of parent.
margin: Total space occupied by margin
dimensions: Which dimension to resolve.
Returns:
List of resolved box models.
"""
margin_width, margin_height = margin
fraction_width = Fraction(max(0, size.width - margin_width))
fraction_height = Fraction(max(0, size.height - margin_height))
margin_size = size - margin
fraction_width = Fraction(size.width)
fraction_height = Fraction(size.height)
box_models: list[BoxModel | None] = [
(
None
@@ -116,12 +121,12 @@ def resolve_box_models(
total_remaining = sum(
box_model.width for box_model in box_models if box_model is not None
)
remaining_space = max(0, size.width - total_remaining)
remaining_space = max(0, size.width - total_remaining - margin_width)
else:
total_remaining = sum(
box_model.height for box_model in box_models if box_model is not None
)
remaining_space = max(0, size.height - total_remaining)
remaining_space = max(0, size.height - total_remaining - margin_height)
fraction_unit = Fraction(
remaining_space,
@@ -136,9 +141,9 @@ def resolve_box_models(
)
if dimension == "width":
width_fraction = fraction_unit
height_fraction = Fraction(size.height)
height_fraction = Fraction(margin_size.height)
else:
width_fraction = Fraction(size.width)
width_fraction = Fraction(margin_size.width)
height_fraction = fraction_unit
box_models = [

View File

@@ -1,10 +1,9 @@
from __future__ import annotations
from fractions import Fraction
from typing import Callable, NamedTuple
from typing import NamedTuple
from .css.styles import StylesBase
from .geometry import Size, Spacing
from .geometry import Spacing
class BoxModel(NamedTuple):
@@ -14,119 +13,3 @@ class BoxModel(NamedTuple):
width: Fraction
height: Fraction
margin: Spacing # Additional margin
def get_box_model(
styles: StylesBase,
container: Size,
viewport: Size,
width_fraction: Fraction,
height_fraction: Fraction,
get_content_width: Callable[[Size, Size], int],
get_content_height: Callable[[Size, Size, int], int],
) -> BoxModel:
"""Resolve the box model for this Styles.
Args:
styles: Styles object.
container: The size of the widget container.
viewport: The viewport size.
width_fraction: A fraction used for 1 `fr` unit on the width dimension.
height_fraction: A fraction used for 1 `fr` unit on the height dimension.
get_content_width: A callable which accepts container size and parent size and returns a width.
get_content_height: A callable which accepts container size and parent size and returns a height.
Returns:
A tuple with the size of the content area and margin.
"""
_content_width, _content_height = container
content_width = Fraction(_content_width)
content_height = Fraction(_content_height)
is_border_box = styles.box_sizing == "border-box"
gutter = styles.gutter
margin = styles.margin
is_auto_width = styles.width and styles.width.is_auto
is_auto_height = styles.height and styles.height.is_auto
# Container minus padding and border
content_container = container - gutter.totals
# The container including the content
sizing_container = content_container if is_border_box else container
if styles.width is None:
# No width specified, fill available space
content_width = Fraction(content_container.width - margin.width)
elif is_auto_width:
# When width is auto, we want enough space to always fit the content
content_width = Fraction(
get_content_width(content_container - styles.margin.totals, viewport)
)
if styles.scrollbar_gutter == "stable" and styles.overflow_x == "auto":
content_width += styles.scrollbar_size_vertical
else:
# An explicit width
styles_width = styles.width
content_width = styles_width.resolve(
sizing_container - styles.margin.totals, viewport, width_fraction
)
if is_border_box and styles_width.excludes_border:
content_width -= gutter.width
if styles.min_width is not None:
# Restrict to minimum width, if set
min_width = styles.min_width.resolve(
content_container, viewport, width_fraction
)
content_width = max(content_width, min_width)
if styles.max_width is not None:
# Restrict to maximum width, if set
max_width = styles.max_width.resolve(
content_container, viewport, width_fraction
)
if is_border_box:
max_width -= gutter.width
content_width = min(content_width, max_width)
content_width = max(Fraction(0), content_width)
if styles.height is None:
# No height specified, fill the available space
content_height = Fraction(content_container.height - margin.height)
elif is_auto_height:
# Calculate dimensions based on content
content_height = Fraction(
get_content_height(content_container, viewport, int(content_width))
)
if styles.scrollbar_gutter == "stable" and styles.overflow_y == "auto":
content_height += styles.scrollbar_size_horizontal
else:
styles_height = styles.height
# Explicit height set
content_height = styles_height.resolve(
sizing_container - styles.margin.totals, viewport, height_fraction
)
if is_border_box and styles_height.excludes_border:
content_height -= gutter.height
if styles.min_height is not None:
# Restrict to minimum height, if set
min_height = styles.min_height.resolve(
content_container, viewport, height_fraction
)
content_height = max(content_height, min_height)
if styles.max_height is not None:
# Restrict maximum height, if set
max_height = styles.max_height.resolve(
content_container, viewport, height_fraction
)
content_height = min(content_height, max_height)
content_height = max(Fraction(0), content_height)
model = BoxModel(
content_width + gutter.width, content_height + gutter.height, margin
)
return model

View File

@@ -384,6 +384,21 @@ class StylesBase(ABC):
has_rule("height") and self.height.is_auto # type: ignore
)
@property
def is_relative_width(self) -> bool:
"""Does the node have a relative width?"""
width = self.width
return width is not None and width.unit in (
Unit.FRACTION,
Unit.PERCENT,
)
@property
def is_relative_height(self) -> bool:
"""Does the node have a relative width?"""
height = self.height
return height is not None and height.unit in (Unit.FRACTION, Unit.PERCENT)
@abstractmethod
def has_rule(self, rule: str) -> bool:
"""Check if a rule is set on this Styles object.

View File

@@ -23,11 +23,28 @@ class HorizontalLayout(Layout):
x = max_height = Fraction(0)
parent_size = parent.outer_size
child_styles = [child.styles for child in children]
box_margins = [styles.margin for styles in child_styles]
if box_margins:
resolve_margin = Size(
(
sum(
max(margin1.right, margin2.left)
for margin1, margin2 in zip(box_margins, box_margins[1:])
)
+ (box_margins[0].left + box_margins[-1].right)
),
max(margin.height for margin in box_margins),
)
else:
resolve_margin = Size(0, 0)
box_models = resolve_box_models(
[child.styles.width for child in children],
[styles.width for styles in child_styles],
children,
size,
parent_size,
resolve_margin,
dimension="width",
)

View File

@@ -23,11 +23,28 @@ class VerticalLayout(Layout):
add_placement = placements.append
parent_size = parent.outer_size
child_styles = [child.styles for child in children]
box_margins = [styles.margin for styles in child_styles]
if box_margins:
resolve_margin = Size(
max([margin.width for margin in box_margins]),
(
sum(
max(margin1.bottom, margin2.top)
for margin1, margin2 in zip(box_margins, box_margins[1:])
)
+ (box_margins[0].top + box_margins[-1].bottom)
),
)
else:
resolve_margin = Size(0, 0)
box_models = resolve_box_models(
[child.styles.height for child in children],
[styles.height for styles in child_styles],
children,
size,
parent_size,
resolve_margin,
dimension="height",
)

View File

@@ -50,7 +50,7 @@ from ._styles_cache import StylesCache
from .actions import SkipAction
from .await_remove import AwaitRemove
from .binding import Binding
from .box_model import BoxModel, get_box_model
from .box_model import BoxModel
from .css.query import NoMatches, WrongType
from .css.scalar import ScalarOffset
from .dom import DOMNode, NoScreen
@@ -316,6 +316,7 @@ class Widget(DOMNode):
self._rich_style_cache: dict[str, tuple[Style, Style]] = {}
self._stabilize_scrollbar: tuple[Size, str, str] | None = None
"""Used to prevent scrollbar logic getting stuck in an infinite loop."""
self._lock = Lock()
super().__init__(
@@ -889,16 +890,110 @@ class Widget(DOMNode):
Returns:
The size and margin for this widget.
"""
box_model = get_box_model(
self.styles,
container,
viewport,
width_fraction,
height_fraction,
self.get_content_width,
self.get_content_height,
styles = self.styles
_content_width, _content_height = container
content_width = Fraction(_content_width)
content_height = Fraction(_content_height)
is_border_box = styles.box_sizing == "border-box"
gutter = styles.gutter
margin = styles.margin
is_auto_width = styles.width and styles.width.is_auto
is_auto_height = styles.height and styles.height.is_auto
# Container minus padding and border
content_container = container - gutter.totals
# The container including the content
sizing_container = content_container if is_border_box else container
if styles.width is None:
# No width specified, fill available space
content_width = Fraction(content_container.width - margin.width)
elif is_auto_width:
# When width is auto, we want enough space to always fit the content
content_width = Fraction(
self.get_content_width(
content_container - styles.margin.totals, viewport
)
return box_model
)
if styles.scrollbar_gutter == "stable" and styles.overflow_x == "auto":
content_width += styles.scrollbar_size_vertical
if (
content_width < content_container.width
and self._has_relative_children_width
):
content_width = Fraction(content_container.width)
else:
# An explicit width
styles_width = styles.width
content_width = styles_width.resolve(
sizing_container - styles.margin.totals, viewport, width_fraction
)
if is_border_box and styles_width.excludes_border:
content_width -= gutter.width
if styles.min_width is not None:
# Restrict to minimum width, if set
min_width = styles.min_width.resolve(
content_container, viewport, width_fraction
)
content_width = max(content_width, min_width)
if styles.max_width is not None:
# Restrict to maximum width, if set
max_width = styles.max_width.resolve(
content_container, viewport, width_fraction
)
if is_border_box:
max_width -= gutter.width
content_width = min(content_width, max_width)
content_width = max(Fraction(0), content_width)
if styles.height is None:
# No height specified, fill the available space
content_height = Fraction(content_container.height - margin.height)
elif is_auto_height:
# Calculate dimensions based on content
content_height = Fraction(
self.get_content_height(content_container, viewport, int(content_width))
)
if styles.scrollbar_gutter == "stable" and styles.overflow_y == "auto":
content_height += styles.scrollbar_size_horizontal
if (
content_height < content_container.height
and self._has_relative_children_height
):
content_height = Fraction(content_container.height)
else:
styles_height = styles.height
# Explicit height set
content_height = styles_height.resolve(
sizing_container - styles.margin.totals, viewport, height_fraction
)
if is_border_box and styles_height.excludes_border:
content_height -= gutter.height
if styles.min_height is not None:
# Restrict to minimum height, if set
min_height = styles.min_height.resolve(
content_container, viewport, height_fraction
)
content_height = max(content_height, min_height)
if styles.max_height is not None:
# Restrict maximum height, if set
max_height = styles.max_height.resolve(
content_container, viewport, height_fraction
)
content_height = min(content_height, max_height)
content_height = max(Fraction(0), content_height)
model = BoxModel(
content_width + gutter.width, content_height + gutter.height, margin
)
return model
def get_content_width(self, container: Size, viewport: Size) -> int:
"""Called by textual to get the width of the content area. May be overridden in a subclass.
@@ -1366,6 +1461,20 @@ class Widget(DOMNode):
"""
return active_app.get().console
@property
def _has_relative_children_width(self) -> bool:
"""Do any children have a relative width?"""
if not self.is_container:
return False
return any(widget.styles.is_relative_width for widget in self.children)
@property
def _has_relative_children_height(self) -> bool:
"""Do any children have a relative height?"""
if not self.is_container:
return False
return any(widget.styles.is_relative_height for widget in self.children)
def animate(
self,
attribute: str,

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
from textual.app import App, ComposeResult
from textual.widget import Widget
from textual.widgets import Label
class FRApp(App):
CSS = """
Screen {
align: center middle;
border: solid cyan;
}
#container {
width: 30;
height: auto;
border: solid green;
overflow-y: auto;
}
#child {
height: 1fr;
border: solid red;
}
#bottom {
margin: 1 2;
background: $primary;
}
"""
def compose(self) -> ComposeResult:
with Widget(id="container"):
yield Label("Hello one line", id="top")
yield Widget(id="child")
yield Label("Two\nLines with 1x2 margin", id="bottom")
if __name__ == "__main__":
app = FRApp()
app.run()

View File

@@ -0,0 +1,35 @@
from textual.app import App, ComposeResult
from textual.widgets import Label
from textual.containers import Container
# Test fr dimensions and margins work in an auto container
# https://github.com/Textualize/textual/issues/2220
class TestApp(App):
CSS = """
Container {
background: green 20%;
border: heavy green;
width: auto;
height: auto;
overflow: hidden;
}
Label {
background: green 20%;
width: 1fr;
height: 1fr;
margin: 2 2;
}
"""
def compose(self) -> ComposeResult:
with Container():
yield Label("Hello")
yield Label("World")
yield Label("!!")
if __name__ == "__main__":
app = TestApp()
app.run()

View File

@@ -388,6 +388,16 @@ def test_dock_scroll(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "dock_scroll.py", terminal_size=(80, 25))
def test_auto_fr(snap_compare):
# https://github.com/Textualize/textual/issues/2220
assert snap_compare(SNAPSHOT_APPS_DIR / "auto_fr.py", terminal_size=(80, 25))
def test_fr_margins(snap_compare):
# https://github.com/Textualize/textual/issues/2220
assert snap_compare(SNAPSHOT_APPS_DIR / "fr_margins.py", terminal_size=(80, 25))
def test_scroll_visible(snap_compare):
# https://github.com/Textualize/textual/issues/2181
assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_visible.py", press=["t"])

View File

@@ -2,206 +2,190 @@ from __future__ import annotations
from fractions import Fraction
from textual.box_model import BoxModel, get_box_model
from textual.box_model import BoxModel
from textual.css.styles import Styles
from textual.geometry import Size, Spacing
from textual.widget import Widget
def test_content_box():
styles = Styles()
styles.width = 10
styles.height = 8
styles.padding = 1
styles.border = ("solid", "red")
one = Fraction(1)
class TestWidget(Widget):
def get_content_width(self, container: Size, parent: Size) -> int:
assert False, "must not be called"
def get_content_height(self, container: Size, parent: Size) -> int:
assert False, "must not be called"
widget = TestWidget()
# border-box is default
assert styles.box_sizing == "border-box"
assert widget.styles.box_sizing == "border-box"
def get_auto_width(container: Size, parent: Size) -> int:
assert False, "must not be called"
widget.styles.width = 10
widget.styles.height = 8
widget.styles.padding = 1
widget.styles.border = ("solid", "red")
def get_auto_height(container: Size, parent: Size) -> int:
assert False, "must not be called"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
box_model = widget._get_box_model(
Size(60, 20),
Size(80, 24),
one,
one,
)
# Size should be inclusive of padding / border
assert box_model == BoxModel(Fraction(10), Fraction(8), Spacing(0, 0, 0, 0))
# Switch to content-box
styles.box_sizing = "content-box"
widget.styles.box_sizing = "content-box"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
# width and height have added padding / border to accommodate content
assert box_model == BoxModel(Fraction(14), Fraction(12), Spacing(0, 0, 0, 0))
def test_width():
"""Test width settings."""
styles = Styles()
one = Fraction(1)
def get_auto_width(container: Size, parent: Size) -> int:
class TestWidget(Widget):
def get_content_width(self, container: Size, parent: Size) -> int:
return 10
def get_auto_height(container: Size, parent: Size, width: int) -> int:
def get_content_height(self, container: Size, parent: Size, width: int) -> int:
return 10
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
widget = TestWidget()
styles = widget.styles
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(60), Fraction(20), Spacing(0, 0, 0, 0))
# Add a margin and check that it is reported
styles.margin = (1, 2, 3, 4)
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
# Set width to auto-detect
styles.width = "auto"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
# Setting width to auto should call get_auto_width
assert box_model == BoxModel(Fraction(10), Fraction(16), Spacing(1, 2, 3, 4))
# Set width to 100 vw which should make it the width of the parent
styles.width = "100vw"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(80), Fraction(16), Spacing(1, 2, 3, 4))
# Set the width to 100% should make it fill the container size
styles.width = "100%"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
styles.width = "100vw"
styles.max_width = "50%"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(30), Fraction(16), Spacing(1, 2, 3, 4))
def test_height():
"""Test height settings."""
styles = Styles()
one = Fraction(1)
def get_auto_width(container: Size, parent: Size) -> int:
class TestWidget(Widget):
def get_content_width(self, container: Size, parent: Size) -> int:
return 10
def get_auto_height(container: Size, parent: Size, width: int) -> int:
def get_content_height(self, container: Size, parent: Size, width: int) -> int:
return 10
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
widget = TestWidget()
styles = widget.styles
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(60), Fraction(20), Spacing(0, 0, 0, 0))
# Add a margin and check that it is reported
styles.margin = (1, 2, 3, 4)
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
# Set height to 100 vw which should make it the height of the parent
styles.height = "100vh"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(54), Fraction(24), Spacing(1, 2, 3, 4))
# Set the height to 100% should make it fill the container size
styles.height = "100%"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
styles.height = "auto"
styles.margin = 2
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
print(box_model)
assert box_model == BoxModel(Fraction(56), Fraction(10), Spacing(2, 2, 2, 2))
styles.margin = 1, 2, 3, 4
styles.height = "100vh"
styles.max_height = "50%"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(54), Fraction(10), Spacing(1, 2, 3, 4))
# Set height to auto and set content height to 0 to check if box collapses.
styles.height = "auto"
box_model = get_box_model(
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, lambda *_: 0
)
assert box_model == BoxModel(Fraction(54), Fraction(0), Spacing(1, 2, 3, 4))
def test_max():
"""Check that max_width and max_height are respected."""
styles = Styles()
one = Fraction(1)
class TestWidget(Widget):
def get_content_width(self, container: Size, parent: Size) -> int:
assert False, "must not be called"
def get_content_height(self, container: Size, parent: Size, width: int) -> int:
assert False, "must not be called"
widget = TestWidget()
styles = widget.styles
styles.width = 100
styles.height = 80
styles.max_width = 40
styles.max_height = 30
one = Fraction(1)
def get_auto_width(container: Size, parent: Size) -> int:
assert False, "must not be called"
def get_auto_height(container: Size, parent: Size) -> int:
assert False, "must not be called"
box_model = get_box_model(
styles, Size(40, 30), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(40, 30), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0))
def test_min():
"""Check that min_width and min_height are respected."""
styles = Styles()
one = Fraction(1)
class TestWidget(Widget):
def get_content_width(self, container: Size, parent: Size) -> int:
assert False, "must not be called"
def get_content_height(self, container: Size, parent: Size, width: int) -> int:
assert False, "must not be called"
widget = TestWidget()
styles = widget.styles
styles.width = 10
styles.height = 5
styles.min_width = 40
styles.min_height = 30
one = Fraction(1)
def get_auto_width(container: Size, parent: Size) -> int:
assert False, "must not be called"
def get_auto_height(container: Size, parent: Size) -> int:
assert False, "must not be called"
box_model = get_box_model(
styles, Size(40, 30), Size(80, 24), one, one, get_auto_width, get_auto_height
)
box_model = widget._get_box_model(Size(40, 30), Size(80, 24), one, one)
assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0))