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 - Allowed border_title and border_subtitle to accept Text objects
- Added additional line around titles - 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
- 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 - Fixed issue which prevent scroll_visible from working https://github.com/Textualize/textual/issues/2181
## [0.18.0] - 2023-04-04 ## [0.18.0] - 2023-04-04

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
from __future__ import annotations from __future__ import annotations
from fractions import Fraction from fractions import Fraction
from typing import Callable, NamedTuple from typing import NamedTuple
from .css.styles import StylesBase from .geometry import Spacing
from .geometry import Size, Spacing
class BoxModel(NamedTuple): class BoxModel(NamedTuple):
@@ -14,119 +13,3 @@ class BoxModel(NamedTuple):
width: Fraction width: Fraction
height: Fraction height: Fraction
margin: Spacing # Additional margin 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 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 @abstractmethod
def has_rule(self, rule: str) -> bool: def has_rule(self, rule: str) -> bool:
"""Check if a rule is set on this Styles object. """Check if a rule is set on this Styles object.

View File

@@ -23,11 +23,28 @@ class HorizontalLayout(Layout):
x = max_height = Fraction(0) x = max_height = Fraction(0)
parent_size = parent.outer_size 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( box_models = resolve_box_models(
[child.styles.width for child in children], [styles.width for styles in child_styles],
children, children,
size, size,
parent_size, parent_size,
resolve_margin,
dimension="width", dimension="width",
) )

View File

@@ -23,11 +23,28 @@ class VerticalLayout(Layout):
add_placement = placements.append add_placement = placements.append
parent_size = parent.outer_size 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( box_models = resolve_box_models(
[child.styles.height for child in children], [styles.height for styles in child_styles],
children, children,
size, size,
parent_size, parent_size,
resolve_margin,
dimension="height", dimension="height",
) )

View File

