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
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).


## [3.7.2] - Unreleased
## Unreleased

### Fixed

- Fixed `query_one` and `query_exactly_one` not raising documented `WrongType` exception.

### Changed

- Breaking change: `Widget.anchor` now has different semantics. It should be applied to a container and anchors to the bottom of the scroll position. https://github.com/Textualize/textual/pull/5950

### Added

- Added `Markdown.append` https://github.com/Textualize/textual/pull/5950
- Added `Widget.release_anchor` https://github.com/Textualize/textual/pull/5950

## [3.7.1] - 2025-07-09

### Fixed
Expand Down
3 changes: 1 addition & 2 deletions examples/mother.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def compose(self) -> ComposeResult:
def on_mount(self) -> None:
"""You might want to change the model if you don't have access to it."""
self.model = llm.get_model("gpt-4o")
self.query_one("#chat-view").anchor()

@on(Input.Submitted)
async def on_input(self, event: Input.Submitted) -> None:
Expand All @@ -82,8 +83,6 @@ async def on_input(self, event: Input.Submitted) -> None:
event.input.clear()
await chat_view.mount(Prompt(event.value))
await chat_view.mount(response := Response())
response.anchor()

self.send_prompt(event.value, response)

@work(thread=True)
Expand Down
14 changes: 13 additions & 1 deletion src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
from textual.geometry import NULL_SPACING, Offset, Region, Size, Spacing
from textual.map_geometry import MapGeometry
from textual.strip import Strip, StripRenderable
from textual.widget import Widget

if TYPE_CHECKING:
from typing_extensions import TypeAlias

from textual.screen import Screen
from textual.widget import Widget


class ReflowResult(NamedTuple):
Expand Down Expand Up @@ -605,6 +605,18 @@ def add_widget(
# Get the region that will be updated
sub_clip = clip.intersection(child_region)

if widget._anchored and not widget._anchor_released:
scroll_y = widget.scroll_y
new_scroll_y = (
arrange_result.spatial_map.total_region.bottom
- (
widget.container_size.height
- widget.scrollbar_size_horizontal
)
)
widget.set_reactive(Widget.scroll_y, new_scroll_y)
widget.watch_scroll_y(scroll_y, new_scroll_y)

if visible_only:
placements = arrange_result.get_visible_placements(
sub_clip - child_region.offset + widget.scroll_offset
Expand Down
15 changes: 9 additions & 6 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1663,12 +1663,15 @@ def _watch__select_end(
if end_region.y <= start_region.bottom or self._box_select:
select_regions.append(Region.union(start_region, end_region))
else:
container_region = Region.from_union(
[
start_widget.select_container.content_region,
end_widget.select_container.content_region,
]
)
try:
container_region = Region.from_union(
[
start_widget.select_container.content_region,
end_widget.select_container.content_region,
]
)
except NoMatches:
return

start_region = Region.from_corners(
start_region.x,
Expand Down
4 changes: 3 additions & 1 deletion src/textual/scrollbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,14 @@ async def _on_mouse_up(self, event: events.MouseUp) -> None:

def _on_mouse_capture(self, event: events.MouseCapture) -> None:
if isinstance(self._parent, Widget):
self._parent._user_scroll_interrupt = True
self._parent.release_anchor()
self.grabbed = event.mouse_position
self.grabbed_position = self.position

def _on_mouse_release(self, event: events.MouseRelease) -> None:
self.grabbed = None
if self.vertical and isinstance(self.parent, Widget):
self.parent._check_anchor()
event.stop()

async def _on_mouse_move(self, event: events.MouseMove) -> None:
Expand Down
Loading
Loading