Skip to content

Commit 678ddaf

Browse files
committed
Merge branch 'main' into build-update-supported-python-versions
2 parents d56cd79 + 38ca1dd commit 678ddaf

35 files changed

+885
-119
lines changed

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,28 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1212
- Eager tasks are now enabled On Python3.12 and above https://github.com/Textualize/textual/pull/6102
1313
- `Widget._arrange` is now public (as `Widget.arrange`) https://github.com/Textualize/textual/pull/6108
1414
- Reduced number of layout operations required to update the screen https://github.com/Textualize/textual/pull/6108
15+
- The :hover pseudo-class no applies to the first widget under the mouse with a hover style set https://github.com/Textualize/textual/pull/6132
16+
- The footer key hover background is more visible https://github.com/Textualize/textual/pull/6132
17+
- Made `App.delay_update` public https://github.com/Textualize/textual/pull/6137
18+
- Pilot.click will return True if the initial mouse down is on the specified target https://github.com/Textualize/textual/pull/6139
1519

1620
### Added
1721

1822
- Added `DOMNode.displayed_and_visible_children` https://github.com/Textualize/textual/pull/6102
1923
- Added `Widget.process_layout` https://github.com/Textualize/textual/pull/6105
2024
- Added `App.viewport_size` https://github.com/Textualize/textual/pull/6105
2125
- Added `Screen.size` https://github.com/Textualize/textual/pull/6105
26+
- Added `compact` to Binding.Group https://github.com/Textualize/textual/pull/6132
27+
- Added `Screen.get_hover_widgets_at` https://github.com/Textualize/textual/pull/6132
28+
- Added `Content.wrap` https://github.com/Textualize/textual/pull/6138
2229

2330
### Fixed
2431

2532
- Fixed issue where Segments with a style of `None` aren't rendered https://github.com/Textualize/textual/pull/6109
33+
- Fixed visual glitches and crash when changing `DataTable.header_height` https://github.com/Textualize/textual/pull/6128
34+
- Fixed TextArea.placeholder not handling multi-lines https://github.com/Textualize/textual/pull/6138
35+
- Fixed issue with RichLog when App.theme is set early https://github.com/Textualize/textual/pull/6141
36+
- Fixed children of collapsible not being focusable after collapsible is expanded https://github.com/Textualize/textual/pull/6143
2637

2738
## [6.1.0] - 2025-08-01
2839

@@ -44,7 +55,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
4455

4556
### Added
4657

47-
- Added `bar_renderable` to `ProgressBar` widget https://github.com/Textualize/textual/pull/5963
58+
- Added `BAR_RENDERABLE` to `ProgressBar` widget https://github.com/Textualize/textual/pull/5963
4859
- Added `OptionList.set_options` https://github.com/Textualize/textual/pull/6048
4960
- Added `TextArea.suggestion` https://github.com/Textualize/textual/pull/6048
5061
- Added `TextArea.placeholder` https://github.com/Textualize/textual/pull/6048

