Skip to content

Commit 0d99b0c

Browse files
authored
Merge pull request #1067 from Textualize/fr-unit
Fr unit
2 parents 881a9c2 + c705752 commit 0d99b0c

File tree

13 files changed

+467
-61
lines changed

13 files changed

+467
-61
lines changed

sandbox/will/fr.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from textual.app import App, ComposeResult
2+
from textual.containers import Horizontal, Vertical
3+
from textual.widgets import Static
4+
5+
6+
class StaticText(Static):
7+
pass
8+
9+
10+
class Header(Static):
11+
pass
12+
13+
14+
class Footer(Static):
15+
pass
16+
17+
18+
class FrApp(App):
19+
20+
CSS = """
21+
Screen {
22+
layout: horizontal;
23+
align: center middle;
24+
25+
}
26+
27+
Vertical {
28+
29+
}
30+
31+
Header {
32+
background: $boost;
33+
34+
content-align: center middle;
35+
text-align: center;
36+
color: $text;
37+
height: 3;
38+
border: tall $warning;
39+
}
40+
41+
Horizontal {
42+
height: 1fr;
43+
align: center middle;
44+
}
45+
46+
Footer {
47+
background: $boost;
48+
49+
content-align: center middle;
50+
text-align: center;
51+
52+
color: $text;
53+
height: 6;
54+
border: tall $warning;
55+
}
56+
57+
StaticText {
58+
background: $boost;
59+
height: 8;
60+
content-align: center middle;
61+
text-align: center;
62+
color: $text;
63+
}
64+
65+
#foo {
66+
width: 10;
67+
border: tall $primary;
68+
}
69+
70+
#bar {
71+
width: 1fr;
72+
border: tall $error;
73+
74+
}
75+
76+
#baz {
77+
width: 20;
78+
border: tall $success;
79+
}
80+
81+
"""
82+
83+
def compose(self) -> ComposeResult:
84+
yield Vertical(
85+
Header("HEADER"),
86+
Horizontal(
87+
StaticText("foo", id="foo"),
88+
StaticText("bar", id="bar"),
89+
StaticText("baz", id="baz"),
90+
),
91+
Footer("FOOTER"),
92+
)
93+
94+
95+
app = FrApp()
96+
app.run()

