Skip to content
Merged
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added

- Added `GridLayout.max_column_width` https://github.com/Textualize/textual/pull/6228

### Changed

- Added `Screen.get_loading_widget` which deferes to `App.get_loading_widget` https://github.com/Textualize/textual/pull/6228

### Fixed

- Fixed `anchor` with `ScrollView` widgets https://github.com/Textualize/textual/pull/6228

## [6.6.0] - 2025-11-10

### Fixed
Expand Down
11 changes: 10 additions & 1 deletion src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,15 @@ def add_widget(
arrange_result.scroll_spacing,
)
layer_order -= 1
else:
if widget._anchored and not widget._anchor_released:
new_scroll_y = widget.virtual_size.height - (
widget.container_size.height
- widget.scrollbar_size_horizontal
)
widget.set_reactive(Widget.scroll_y, new_scroll_y)
widget.set_reactive(Widget.scroll_target_y, new_scroll_y)
widget.vertical_scrollbar._reactive_position = new_scroll_y

if visible:
# Add any scrollbars
Expand All @@ -709,7 +718,7 @@ def add_widget(
dock_gutter,
)

map[widget] = _MapGeometry(
map[widget._render_widget] = _MapGeometry(
region,
order,
clip,
Expand Down
4 changes: 4 additions & 0 deletions src/textual/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ class ItemGrid(Widget):

stretch_height: reactive[bool] = reactive(True)
min_column_width: reactive[int | None] = reactive(None, layout=True)
max_column_width: reactive[int | None] = reactive(None, layout=True)
regular: reactive[bool] = reactive(False)

def __init__(
Expand All @@ -277,6 +278,7 @@ def __init__(
classes: str | None = None,
disabled: bool = False,
min_column_width: int | None = None,
max_column_width: int | None = None,
stretch_height: bool = True,
regular: bool = False,
) -> None:
Expand All @@ -298,10 +300,12 @@ def __init__(
)
self.set_reactive(ItemGrid.stretch_height, stretch_height)
self.set_reactive(ItemGrid.min_column_width, min_column_width)
self.set_reactive(ItemGrid.max_column_width, max_column_width)
self.set_reactive(ItemGrid.regular, regular)

def pre_layout(self, layout: Layout) -> None:
if isinstance(layout, GridLayout):
layout.stretch_height = self.stretch_height
layout.min_column_width = self.min_column_width
layout.max_column_width = self.max_column_width
layout.regular = self.regular
35 changes: 20 additions & 15 deletions src/textual/css/_style_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,8 +667,9 @@ def __get__(
Args:
obj: The Styles object.
objtype: The Styles class.

Returns:
The ``Layout`` object.
The `Layout` object.
"""
return obj.get_rule(self.name) # type: ignore[return-value]

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

from textual.layouts.factory import Layout # Prevents circular import
Expand All @@ -687,19 +688,23 @@ def __set__(self, obj: StylesBase, layout: str | Layout | None):
if layout is None:
if obj.clear_rule("layout"):
obj.refresh(layout=True, children=True)
elif isinstance(layout, Layout):
if obj.set_rule("layout", layout):
obj.refresh(layout=True, children=True)
else:
try:
layout_object = get_layout(layout)
except MissingLayout as error:
raise StyleValueError(
str(error),
help_text=layout_property_help_text(self.name, context="inline"),
)
if obj.set_rule("layout", layout_object):
obj.refresh(layout=True, children=True)
return

if isinstance(layout, Layout):
layout = layout.name

if obj.layout is not None and obj.layout.name == layout:
return

try:
layout_object = get_layout(layout)
except MissingLayout as error:
raise StyleValueError(
str(error),
help_text=layout_property_help_text(self.name, context="inline"),
)
if obj.set_rule("layout", layout_object):
obj.refresh(layout=True, children=True)


class OffsetProperty:
Expand Down
18 changes: 15 additions & 3 deletions src/textual/layouts/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ class GridLayout(Layout):

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

table_size_columns = max(1, styles.grid_size_columns)
min_column_width = self.min_column_width
max_column_width = self.max_column_width

container_width = size.width
if max_column_width is not None:
container_width = (
max(1, min(len(children), (container_width // max_column_width)))
* max_column_width
)
size = Size(container_width, size.height)

if min_column_width is not None:
container_width = size.width
table_size_columns = max(
1,
(container_width + gutter_horizontal)
// (min_column_width + gutter_horizontal),
)

table_size_columns = min(table_size_columns, len(children))
if self.regular:
while len(children) % table_size_columns and table_size_columns > 1:
Expand Down Expand Up @@ -139,8 +152,7 @@ def repeat_scalars(scalars: Iterable[Scalar], count: int) -> list[Scalar]:
cell_map: dict[tuple[int, int], tuple[Widget, bool]] = {}
cell_size_map: dict[Widget, tuple[int, int, int, int]] = {}

column_count = table_size_columns
next_coord = iter(cell_coords(column_count)).__next__
next_coord = iter(cell_coords(table_size_columns)).__next__
cell_coord = (0, 0)
column = row = 0

Expand Down
11 changes: 11 additions & 0 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,17 @@ def allow_select(self) -> bool:
"""Check if this widget permits text selection."""
return self.ALLOW_SELECT

def get_loading_widget(self) -> Widget:
"""Get a widget to display a loading indicator.

The default implementation will defer to App.get_loading_widget.

Returns:
A widget in place of this widget to indicate a loading.
"""
loading_widget = self.app.get_loading_widget()
return loading_widget

def render(self) -> RenderableType:
"""Render method inherited from widget, used to render the screen's background.

Expand Down
11 changes: 11 additions & 0 deletions src/textual/scroll_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class ScrollView(ScrollableContainer):
"""
A base class for a Widget that handles its own scrolling (i.e. doesn't rely
on the compositor to render children).

!!! note

This is the typically wrong class for making something scrollable. If you want to make something scroll, set it's
`overflow` style to auto or scroll. Or use one of the pre-defined scrolling containers such as [VerticalScroll][textual.containers.VerticalScroll].
"""

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

@property
def is_container(self) -> bool:
"""Since a ScrollView should be a line-api widget, it won't have children,
and therefore isn't a container."""
return False

def watch_scroll_x(self, old_value: float, new_value: float) -> None:
if self.show_horizontal_scrollbar:
self.horizontal_scrollbar.position = new_value
Expand Down
2 changes: 1 addition & 1 deletion src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -1014,7 +1014,7 @@ def get_loading_widget(self) -> Widget:
Returns:
A widget in place of this widget to indicate a loading.
"""
loading_widget = self.app.get_loading_widget()
loading_widget = self.screen.get_loading_widget()
return loading_widget

def set_loading(self, loading: bool) -> None:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading