Skip to content

Commit 44367a7

Browse files
authored
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]
1 parent c76667b commit 44367a7

File tree

14 files changed

+859
-417
lines changed

14 files changed

+859
-417
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1515

1616
- Allowed border_title and border_subtitle to accept Text objects
1717
- Added additional line around titles
18+
- When a container is auto, relative dimensions in children stretch the container. https://github.com/Textualize/textual/pull/2221
1819

1920
### Fixed
2021

22+
- Fixed margin not being respected when width or height is "auto" https://github.com/Textualize/textual/issues/2220
2123
- Fixed issue which prevent scroll_visible from working https://github.com/Textualize/textual/issues/2181
2224

2325
## [0.18.0] - 2023-04-04

docs/examples/widgets/content_switcher.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Screen {
22
align: center middle;
3+
padding: 1;
34
}
45

56
#buttons {
6-
margin-top: 1;
77
height: 3;
88
width: auto;
99
}
@@ -12,7 +12,7 @@ ContentSwitcher {
1212
background: $panel;
1313
border: round $primary;
1414
width: 90%;
15-
height: 80%;
15+
height: 1fr;
1616
}
1717

1818
DataTable {

src/textual/_layout.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,5 @@ def get_content_height(
136136
# Use a height of zero to ignore relative heights
137137
arrangement = widget._arrange(Size(width, 0))
138138
height = arrangement.total_region.bottom
139+
139140
return height

src/textual/_resolve.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ def resolve(
3333
Returns:
3434
List of (<OFFSET>, <LENGTH>)
3535
"""
36-
3736
resolved: list[tuple[Scalar, Fraction | None]] = [
3837
(
3938
(scalar, None)
@@ -84,6 +83,7 @@ def resolve_box_models(
8483
widgets: list[Widget],
8584
size: Size,
8685
parent_size: Size,
86+
margin: Size,
8787
dimension: Literal["width", "height"] = "width",
8888
) -> list[BoxModel]:
8989
"""Resolve box models for a list of dimensions
@@ -93,14 +93,19 @@ def resolve_box_models(
9393
widgets: Widgets in resolve.
9494
size: Size of container.
9595
parent_size: Size of parent.
96+
margin: Total space occupied by margin
9697
dimensions: Which dimension to resolve.
9798
9899
Returns:
99100
List of resolved box models.
100101
"""
102+
margin_width, margin_height = margin
103+
104+
fraction_width = Fraction(max(0, size.width - margin_width))
105+
fraction_height = Fraction(max(0, size.height - margin_height))
106+
107+
margin_size = size - margin
101108

102-
fraction_width = Fraction(size.width)
103-
fraction_height = Fraction(size.height)
104109
box_models: list[BoxModel | None] = [
105110
(
106111
None
@@ -116,12 +121,12 @@ def resolve_box_models(
116121
total_remaining = sum(
117122
box_model.width for box_model in box_models if box_model is not None
118123
)
119-
remaining_space = max(0, size.width - total_remaining)
124+
remaining_space = max(0, size.width - total_remaining - margin_width)
120125
else:
121126
total_remaining = sum(
122127
box_model.height for box_model in box_models if box_model is not None
123128
)
124-
remaining_space = max(0, size.height - total_remaining)
129+
remaining_space = max(0, size.height - total_remaining - margin_height)
125130

126131
fraction_unit = Fraction(
127132
remaining_space,
@@ -136,9 +141,9 @@ def resolve_box_models(
136141
)
137142
if dimension == "width":
138143
width_fraction = fraction_unit
139-
height_fraction = Fraction(size.height)
144+
height_fraction = Fraction(margin_size.height)
140145
else:
141-
width_fraction = Fraction(size.width)
146+
width_fraction = Fraction(margin_size.width)
142147
height_fraction = fraction_unit
143148

144149
box_models = [

src/textual/box_model.py

Lines changed: 2 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from __future__ import annotations
22

33
from fractions import Fraction
4-
from typing import Callable, NamedTuple
4+
from typing import NamedTuple
55

6-
from .css.styles import StylesBase
7-
from .geometry import Size, Spacing
6+
from .geometry import Spacing
87

98

109
class BoxModel(NamedTuple):
@@ -14,119 +13,3 @@ class BoxModel(NamedTuple):
1413
width: Fraction
1514
height: Fraction
1615
margin: Spacing # Additional margin
17-
18-
19-
def get_box_model(
20-
styles: StylesBase,
21-
container: Size,
22-
viewport: Size,
23-
width_fraction: Fraction,
24-
height_fraction: Fraction,
25-
get_content_width: Callable[[Size, Size], int],
26-
get_content_height: Callable[[Size, Size, int], int],
27-
) -> BoxModel:
28-
"""Resolve the box model for this Styles.
29-
30-
Args:
31-
styles: Styles object.
32-
container: The size of the widget container.
33-
viewport: The viewport size.
34-
width_fraction: A fraction used for 1 `fr` unit on the width dimension.
35-
height_fraction: A fraction used for 1 `fr` unit on the height dimension.
36-
get_content_width: A callable which accepts container size and parent size and returns a width.
37-
get_content_height: A callable which accepts container size and parent size and returns a height.
38-
39-
Returns:
40-
A tuple with the size of the content area and margin.
41-
"""
42-
_content_width, _content_height = container
43-
content_width = Fraction(_content_width)
44-
content_height = Fraction(_content_height)
45-
is_border_box = styles.box_sizing == "border-box"
46-
gutter = styles.gutter
47-
margin = styles.margin
48-
49-
is_auto_width = styles.width and styles.width.is_auto
50-
is_auto_height = styles.height and styles.height.is_auto
51-
52-
# Container minus padding and border
53-
content_container = container - gutter.totals
54-
# The container including the content
55-
sizing_container = content_container if is_border_box else container
56-
57-
if styles.width is None:
58-
# No width specified, fill available space
59-
content_width = Fraction(content_container.width - margin.width)
60-
elif is_auto_width:
61-
# When width is auto, we want enough space to always fit the content
62-
content_width = Fraction(
63-
get_content_width(content_container - styles.margin.totals, viewport)
64-
)
65-
if styles.scrollbar_gutter == "stable" and styles.overflow_x == "auto":
66-
content_width += styles.scrollbar_size_vertical
67-
else:
68-
# An explicit width
69-
styles_width = styles.width
70-
content_width = styles_width.resolve(
71-
sizing_container - styles.margin.totals, viewport, width_fraction
72-
)
73-
if is_border_box and styles_width.excludes_border:
74-
content_width -= gutter.width
75-
76-
if styles.min_width is not None:
77-
# Restrict to minimum width, if set
78-
min_width = styles.min_width.resolve(
79-
content_container, viewport, width_fraction
80-
)
81-
content_width = max(content_width, min_width)
82-
83-
if styles.max_width is not None:
84-
# Restrict to maximum width, if set
85-
max_width = styles.max_width.resolve(
86-
content_container, viewport, width_fraction
87-
)
88-
if is_border_box:
89-
max_width -= gutter.width
90-
content_width = min(content_width, max_width)
91-
92-
content_width = max(Fraction(0), content_width)
93-
94-
if styles.height is None:
95-
# No height specified, fill the available space
96-
content_height = Fraction(content_container.height - margin.height)
97-
elif is_auto_height:
98-
# Calculate dimensions based on content
99-
content_height = Fraction(
100-
get_content_height(content_container, viewport, int(content_width))
101-
)
102-
if styles.scrollbar_gutter == "stable" and styles.overflow_y == "auto":
103-
content_height += styles.scrollbar_size_horizontal
104-
else:
105-
styles_height = styles.height
106-
# Explicit height set
107-
content_height = styles_height.resolve(
108-
sizing_container - styles.margin.totals, viewport, height_fraction
109-
)
110-
if is_border_box and styles_height.excludes_border:
111-
content_height -= gutter.height
112-
113-
if styles.min_height is not None:
114-
# Restrict to minimum height, if set
115-
min_height = styles.min_height.resolve(
116-
content_container, viewport, height_fraction
117-
)
118-
content_height = max(content_height, min_height)
119-
120-
if styles.max_height is not None:
121-
# Restrict maximum height, if set
122-
max_height = styles.max_height.resolve(
123-
content_container, viewport, height_fraction
124-
)
125-
content_height = min(content_height, max_height)
126-
127-
content_height = max(Fraction(0), content_height)
128-
129-
model = BoxModel(
130-
content_width + gutter.width, content_height + gutter.height, margin
131-
)
132-
return model

src/textual/css/styles.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,21 @@ def auto_dimensions(self) -> bool:
384384
has_rule("height") and self.height.is_auto # type: ignore
385385
)
386386

387+
@property
388+
def is_relative_width(self) -> bool:
389+
"""Does the node have a relative width?"""
390+
width = self.width
391+
return width is not None and width.unit in (
392+
Unit.FRACTION,
393+
Unit.PERCENT,
394+
)
395+
396+
@property
397+
def is_relative_height(self) -> bool:
398+
"""Does the node have a relative width?"""
399+
height = self.height
400+
return height is not None and height.unit in (Unit.FRACTION, Unit.PERCENT)
401+
387402
@abstractmethod
388403
def has_rule(self, rule: str) -> bool:
389404
"""Check if a rule is set on this Styles object.

src/textual/layouts/horizontal.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,28 @@ def arrange(
2323
x = max_height = Fraction(0)
2424
parent_size = parent.outer_size
2525

26+
child_styles = [child.styles for child in children]
27+
box_margins = [styles.margin for styles in child_styles]
28+
if box_margins:
29+
resolve_margin = Size(
30+
(
31+
sum(
32+
max(margin1.right, margin2.left)
33+
for margin1, margin2 in zip(box_margins, box_margins[1:])
34+
)
35+
+ (box_margins[0].left + box_margins[-1].right)
36+
),
37+
max(margin.height for margin in box_margins),
38+
)
39+
else:
40+
resolve_margin = Size(0, 0)
41+
2642
box_models = resolve_box_models(
27-
[child.styles.width for child in children],
43+
[styles.width for styles in child_styles],
2844
children,
2945
size,
3046
parent_size,
47+
resolve_margin,
3148
dimension="width",
3249
)
3350

src/textual/layouts/vertical.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,28 @@ def arrange(
2323
add_placement = placements.append
2424
parent_size = parent.outer_size
2525

26+
child_styles = [child.styles for child in children]
27+
box_margins = [styles.margin for styles in child_styles]
28+
if box_margins:
29+
resolve_margin = Size(
30+
max([margin.width for margin in box_margins]),
31+
(
32+
sum(
33+
max(margin1.bottom, margin2.top)
34+
for margin1, margin2 in zip(box_margins, box_margins[1:])
35+
)
36+
+ (box_margins[0].top + box_margins[-1].bottom)
37+
),
38+
)
39+
else:
40+
resolve_margin = Size(0, 0)
41+
2642
box_models = resolve_box_models(
27-
[child.styles.height for child in children],
43+
[styles.height for styles in child_styles],
2844
children,
2945
size,
3046
parent_size,
47+
resolve_margin,
3148
dimension="height",
3249
)
3350

0 commit comments

Comments
 (0)