src/textual/_arrange.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,9 @@ def arrange(
6060
for dock_widget in dock_widgets:
6161
edge = dock_widget.styles.dock
6262

63-
fraction_unit = Fraction(
64-
size.height if edge in ("top", "bottom") else size.width
63+
box_model = dock_widget._get_box_model(
64+
size, viewport, Fraction(size.width), Fraction(size.height)
6565
)
66-
box_model = dock_widget._get_box_model(size, viewport, fraction_unit)
6766
widget_width_fraction, widget_height_fraction, margin = box_model
6867

6968
widget_width = int(widget_width_fraction) + margin.width

src/textual/_resolve.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
from __future__ import annotations
22

3+
import sys
34
from fractions import Fraction
45
from itertools import accumulate
5-
from typing import cast, Sequence
6+
from typing import cast, Sequence, TYPE_CHECKING
67

8+
from .box_model import BoxModel
79
from .css.scalar import Scalar
810
from .geometry import Size
911

12+
if TYPE_CHECKING:
13+
from .widget import Widget
14+
15+
16+
if sys.version_info >= (3, 8):
17+
from typing import Literal
18+
else:
19+
from typing_extensions import Literal
20+
1021

1122
def resolve(
1223
dimensions: Sequence[Scalar],
@@ -71,3 +82,79 @@ def resolve(
7182
]
7283

7384
return results
85+
86+
87+
def resolve_box_models(
88+
dimensions: list[Scalar | None],
89+
widgets: list[Widget],
90+
size: Size,
91+
parent_size: Size,
92+
dimension: Literal["width", "height"] = "width",
93+
) -> list[BoxModel]:
94+
"""Resolve box models for a list of dimensions
95+
96+
Args:
97+
dimensions (list[Scalar | None]): A list of Scalars or Nones for each dimension.
98+
widgets (list[Widget]): Widgets in resolve.
99+
size (Size): size of container.
100+
parent_size (Size): Size of parent.
101+
dimensions (Literal["width", "height"]): Which dimension to resolve.
102+
103+
Returns:
104+
list[BoxModel]: List of resolved box models.
105+
"""
106+
107+
fraction_width = Fraction(size.width)
108+
fraction_height = Fraction(size.height)
109+
box_models: list[BoxModel | None] = [
110+
(
111+
None
112+
if dimension is not None and dimension.is_fraction
113+
else widget._get_box_model(
114+
size, parent_size, fraction_width, fraction_height
115+
)
116+
)
117+
for (dimension, widget) in zip(dimensions, widgets)
118+
]
119+
120+
if dimension == "width":
121+
total_remaining = sum(
122+
box_model.width for box_model in box_models if box_model is not None
123+
)
124+
remaining_space = max(0, size.width - total_remaining)
125+
else:
126+
total_remaining = sum(
127+
box_model.height for box_model in box_models if box_model is not None
128+
)
129+
remaining_space = max(0, size.height - total_remaining)
130+
131+
fraction_unit = Fraction(
132+
remaining_space,
133+
int(
134+
sum(
135+
dimension.value
136+
for dimension in dimensions
137+
if dimension and dimension.is_fraction
138+
)
139+
)
140+
or 1,
141+
)
142+
if dimension == "width":
143+
width_fraction = fraction_unit
144+
height_fraction = Fraction(size.height)
145+
else:
146+
width_fraction = Fraction(size.width)
147+
height_fraction = fraction_unit
148+
149+
box_models = [
150+
box_model
151+
or widget._get_box_model(
152+
size,
153+
parent_size,
154+
width_fraction,
155+
height_fraction,
156+
)
157+
for widget, box_model in zip(widgets, box_models)
158+
]
159+
160+
return cast("list[BoxModel]", box_models)

src/textual/app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,12 @@ async def on_screenshot():
12711271
self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer")
12721272

12731273
async def _on_compose(self) -> None:
1274-
widgets = list(self.compose())
1274+
try:
1275+
widgets = list(self.compose())
1276+
except TypeError as error:
1277+
raise TypeError(
1278+
f"{self!r} compose() returned an invalid response; {error}"
1279+
) from None
12751280
await self.mount_all(widgets)
12761281

12771282
def _on_idle(self) -> None:

src/textual/box_model.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ def get_box_model(
2020
styles: StylesBase,
2121
container: Size,
2222
viewport: Size,
23-
fraction_unit: Fraction,
23+
width_fraction: Fraction,
24+
height_fraction: Fraction,
2425
get_content_width: Callable[[Size, Size], int],
2526
get_content_height: Callable[[Size, Size, int], int],
2627
) -> BoxModel:
@@ -30,6 +31,8 @@ def get_box_model(
3031
styles (StylesBase): Styles object.
3132
container (Size): The size of the widget container.
3233
viewport (Size): The viewport size.
34+
width_fraction (Fraction): A fraction used for 1 `fr` unit on the width dimension.
35+
height_fraction (Fraction):A fraction used for 1 `fr` unit on the height dimension.
3336
get_auto_width (Callable): A callable which accepts container size and parent size and returns a width.
3437
get_auto_height (Callable): A callable which accepts container size and parent size and returns a height.
3538
@@ -63,22 +66,22 @@ def get_box_model(
6366
# An explicit width
6467
styles_width = styles.width
6568
content_width = styles_width.resolve_dimension(
66-
sizing_container - styles.margin.totals, viewport, fraction_unit
69+
sizing_container - styles.margin.totals, viewport, width_fraction
6770
)
6871
if is_border_box and styles_width.excludes_border:
6972
content_width -= gutter.width
7073

7174
if styles.min_width is not None:
7275
# Restrict to minimum width, if set
7376
min_width = styles.min_width.resolve_dimension(
74-
content_container, viewport, fraction_unit
77+
content_container, viewport, width_fraction
7578
)
7679
content_width = max(content_width, min_width)
7780

7881
if styles.max_width is not None:
7982
# Restrict to maximum width, if set
8083
max_width = styles.max_width.resolve_dimension(
81-
content_container, viewport, fraction_unit
84+
content_container, viewport, width_fraction
8285
)
8386
if is_border_box:
8487
max_width -= gutter.width
@@ -98,22 +101,22 @@ def get_box_model(
98101
styles_height = styles.height
99102
# Explicit height set
100103
content_height = styles_height.resolve_dimension(
101-
sizing_container - styles.margin.totals, viewport, fraction_unit
104+
sizing_container - styles.margin.totals, viewport, height_fraction
102105
)
103106
if is_border_box and styles_height.excludes_border:
104107
content_height -= gutter.height
105108

106109
if styles.min_height is not None:
107110
# Restrict to minimum height, if set
108111
min_height = styles.min_height.resolve_dimension(
109-
content_container, viewport, fraction_unit
112+
content_container, viewport, height_fraction
110113
)
111114
content_height = max(content_height, min_height)
112115

113116
if styles.max_height is not None:
114117
# Restrict maximum height, if set
115118
max_height = styles.max_height.resolve_dimension(
116-
content_container, viewport, fraction_unit
119+
content_container, viewport, height_fraction
117120
)
118121
content_height = min(content_height, max_height)
119122

src/textual/layouts/grid.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,7 @@ def repeat_scalars(scalars: Iterable[Scalar], count: int) -> list[Scalar]:
145145
y2, cell_height = rows[min(max_row, row + row_span)]
146146
cell_size = Size(cell_width + x2 - x, cell_height + y2 - y)
147147
width, height, margin = widget._get_box_model(
148-
cell_size,
149-
viewport,
150-
fraction_unit,
148+
cell_size, viewport, fraction_unit, fraction_unit
151149
)
152150
region = (
153151
Region(x, y, int(width + margin.width), int(height + margin.height))

src/textual/layouts/horizontal.py

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

33
from fractions import Fraction
4-
from typing import cast
54

6-
from textual.geometry import Size, Region
7-
from textual._layout import ArrangeResult, Layout, WidgetPlacement
8-
9-
from textual.widget import Widget
5+
from .._resolve import resolve_box_models
6+
from ..geometry import Size, Region
7+
from .._layout import ArrangeResult, Layout, WidgetPlacement
8+
from ..widget import Widget
109

1110

1211
class HorizontalLayout(Layout):
@@ -22,20 +21,16 @@ def arrange(
2221

2322
placements: list[WidgetPlacement] = []
2423
add_placement = placements.append
25-
2624
x = max_height = Fraction(0)
2725
parent_size = parent.outer_size
2826

29-
styles = [child.styles for child in children if child.styles.width is not None]
30-
total_fraction = sum(
31-
[int(style.width.value) for style in styles if style.width.is_fraction]
27+
box_models = resolve_box_models(
28+
[child.styles.width for child in children],
29+
children,
30+
size,
31+
parent_size,
32+
dimension="width",
3233
)
33-
fraction_unit = Fraction(size.width, total_fraction or 1)
34-
35-
box_models = [
36-
widget._get_box_model(size, parent_size, fraction_unit)
37-
for widget in cast("list[Widget]", children)
38-
]
3934

4035
margins = [
4136
max((box1.margin.right, box2.margin.left))

src/textual/layouts/vertical.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from fractions import Fraction
44
from typing import TYPE_CHECKING
55

6+
from .._resolve import resolve_box_models
67
from ..geometry import Region, Size
78
from .._layout import ArrangeResult, Layout, WidgetPlacement
89

@@ -21,19 +22,15 @@ def arrange(
2122

2223
placements: list[WidgetPlacement] = []
2324
add_placement = placements.append
24-
2525
parent_size = parent.outer_size
2626

27-
styles = [child.styles for child in children if child.styles.height is not None]
28-
total_fraction = sum(
29-
[int(style.height.value) for style in styles if style.height.is_fraction]
27+
box_models = resolve_box_models(
28+
[child.styles.height for child in children],
29+
children,
30+
size,
31+
parent_size,
32+
dimension="height",
3033
)
31-
fraction_unit = Fraction(size.height, total_fraction or 1)
32-
33-
box_models = [
34-
widget._get_box_model(size, parent_size, fraction_unit)
35-
for widget in children
36-
]
3734

3835
margins = [
3936
max((box1.margin.bottom, box2.margin.top))

0 commit comments

Comments
 (0)