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

Fix scroll placements #5253

Merged
merged 13 commits into from
Nov 18, 2024
13 changes: 9 additions & 4 deletions src/textual/_arrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import TYPE_CHECKING, Iterable, Mapping, Sequence

from textual._partition import partition
from textual.geometry import Region, Size, Spacing
from textual.geometry import NULL_OFFSET, NULL_SPACING, Region, Size, Spacing
from textual.layout import DockArrangeResult, WidgetPlacement

if TYPE_CHECKING:
Expand Down Expand Up @@ -133,7 +133,8 @@ def _arrange_dock_widgets(
region_offset = region.offset
size = region.size
width, height = size
null_spacing = Spacing()
null_spacing = NULL_SPACING
null_offset = NULL_OFFSET

top = right = bottom = left = 0

Expand Down Expand Up @@ -173,6 +174,7 @@ def _arrange_dock_widgets(
append_placement(
_WidgetPlacement(
dock_region.translate(region_offset),
null_offset,
null_spacing,
dock_widget,
top_z,
Expand Down Expand Up @@ -202,7 +204,8 @@ def _arrange_split_widgets(
placements: list[WidgetPlacement] = []
append_placement = placements.append
view_region = size.region
null_spacing = Spacing()
null_spacing = NULL_SPACING
null_offset = NULL_OFFSET

for split_widget in split_widgets:
split = split_widget.styles.split
Expand All @@ -226,7 +229,9 @@ def _arrange_split_widgets(
raise AssertionError("invalid value for split edge") # pragma: no-cover

append_placement(
_WidgetPlacement(split_region, null_spacing, split_widget, 1, True)
_WidgetPlacement(
split_region, null_offset, null_spacing, split_widget, 1, True
)
)

return placements, view_region
48 changes: 23 additions & 25 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from textual._cells import cell_len
from textual._context import visible_screen_stack
from textual._loop import loop_last
from textual.geometry import NULL_OFFSET, NULL_SPACING, Offset, Region, Size, Spacing
from textual.geometry import NULL_SPACING, Offset, Region, Size, Spacing
from textual.map_geometry import MapGeometry
from textual.strip import Strip, StripRenderable

Expand Down Expand Up @@ -535,8 +535,6 @@ def _arrange_root(
Compositor map and set of widgets.
"""

ORIGIN = NULL_OFFSET

map: CompositorMap = {}
widgets: set[Widget] = set()
add_new_widget = widgets.add
Expand Down Expand Up @@ -580,15 +578,9 @@ def add_widget(
add_new_widget(widget)
else:
add_new_invisible_widget(widget)
styles_offset = styles.offset
layout_offset = (
styles_offset.resolve(region.size, clip.size)
if styles_offset
else ORIGIN
)

# Container region is minus border
container_region = region.shrink(styles.gutter).translate(layout_offset)
container_region = region.shrink(styles.gutter)
container_size = container_region.size

# Widgets with scrollbars (containers or scroll view) require additional processing
Expand Down Expand Up @@ -643,15 +635,25 @@ def add_widget(
)

# Add all the widgets
for sub_region, _, sub_widget, z, fixed, overlay in reversed(
placements
):
for (
sub_region,
sub_region_offset,
_,
sub_widget,
z,
fixed,
overlay,
) in reversed(placements):
layer_index = get_layer_index(sub_widget.layer, 0)
# Combine regions with children to calculate the "virtual size"
if fixed:
widget_region = sub_region + placement_offset
widget_region = (
sub_region + sub_region_offset + placement_offset
)
else:
widget_region = sub_region + placement_scroll_offset
widget_region = (
sub_region + sub_region_offset + placement_scroll_offset
)

widget_order = order + ((layer_index, z, layer_order),)

Expand Down Expand Up @@ -699,7 +701,7 @@ def add_widget(
)

map[widget] = _MapGeometry(
region + layout_offset,
region,
order,
clip,
total_region.size,
Expand All @@ -711,27 +713,23 @@ def add_widget(
elif visible:
# Add the widget to the map

widget_region = region + layout_offset

if widget.absolute_offset is not None:
margin = styles.margin
widget_region = widget_region.at_offset(
widget.absolute_offset + margin.top_left
)
widget_region = widget_region.translate(
styles.offset.resolve(widget_region.grow(margin).size, size)
region = region.at_offset(widget.absolute_offset + margin.top_left)
region = region.translate(
styles.offset.resolve(region.grow(margin).size, size)
)
has_rule = styles.has_rule
if has_rule("constrain_x") or has_rule("constrain_y"):
widget_region = widget_region.constrain(
region = region.constrain(
styles.constrain_x,
styles.constrain_y,
styles.margin,
size.region,
)

map[widget._render_widget] = _MapGeometry(
widget_region,
region,
order,
clip,
region.size,
Expand Down
10 changes: 5 additions & 5 deletions src/textual/_spatial_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from typing_extensions import TypeAlias

from textual.geometry import Region
from textual.geometry import Offset, Region

ValueType = TypeVar("ValueType")
GridCoordinate: TypeAlias = "tuple[int, int]"
Expand Down Expand Up @@ -57,27 +57,27 @@ def _region_to_grid_coordinates(self, region: Region) -> Iterable[GridCoordinate
)

def insert(
self, regions_and_values: Iterable[tuple[Region, bool, bool, ValueType]]
self, regions_and_values: Iterable[tuple[Region, Offset, bool, bool, ValueType]]
) -> None:
"""Insert values into the Spatial map.

Values are associated with their region in Euclidean space, and a boolean that
indicates fixed regions. Fixed regions don't scroll and are always visible.

Args:
regions_and_values: An iterable of (REGION, FIXED, OVERLAY, VALUE).
regions_and_values: An iterable of (REGION, OFFSET, FIXED, OVERLAY, VALUE).
"""
append_fixed = self._fixed.append
get_grid_list = self._map.__getitem__
_region_to_grid = self._region_to_grid_coordinates
total_region = self.total_region
for region, fixed, overlay, value in regions_and_values:
for region, offset, fixed, overlay, value in regions_and_values:
if fixed:
append_fixed(value)
else:
if not overlay:
total_region = total_region.union(region)
for grid in _region_to_grid(region):
for grid in _region_to_grid(region + offset):
get_grid_list(grid).append(value)
self.total_region = total_region

Expand Down
20 changes: 15 additions & 5 deletions src/textual/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def spatial_map(self) -> SpatialMap[WidgetPlacement]:
self._spatial_map.insert(
(
placement.region.grow(placement.margin),
placement.offset,
placement.fixed,
placement.overlay,
placement,
Expand Down Expand Up @@ -75,7 +76,7 @@ def get_visible_placements(self, region: Region) -> list[WidgetPlacement]:
culled_placements = [
placement
for placement in visible_placements
if placement.fixed or overlaps(placement.region)
if placement.fixed or overlaps(placement.region + placement.offset)
]
return culled_placements

Expand All @@ -84,6 +85,7 @@ class WidgetPlacement(NamedTuple):
"""The position, size, and relative order of a widget within its parent."""

region: Region
offset: Offset
margin: Spacing
widget: Widget
order: int = 0
Expand All @@ -92,7 +94,7 @@ class WidgetPlacement(NamedTuple):

@classmethod
def translate(
cls, placements: list[WidgetPlacement], offset: Offset
cls, placements: list[WidgetPlacement], translate_offset: Offset
) -> list[WidgetPlacement]:
"""Move all placements by a given offset.

Expand All @@ -103,10 +105,18 @@ def translate(
Returns:
Placements with adjusted region, or same instance if offset is null.
"""
if offset:
if translate_offset:
return [
cls(region + offset, margin, layout_widget, order, fixed, overlay)
for region, margin, layout_widget, order, fixed, overlay in placements
cls(
region + translate_offset,
offset,
margin,
layout_widget,
order,
fixed,
overlay,
)
for region, offset, margin, layout_widget, order, fixed, overlay in placements
]
return placements

Expand Down
27 changes: 18 additions & 9 deletions src/textual/layouts/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from textual._resolve import resolve
from textual.css.scalar import Scalar
from textual.geometry import Region, Size, Spacing
from textual.geometry import NULL_OFFSET, Region, Size, Spacing
from textual.layout import ArrangeResult, Layout, WidgetPlacement

if TYPE_CHECKING:
Expand Down Expand Up @@ -258,6 +258,7 @@ def apply_height_limits(widget: Widget, height: int) -> int:
rows = resolve(row_scalars, size.height, gutter_horizontal, size, viewport)

placements: list[WidgetPlacement] = []
_WidgetPlacement = WidgetPlacement
add_placement = placements.append
widgets: list[Widget] = []
add_widget = widgets.append
Expand All @@ -272,26 +273,34 @@ def apply_height_limits(widget: Widget, height: int) -> int:
x2, cell_width = columns[min(max_column, column + column_span)]
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(
box_width, box_height, margin = widget._get_box_model(
cell_size,
viewport,
Fraction(cell_size.width),
Fraction(cell_size.height),
)
if self.stretch_height and len(children) > 1:
height = (
height
if (height > cell_size.height)
else Fraction(cell_size.height)
)
if box_height <= cell_size.height:
box_height = Fraction(cell_size.height)

region = (
Region(x, y, int(width + margin.width), int(height + margin.height))
Region(
x, y, int(box_width + margin.width), int(box_height + margin.height)
)
.crop_size(cell_size)
.shrink(margin)
)

placement_offset = (
styles.offset.resolve(cell_size, viewport)
if styles.has_rule("offset")
else NULL_OFFSET
)

add_placement(
WidgetPlacement(
_WidgetPlacement(
region + offset,
placement_offset,
(
margin
if gutter_spacing is None
Expand Down
24 changes: 18 additions & 6 deletions src/textual/layouts/horizontal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import TYPE_CHECKING

from textual._resolve import resolve_box_models
from textual.geometry import Region, Size
from textual.geometry import NULL_OFFSET, Region, Size
from textual.layout import ArrangeResult, Layout, WidgetPlacement

if TYPE_CHECKING:
Expand All @@ -24,6 +24,7 @@ def arrange(
) -> ArrangeResult:
placements: list[WidgetPlacement] = []
add_placement = placements.append
viewport = parent.app.size

child_styles = [child.styles for child in children]
box_margins: list[Spacing] = [
Expand Down Expand Up @@ -52,7 +53,7 @@ def arrange(
[styles.width for styles in child_styles],
children,
size,
parent.app.size,
viewport,
resolve_margin,
resolve_dimension="width",
)
Expand All @@ -75,20 +76,31 @@ def arrange(

_Region = Region
_WidgetPlacement = WidgetPlacement
_Size = Size
for widget, (content_width, content_height, box_margin), margin in zip(
children, box_models, margins
):
overlay = widget.styles.overlay == "screen"
styles = widget.styles
overlay = styles.overlay == "screen"
offset = (
styles.offset.resolve(
_Size(content_width.__floor__(), content_height.__floor__()),
viewport,
)
if styles.has_rule("offset")
else NULL_OFFSET
)
offset_y = box_margin.top
next_x = x + content_width
add_placement(
_WidgetPlacement(
_Region(
int(x),
x.__floor__(),
offset_y,
int(next_x - int(x)),
int(content_height),
(next_x - x.__floor__()).__floor__(),
content_height.__floor__(),
),
offset,
box_margin,
widget,
0,
Expand Down
Loading
Loading