Skip to content

Commit 422852a

Browse files
authored
Merge pull request #6228 from Textualize/loading-widget-fixes
loading widget mechanism
2 parents 3d13fbf + 3da69f4 commit 422852a

File tree

9 files changed

+114
-48
lines changed

9 files changed

+114
-48
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## Unreleased
9+
10+
### Added
11+
12+
- Added `GridLayout.max_column_width` https://github.com/Textualize/textual/pull/6228
13+
14+
### Changed
15+
16+
- Added `Screen.get_loading_widget` which deferes to `App.get_loading_widget` https://github.com/Textualize/textual/pull/6228
17+
18+
### Fixed
19+
20+
- Fixed `anchor` with `ScrollView` widgets https://github.com/Textualize/textual/pull/6228
21+
822
## [6.6.0] - 2025-11-10
923

1024
### Fixed

src/textual/_compositor.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,15 @@ def add_widget(
689689
arrange_result.scroll_spacing,
690690
)
691691
layer_order -= 1
692+
else:
693+
if widget._anchored and not widget._anchor_released:
694+
new_scroll_y = widget.virtual_size.height - (
695+
widget.container_size.height
696+
- widget.scrollbar_size_horizontal
697+
)
698+
widget.set_reactive(Widget.scroll_y, new_scroll_y)
699+
widget.set_reactive(Widget.scroll_target_y, new_scroll_y)
700+
widget.vertical_scrollbar._reactive_position = new_scroll_y
692701

693702
if visible:
694703
# Add any scrollbars
@@ -709,7 +718,7 @@ def add_widget(
709718
dock_gutter,
710719
)
711720

