Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fr unit #1067

Merged
merged 4 commits into from
Oct 31, 2022
Merged

Fr unit #1067

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions sandbox/will/fr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Static


class StaticText(Static):
pass


class Header(Static):
pass


class Footer(Static):
pass


class FrApp(App):

CSS = """
Screen {
layout: horizontal;
align: center middle;

}

Vertical {

}

Header {
background: $boost;

content-align: center middle;
text-align: center;
color: $text;
height: 3;
border: tall $warning;
}

Horizontal {
height: 1fr;
align: center middle;
}

Footer {
background: $boost;

content-align: center middle;
text-align: center;

color: $text;
height: 6;
border: tall $warning;
}

StaticText {
background: $boost;
height: 8;
content-align: center middle;
text-align: center;
color: $text;
}

#foo {
width: 10;
border: tall $primary;
}

#bar {
width: 1fr;
border: tall $error;

}

#baz {
width: 20;
border: tall $success;
}

"""

def compose(self) -> ComposeResult:
yield Vertical(
Header("HEADER"),
Horizontal(
StaticText("foo", id="foo"),
StaticText("bar", id="bar"),
StaticText("baz", id="baz"),
),
Footer("FOOTER"),
)


app = FrApp()
app.run()
5 changes: 2 additions & 3 deletions src/textual/_arrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,9 @@ def arrange(
for dock_widget in dock_widgets:
edge = dock_widget.styles.dock

fraction_unit = Fraction(
size.height if edge in ("top", "bottom") else size.width
box_model = dock_widget._get_box_model(
size, viewport, Fraction(size.width), Fraction(size.height)
)
box_model = dock_widget._get_box_model(size, viewport, fraction_unit)
widget_width_fraction, widget_height_fraction, margin = box_model

widget_width = int(widget_width_fraction) + margin.width
Expand Down
89 changes: 88 additions & 1 deletion src/textual/_resolve.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
from __future__ import annotations

import sys
from fractions import Fraction
from itertools import accumulate
from typing import cast, Sequence
from typing import cast, Sequence, TYPE_CHECKING

from .box_model import BoxModel
from .css.scalar import Scalar
from .geometry import Size

if TYPE_CHECKING:
from .widget import Widget


if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal


def resolve(
dimensions: Sequence[Scalar],
Expand Down Expand Up @@ -71,3 +82,79 @@ def resolve(
]

return results


def resolve_box_models(
dimensions: list[Scalar | None],
widgets: list[Widget],
size: Size,
parent_size: Size,
dimension: Literal["width", "height"] = "width",
) -> list[BoxModel]:
"""Resolve box models for a list of dimensions

Args:
dimensions (list[Scalar | None]): A list of Scalars or Nones for each dimension.
widgets (list[Widget]): Widgets in resolve.
size (Size): size of container.
parent_size (Size): Size of parent.
dimensions (Literal["width", "height"]): Which dimension to resolve.

Returns:
list[BoxModel]: List of resolved box models.
"""

fraction_width = Fraction(size.width)
fraction_height = Fraction(size.height)
box_models: list[BoxModel | None] = [
(
None
if dimension is not None and dimension.is_fraction
else widget._get_box_model(
size, parent_size, fraction_width, fraction_height
)
)
for (dimension, widget) in zip(dimensions, widgets)
]

if dimension == "width":
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)
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)

fraction_unit = Fraction(
remaining_space,
int(
sum(
dimension.value
for dimension in dimensions
if dimension and dimension.is_fraction
)
)
or 1,
)
if dimension == "width":
width_fraction = fraction_unit
height_fraction = Fraction(size.height)
else:
width_fraction = Fraction(size.width)
height_fraction = fraction_unit

box_models = [
box_model
or widget._get_box_model(
size,
parent_size,
width_fraction,
height_fraction,
)
for widget, box_model in zip(widgets, box_models)
]

return cast("list[BoxModel]", box_models)
7 changes: 6 additions & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,12 @@ async def on_screenshot():
self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer")

async def _on_compose(self) -> None:
widgets = list(self.compose())
try:
widgets = list(self.compose())
except TypeError as error:
raise TypeError(
f"{self!r} compose() returned an invalid response; {error}"
) from None
await self.mount_all(widgets)

def _on_idle(self) -> None:
Expand Down
17 changes: 10 additions & 7 deletions src/textual/box_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ def get_box_model(
styles: StylesBase,
container: Size,
viewport: Size,
fraction_unit: Fraction,
width_fraction: Fraction,
height_fraction: Fraction,
get_content_width: Callable[[Size, Size], int],
get_content_height: Callable[[Size, Size, int], int],
) -> BoxModel:
Expand All @@ -30,6 +31,8 @@ def get_box_model(
styles (StylesBase): Styles object.
container (Size): The size of the widget container.
viewport (Size): The viewport size.
width_fraction (Fraction): A fraction used for 1 `fr` unit on the width dimension.
height_fraction (Fraction):A fraction used for 1 `fr` unit on the height dimension.
get_auto_width (Callable): A callable which accepts container size and parent size and returns a width.
get_auto_height (Callable): A callable which accepts container size and parent size and returns a height.

Expand Down Expand Up @@ -63,22 +66,22 @@ def get_box_model(
# An explicit width
styles_width = styles.width
content_width = styles_width.resolve_dimension(
sizing_container - styles.margin.totals, viewport, fraction_unit
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_dimension(
content_container, viewport, fraction_unit
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_dimension(
content_container, viewport, fraction_unit
content_container, viewport, width_fraction
)
if is_border_box:
max_width -= gutter.width
Expand All @@ -98,22 +101,22 @@ def get_box_model(
styles_height = styles.height
# Explicit height set
content_height = styles_height.resolve_dimension(
sizing_container - styles.margin.totals, viewport, fraction_unit
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_dimension(
content_container, viewport, fraction_unit
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_dimension(
content_container, viewport, fraction_unit
content_container, viewport, height_fraction
)
content_height = min(content_height, max_height)

Expand Down
4 changes: 1 addition & 3 deletions src/textual/layouts/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,7 @@ def repeat_scalars(scalars: Iterable[Scalar], count: int) -> list[Scalar]:
y2, cell_height = rows[min(max_row, row + row_span)]
cell_size = Size(cell_width + x2 - x, cell_height + y2 - y)
width, height, margin = widget._get_box_model(
cell_size,
viewport,
fraction_unit,
cell_size, viewport, fraction_unit, fraction_unit
)
region = (
Region(x, y, int(width + margin.width), int(height + margin.height))
Expand Down
25 changes: 10 additions & 15 deletions src/textual/layouts/horizontal.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from __future__ import annotations

from fractions import Fraction
from typing import cast

from textual.geometry import Size, Region
from textual._layout import ArrangeResult, Layout, WidgetPlacement

from textual.widget import Widget
from .._resolve import resolve_box_models
from ..geometry import Size, Region
from .._layout import ArrangeResult, Layout, WidgetPlacement
from ..widget import Widget


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

placements: list[WidgetPlacement] = []
add_placement = placements.append

x = max_height = Fraction(0)
parent_size = parent.outer_size

styles = [child.styles for child in children if child.styles.width is not None]
total_fraction = sum(
[int(style.width.value) for style in styles if style.width.is_fraction]
box_models = resolve_box_models(
[child.styles.width for child in children],
children,
size,
parent_size,
dimension="width",
)
fraction_unit = Fraction(size.width, total_fraction or 1)

box_models = [
widget._get_box_model(size, parent_size, fraction_unit)
for widget in cast("list[Widget]", children)
]

margins = [
max((box1.margin.right, box2.margin.left))
Expand Down
17 changes: 7 additions & 10 deletions src/textual/layouts/vertical.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from fractions import Fraction
from typing import TYPE_CHECKING

from .._resolve import resolve_box_models
from ..geometry import Region, Size
from .._layout import ArrangeResult, Layout, WidgetPlacement

Expand All @@ -21,19 +22,15 @@ def arrange(

placements: list[WidgetPlacement] = []
add_placement = placements.append

parent_size = parent.outer_size

styles = [child.styles for child in children if child.styles.height is not None]
total_fraction = sum(
[int(style.height.value) for style in styles if style.height.is_fraction]
box_models = resolve_box_models(
[child.styles.height for child in children],
children,
size,
parent_size,
dimension="height",
)
fraction_unit = Fraction(size.height, total_fraction or 1)

box_models = [
widget._get_box_model(size, parent_size, fraction_unit)
for widget in children
]

margins = [
max((box1.margin.bottom, box2.margin.top))
Expand Down
Loading