@@ -50,7 +50,7 @@ from ._styles_cache import StylesCache
from .actions import SkipAction from .actions import SkipAction
from .await_remove import AwaitRemove from .await_remove import AwaitRemove
from .binding import Binding 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.query import NoMatches, WrongType
from .css.scalar import ScalarOffset from .css.scalar import ScalarOffset
from .dom import DOMNode, NoScreen from .dom import DOMNode, NoScreen
@@ -316,6 +316,7 @@ class Widget(DOMNode):
self._rich_style_cache: dict[str, tuple[Style, Style]] = {} self._rich_style_cache: dict[str, tuple[Style, Style]] = {}
self._stabilize_scrollbar: tuple[Size, str, str] | None = None self._stabilize_scrollbar: tuple[Size, str, str] | None = None
"""Used to prevent scrollbar logic getting stuck in an infinite loop.""" """Used to prevent scrollbar logic getting stuck in an infinite loop."""
self._lock = Lock() self._lock = Lock()
super().__init__( super().__init__(
@@ -889,16 +890,110 @@ class Widget(DOMNode):
Returns: Returns:
The size and margin for this widget. The size and margin for this widget.
""" """
box_model = get_box_model( styles = self.styles
self.styles, _content_width, _content_height = container
container, content_width = Fraction(_content_width)
viewport, content_height = Fraction(_content_height)
width_fraction, is_border_box = styles.box_sizing == "border-box"
height_fraction, gutter = styles.gutter
self.get_content_width, margin = styles.margin
self.get_content_height,
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
)
)
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 box_model return model
def get_content_width(self, container: Size, viewport: Size) -> int: 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. """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 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( def animate(
self, self,
attribute: str, 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)) 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): def test_scroll_visible(snap_compare):
# https://github.com/Textualize/textual/issues/2181 # https://github.com/Textualize/textual/issues/2181
assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_visible.py", press=["t"]) 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 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.css.styles import Styles
from textual.geometry import Size, Spacing from textual.geometry import Size, Spacing
from textual.widget import Widget
def test_content_box(): def test_content_box():
styles = Styles()
styles.width = 10
styles.height = 8
styles.padding = 1
styles.border = ("solid", "red")
one = Fraction(1) 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 # 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: widget.styles.width = 10
assert False, "must not be called" widget.styles.height = 8
widget.styles.padding = 1
widget.styles.border = ("solid", "red")
def get_auto_height(container: Size, parent: Size) -> int: box_model = widget._get_box_model(
assert False, "must not be called" Size(60, 20),
Size(80, 24),
box_model = get_box_model( one,
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height one,
) )
# Size should be inclusive of padding / border # Size should be inclusive of padding / border
assert box_model == BoxModel(Fraction(10), Fraction(8), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(10), Fraction(8), Spacing(0, 0, 0, 0))
# Switch to content-box # Switch to content-box
styles.box_sizing = "content-box" widget.styles.box_sizing = "content-box"
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
# width and height have added padding / border to accommodate content # width and height have added padding / border to accommodate content
assert box_model == BoxModel(Fraction(14), Fraction(12), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(14), Fraction(12), Spacing(0, 0, 0, 0))
def test_width(): def test_width():
"""Test width settings.""" """Test width settings."""
styles = Styles()
one = Fraction(1) one = Fraction(1)
def get_auto_width(container: Size, parent: Size) -> int: class TestWidget(Widget):
return 10 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 return 10
box_model = get_box_model( widget = TestWidget()
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height 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)) assert box_model == BoxModel(Fraction(60), Fraction(20), Spacing(0, 0, 0, 0))
# Add a margin and check that it is reported # Add a margin and check that it is reported
styles.margin = (1, 2, 3, 4) styles.margin = (1, 2, 3, 4)
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
# Set width to auto-detect # Set width to auto-detect
styles.width = "auto" styles.width = "auto"
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
# Setting width to auto should call get_auto_width # Setting width to auto should call get_auto_width
assert box_model == BoxModel(Fraction(10), Fraction(16), Spacing(1, 2, 3, 4)) 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 # Set width to 100 vw which should make it the width of the parent
styles.width = "100vw" styles.width = "100vw"
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
assert box_model == BoxModel(Fraction(80), Fraction(16), Spacing(1, 2, 3, 4)) 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 # Set the width to 100% should make it fill the container size
styles.width = "100%" styles.width = "100%"
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
styles.width = "100vw" styles.width = "100vw"
styles.max_width = "50%" styles.max_width = "50%"
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
assert box_model == BoxModel(Fraction(30), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(30), Fraction(16), Spacing(1, 2, 3, 4))
def test_height(): def test_height():
"""Test height settings.""" """Test height settings."""
styles = Styles()
one = Fraction(1) one = Fraction(1)
def get_auto_width(container: Size, parent: Size) -> int: class TestWidget(Widget):
return 10 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 return 10
box_model = get_box_model( widget = TestWidget()
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height 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)) assert box_model == BoxModel(Fraction(60), Fraction(20), Spacing(0, 0, 0, 0))
# Add a margin and check that it is reported # Add a margin and check that it is reported
styles.margin = (1, 2, 3, 4) styles.margin = (1, 2, 3, 4)
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4)) 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 # Set height to 100 vw which should make it the height of the parent
styles.height = "100vh" styles.height = "100vh"
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
assert box_model == BoxModel(Fraction(54), Fraction(24), Spacing(1, 2, 3, 4)) 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 # Set the height to 100% should make it fill the container size
styles.height = "100%" styles.height = "100%"
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(16), Spacing(1, 2, 3, 4))
styles.height = "auto" styles.height = "auto"
styles.margin = 2 styles.margin = 2
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height print(box_model)
)
assert box_model == BoxModel(Fraction(56), Fraction(10), Spacing(2, 2, 2, 2)) assert box_model == BoxModel(Fraction(56), Fraction(10), Spacing(2, 2, 2, 2))
styles.margin = 1, 2, 3, 4 styles.margin = 1, 2, 3, 4
styles.height = "100vh" styles.height = "100vh"
styles.max_height = "50%" styles.max_height = "50%"
box_model = get_box_model( box_model = widget._get_box_model(Size(60, 20), Size(80, 24), one, one)
styles, Size(60, 20), Size(80, 24), one, one, get_auto_width, get_auto_height
)
assert box_model == BoxModel(Fraction(54), Fraction(10), Spacing(1, 2, 3, 4)) 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(): def test_max():
"""Check that max_width and max_height are respected.""" """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.width = 100
styles.height = 80 styles.height = 80
styles.max_width = 40 styles.max_width = 40
styles.max_height = 30 styles.max_height = 30
one = Fraction(1)
def get_auto_width(container: Size, parent: Size) -> int: box_model = widget._get_box_model(Size(40, 30), Size(80, 24), one, one)
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
)
assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0))
def test_min(): def test_min():
"""Check that min_width and min_height are respected.""" """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.width = 10
styles.height = 5 styles.height = 5
styles.min_width = 40 styles.min_width = 40
styles.min_height = 30 styles.min_height = 30
one = Fraction(1)
def get_auto_width(container: Size, parent: Size) -> int: box_model = widget._get_box_model(Size(40, 30), Size(80, 24), one, one)
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
)
assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0))