docs/how-to/design-a-layout.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Textual's layout system is flexible enough to accommodate just about any applica
1010
The initial design of your application is best done with a sketch.
1111
You could use a drawing package such as [Excalidraw](https://excalidraw.com/) for your sketch, but pen and paper is equally as good.
1212

13-
Start by drawing a rectangle to represent a blank terminal, then draw a rectangle for each element in your application. Annotate each of the rectangles with the content they will contain, and note wether they will scroll (and in what direction).
13+
Start by drawing a rectangle to represent a blank terminal, then draw a rectangle for each element in your application. Annotate each of the rectangles with the content they will contain, and note whether they will scroll (and in what direction).
1414

1515
For the purposes of this article we are going to design a layout for a Twitter or Mastodon client, which will have a header / footer and a number of columns.
1616

src/textual/_compositor.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -848,9 +848,10 @@ def get_widgets_at(self, x: int, y: int) -> Iterable[tuple[Widget, Region]]:
848848
Sequence of (WIDGET, REGION) tuples.
849849
"""
850850
contains = Region.contains
851-
for widget, cropped_region, region in self.layers_visible[y]:
852-
if contains(cropped_region, x, y) and widget.visible:
853-
yield widget, region
851+
if len(self.layers_visible) > y >= 0:
852+
for widget, cropped_region, region in self.layers_visible[y]:
853+
if contains(cropped_region, x, y) and widget.visible:
854+
yield widget, region
854855

855856
def get_style_at(self, x: int, y: int) -> Style:
856857
"""Get the Style at the given cell or Style.null()

src/textual/app.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,9 @@ def __init__(
617617
self._sync_available = False
618618

619619
self.mouse_over: Widget | None = None
620+
"""The widget directly under the mouse."""
621+
self.hover_over: Widget | None = None
622+
"""The first widget with a hover style under the mouse."""
620623
self.mouse_captured: Widget | None = None
621624
self._driver: Driver | None = None
622625
self._exit_renderables: list[RenderableType] = []
@@ -1016,10 +1019,11 @@ def _end_batch(self) -> None:
10161019
if not self._batch_count:
10171020
self.check_idle()
10181021

1019-
def _delay_update(self, delay: float = 0.05) -> None:
1022+
def delay_update(self, delay: float = 0.05) -> None:
10201023
"""Delay updates for a short period of time.
10211024
10221025
May be used to mask a brief transition.
1026+
Consider this method only if you aren't able to use `App.batch_update`.
10231027
10241028
Args:
10251029
delay: Delay before updating.
@@ -1032,7 +1036,7 @@ def end_batch() -> None:
10321036
if not self._batch_count:
10331037
self.screen.refresh()
10341038

1035-
self.set_timer(delay, end_batch, name="_delay_update")
1039+
self.set_timer(delay, end_batch, name="delay_update")
10361040

10371041
@contextmanager
10381042
def _context(self) -> Generator[None, None, None]:
@@ -2095,7 +2099,7 @@ async def run_app(app: App[ReturnType]) -> None:
20952099

20962100
# Launch the app in the "background"
20972101

2098-
app_task = create_task(run_app(app), name=f"run_test {app}")
2102+
self._task = app_task = create_task(run_app(app), name=f"run_test {app}")
20992103

21002104
# Wait until the app has performed all startup routines.
21012105
await app_ready_event.wait()
@@ -3010,7 +3014,9 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
30103014
"""
30113015
self.screen.set_focus(widget, scroll_visible)
30123016

3013-
def _set_mouse_over(self, widget: Widget | None) -> None:
3017+
def _set_mouse_over(
3018+
self, widget: Widget | None, hover_widget: Widget | None
3019+
) -> None:
30143020
"""Called when the mouse is over another widget.
30153021
30163022
Args:
@@ -3031,6 +3037,12 @@ def _set_mouse_over(self, widget: Widget | None) -> None:
30313037
widget.post_message(events.Enter(widget))
30323038
finally:
30333039
self.mouse_over = widget
3040+
if self.hover_over is not None:
3041+
self.hover_over.mouse_hover = False
3042+
if hover_widget is not None:
3043+
hover_widget.mouse_hover = True
3044+
3045+
self.hover_over = hover_widget
30343046

30353047
def _update_mouse_over(self, screen: Screen) -> None:
30363048
"""Updates the mouse over after the next refresh.
@@ -3046,12 +3058,16 @@ def _update_mouse_over(self, screen: Screen) -> None:
30463058
async def check_mouse() -> None:
30473059
"""Check if the mouse over widget has changed."""
30483060
try:
3049-
widget, _ = screen.get_widget_at(*self.mouse_position)
3061+
hover_widgets = screen.get_hover_widgets_at(*self.mouse_position)
30503062
except NoWidget:
30513063
pass
30523064
else:
3053-
if widget is not self.mouse_over:
3054-
self._set_mouse_over(widget)
3065+
mouse_over, hover_over = hover_widgets.widgets
3066+
if (
3067+
mouse_over is not self.mouse_over
3068+
or hover_over is not self.hover_over
3069+
):
3070+
self._set_mouse_over(mouse_over, hover_over)
30553071

30563072
self.call_after_refresh(check_mouse)
30573073

@@ -3641,8 +3657,9 @@ def refresh_css(self, animate: bool = True) -> None:
36413657
stylesheet.reparse()
36423658
stylesheet.update(self.app, animate=animate)
36433659
try:
3644-
self.screen._refresh_layout(self.size)
3645-
self.screen._css_update_count = self._css_update_count
3660+
if self.screen.is_mounted:
3661+
self.screen._refresh_layout(self.size)
3662+
self.screen._css_update_count = self._css_update_count
36463663
except ScreenError:
36473664
pass
36483665
# The other screens in the stack will need to know about some style

src/textual/binding.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ class Group:
9191
description: str = ""
9292
"""Description of the group."""
9393

94+
compact: bool = False
95+
"""Show keys in compact form (no spaces)."""
96+
9497
group: Group | None = None
9598
"""Optional binding group (used to group related bindings in the footer)."""
9699

src/textual/command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,7 @@ def _select_or_command(
12301230
# decide what to do with it (hopefully it'll run it).
12311231
self._cancel_gather_commands()
12321232
self.app.post_message(CommandPalette.Closed(option_selected=True))
1233-
self.app._delay_update()
1233+
self.app.delay_update()
12341234
self.dismiss()
12351235
self.app.call_later(self._selected_command.command)
12361236

src/textual/content.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,26 @@ def iter_content() -> Iterable[Content]:
869869

870870
return Content("".join(text), spans, total_cell_length)
871871

872+
def wrap(
873+
self, width: int, *, align: TextAlign = "left", overflow: TextOverflow = "fold"
874+
) -> list[Content]:
875+
"""Wrap text so that it fits within the given dimensions.
876+
877+
Note that Textual will automatically wrap Content in widgets.
878+
This method is only required if you need some additional processing to lines.
879+
880+
Args:
881+
width: Maximum width of the line (in cells).
882+
align: Alignment of lines.
883+
overflow: Overflow of lines (what happens when the text doesn't fit).
884+
885+
Returns:
886+
A list of Content objects, one per line.
887+
"""
888+
lines = self._wrap_and_format(width, align, overflow)
889+
content_lines = [line.content for line in lines]
890+
return content_lines
891+
872892
def get_style_at_offset(self, offset: int) -> Style:
873893
"""Get the style of a character at give offset.
874894

src/textual/css/_style_properties.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,12 @@ def __set__(self, obj: StylesBase, value: EnumType | None = None):
841841
children=self._refresh_children,
842842
parent=self._refresh_parent,
843843
)
844+
845+
if self._display:
846+
node = obj.node
847+
if node is not None and node.parent:
848+
node._nodes.updated()
849+
844850
else:
845851
if value not in self._valid_values:
846852
raise StyleValueError(

src/textual/design.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def luminosity_range(spread: float) -> Iterable[tuple[str, float]]:
251251
"block-cursor-blurred-text-style", "none"
252252
)
253253
colors["block-hover-background"] = get(
254-
"block-hover-background", boost.with_alpha(0.05).hex
254+
"block-hover-background", boost.with_alpha(0.1).hex
255255
)
256256

257257
# The border color for focused widgets which have a border.

src/textual/layouts/horizontal.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ def arrange(
8383
children, box_models, margins
8484
):
8585
styles = widget.styles
86-
8786
overlay = styles.overlay == "screen"
8887
offset = (
8988
styles.offset.resolve(

0 commit comments

Comments
 (0)