712-
map[widget] = _MapGeometry(
721+
map[widget._render_widget] = _MapGeometry(
713722
region,
714723
order,
715724
clip,

src/textual/containers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ class ItemGrid(Widget):
267267

268268
stretch_height: reactive[bool] = reactive(True)
269269
min_column_width: reactive[int | None] = reactive(None, layout=True)
270+
max_column_width: reactive[int | None] = reactive(None, layout=True)
270271
regular: reactive[bool] = reactive(False)
271272

272273
def __init__(
@@ -277,6 +278,7 @@ def __init__(
277278
classes: str | None = None,
278279
disabled: bool = False,
279280
min_column_width: int | None = None,
281+
max_column_width: int | None = None,
280282
stretch_height: bool = True,
281283
regular: bool = False,
282284
) -> None:
@@ -298,10 +300,12 @@ def __init__(
298300
)
299301
self.set_reactive(ItemGrid.stretch_height, stretch_height)
300302
self.set_reactive(ItemGrid.min_column_width, min_column_width)
303+
self.set_reactive(ItemGrid.max_column_width, max_column_width)
301304
self.set_reactive(ItemGrid.regular, regular)
302305

303306
def pre_layout(self, layout: Layout) -> None:
304307
if isinstance(layout, GridLayout):
305308
layout.stretch_height = self.stretch_height
306309
layout.min_column_width = self.min_column_width
310+
layout.max_column_width = self.max_column_width
307311
layout.regular = self.regular

src/textual/css/_style_properties.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -667,8 +667,9 @@ def __get__(
667667
Args:
668668
obj: The Styles object.
669669
objtype: The Styles class.
670+
670671
Returns:
671-
The ``Layout`` object.
672+
The `Layout` object.
672673
"""
673674
return obj.get_rule(self.name) # type: ignore[return-value]
674675

@@ -677,7 +678,7 @@ def __set__(self, obj: StylesBase, layout: str | Layout | None):
677678
Args:
678679
obj: The Styles object.
679680
layout: The layout to use. You can supply the name of the layout
680-
or a ``Layout`` object.
681+
or a `Layout` object.
681682
"""
682683

683684
from textual.layouts.factory import Layout # Prevents circular import
@@ -687,19 +688,23 @@ def __set__(self, obj: StylesBase, layout: str | Layout | None):
687688
if layout is None:
688689
if obj.clear_rule("layout"):
689690
obj.refresh(layout=True, children=True)
690-
elif isinstance(layout, Layout):
691-
if obj.set_rule("layout", layout):
692-
obj.refresh(layout=True, children=True)
693-
else:
694-
try:
695-
layout_object = get_layout(layout)
696-
except MissingLayout as error:
697-
raise StyleValueError(
698-
str(error),
699-
help_text=layout_property_help_text(self.name, context="inline"),
700-
)
701-
if obj.set_rule("layout", layout_object):
702-
obj.refresh(layout=True, children=True)
691+
return
692+
693+
if isinstance(layout, Layout):
694+
layout = layout.name
695+
696+
if obj.layout is not None and obj.layout.name == layout:
697+
return
698+
699+
try:
700+
layout_object = get_layout(layout)
701+
except MissingLayout as error:
702+
raise StyleValueError(
703+
str(error),
704+
help_text=layout_property_help_text(self.name, context="inline"),
705+
)
706+
if obj.set_rule("layout", layout_object):
707+
obj.refresh(layout=True, children=True)
703708

704709

705710
class OffsetProperty:

src/textual/layouts/grid.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ class GridLayout(Layout):
2020

2121
def __init__(self) -> None:
2222
self.min_column_width: int | None = None
23+
"""Maintain a minimum column width, or `None` for no minimum."""
24+
self.max_column_width: int | None = None
25+
"""Maintain a maximum column width, or `None` for no maximum."""
2326
self.stretch_height: bool = False
2427
"""Stretch the height of cells to be equal in each row."""
2528
self.regular: bool = False
29+
"""Grid should be regular (no remainder in last row)."""
2630
self.expand: bool = False
2731
"""Expand the grid to fit the container if it is smaller."""
2832
self.shrink: bool = False
@@ -57,14 +61,23 @@ def arrange(
5761

5862
table_size_columns = max(1, styles.grid_size_columns)
5963
min_column_width = self.min_column_width
64+
max_column_width = self.max_column_width
65+
66+
container_width = size.width
67+
if max_column_width is not None:
68+
container_width = (
69+
max(1, min(len(children), (container_width // max_column_width)))
70+
* max_column_width
71+
)
72+
size = Size(container_width, size.height)
6073

6174
if min_column_width is not None:
62-
container_width = size.width
6375
table_size_columns = max(
6476
1,
6577
(container_width + gutter_horizontal)
6678
// (min_column_width + gutter_horizontal),
6779
)
80+
6881
table_size_columns = min(table_size_columns, len(children))
6982
if self.regular:
7083
while len(children) % table_size_columns and table_size_columns > 1:
@@ -139,8 +152,7 @@ def repeat_scalars(scalars: Iterable[Scalar], count: int) -> list[Scalar]:
139152
cell_map: dict[tuple[int, int], tuple[Widget, bool]] = {}
140153
cell_size_map: dict[Widget, tuple[int, int, int, int]] = {}
141154

142-
column_count = table_size_columns
143-
next_coord = iter(cell_coords(column_count)).__next__
155+
next_coord = iter(cell_coords(table_size_columns)).__next__
144156
cell_coord = (0, 0)
145157
column = row = 0
146158

src/textual/screen.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,17 @@ def allow_select(self) -> bool:
558558
"""Check if this widget permits text selection."""
559559
return self.ALLOW_SELECT
560560

561+
def get_loading_widget(self) -> Widget:
562+
"""Get a widget to display a loading indicator.
563+
564+
The default implementation will defer to App.get_loading_widget.
565+
566+
Returns:
567+
A widget in place of this widget to indicate a loading.
568+
"""
569+
loading_widget = self.app.get_loading_widget()
570+
return loading_widget
571+
561572
def render(self) -> RenderableType:
562573
"""Render method inherited from widget, used to render the screen's background.
563574

src/textual/scroll_view.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ class ScrollView(ScrollableContainer):
1616
"""
1717
A base class for a Widget that handles its own scrolling (i.e. doesn't rely
1818
on the compositor to render children).
19+
20+
!!! note
21+
22+
This is the typically wrong class for making something scrollable. If you want to make something scroll, set it's
23+
`overflow` style to auto or scroll. Or use one of the pre-defined scrolling containers such as [VerticalScroll][textual.containers.VerticalScroll].
1924
"""
2025

2126
ALLOW_MAXIMIZE = True
@@ -32,6 +37,12 @@ def is_scrollable(self) -> bool:
3237
"""Always scrollable."""
3338
return True
3439

40+
@property
41+
def is_container(self) -> bool:
42+
"""Since a ScrollView should be a line-api widget, it won't have children,
43+
and therefore isn't a container."""
44+
return False
45+
3546
def watch_scroll_x(self, old_value: float, new_value: float) -> None:
3647
if self.show_horizontal_scrollbar:
3748
self.horizontal_scrollbar.position = new_value

src/textual/widget.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,7 @@ def get_loading_widget(self) -> Widget:
10141014
Returns:
10151015
A widget in place of this widget to indicate a loading.
10161016
"""
1017-
loading_widget = self.app.get_loading_widget()
1017+
loading_widget = self.screen.get_loading_widget()
10181018
return loading_widget
10191019

10201020
def set_loading(self, loading: bool) -> None:

tests/snapshot_tests/__snapshots__/test_snapshots/test_loading_indicator.svg

Lines changed: 28 additions & 28 deletions
Loading

0 commit comments

Comments
 (0)