Merge pull request #6138 from Textualize/long-placeholder

long placeholder
This commit is contained in:
Will McGugan
2025-09-28 17:26:37 +01:00
committed by GitHub
6 changed files with 236 additions and 17 deletions

View File

@@ -24,11 +24,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `Screen.size` https://github.com/Textualize/textual/pull/6105
- Added `compact` to Binding.Group https://github.com/Textualize/textual/pull/6132
- Added `Screen.get_hover_widgets_at` https://github.com/Textualize/textual/pull/6132
- Added `Content.wrap` https://github.com/Textualize/textual/pull/6138
### Fixed
- Fixed issue where Segments with a style of `None` aren't rendered https://github.com/Textualize/textual/pull/6109
- Fixed visual glitches and crash when changing `DataTable.header_height` https://github.com/Textualize/textual/pull/6128
- Fixed TextArea.placeholder not handling multi-lines https://github.com/Textualize/textual/pull/6138
## [6.1.0] - 2025-08-01

View File

@@ -869,6 +869,26 @@ class Content(Visual):
return Content("".join(text), spans, total_cell_length)
def wrap(
self, width: int, *, align: TextAlign = "left", overflow: TextOverflow = "fold"
) -> list[Content]:
"""Wrap text so that it fits within the given dimensions.
Note that Textual will automatically wrap Content in widgets.
This method is only required if you need some additional processing to lines.
Args:
width: Maximum width of the line (in cells).
align: Alignment of lines.
overflow: Overflow of lines (what happens when the text doesn't fit).
Returns:
A list of Content objects, one per line.
"""
lines = self._wrap_and_format(width, align, overflow)
content_lines = [line.content for line in lines]
return content_lines
def get_style_at_offset(self, offset: int) -> Style:
"""Get the style of a character at give offset.

View File

@@ -1206,24 +1206,24 @@ TextArea {
Returns:
A rendered line.
"""
if y == 0 and not self.text and self.placeholder:
style = self.get_visual_style("text-area--placeholder")
content = (
Content(self.placeholder)
if isinstance(self.placeholder, str)
else self.placeholder
)
content = content.stylize(style)
if self._draw_cursor:
theme = self._theme
cursor_style = theme.cursor_style if theme else None
if cursor_style:
content = content.stylize(
ContentStyle.from_rich_style(cursor_style), 0, 1
)
return Strip(
content.render_segments(self.visual_style), content.cell_length
if not self.text and self.placeholder:
placeholder_lines = Content.from_text(self.placeholder).wrap(
self.content_size.width
)
if y < len(placeholder_lines):
style = self.get_visual_style("text-area--placeholder")
content = placeholder_lines[y].stylize(style)
if self._draw_cursor and y == 0:
theme = self._theme
cursor_style = theme.cursor_style if theme else None
if cursor_style:
content = content.stylize(
ContentStyle.from_rich_style(cursor_style), 0, 1
)
return Strip(
content.render_segments(self.visual_style), content.cell_length
)
scroll_x, scroll_y = self.scroll_offset
absolute_y = scroll_y + y

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -4626,3 +4626,31 @@ def test_header_format(snap_compare):
yield Header()
assert snap_compare(HeaderApp())
def test_long_textarea_placeholder(snap_compare) -> None:
"""Test multi-line placeholders are wrapped and rendered.
You should see a TextArea at 50% width, with several lines of wrapped text.
"""
TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""
class PlaceholderApp(App):
CSS = """
TextArea {
width: 50%;
}
"""
def compose(self) -> ComposeResult:
yield TextArea(placeholder=TEXT)
assert snap_compare(PlaceholderApp())

View File

@@ -349,3 +349,19 @@ def test_add_spans() -> None:
Span(7, 9, style="blue"),
]
assert content.spans == expected
def test_wrap() -> None:
content = Content.from_markup("[green]Hello, [b]World, One two three[/b]")
wrapped = content.wrap(6)
print(wrapped)
expected = [
Content("Hello,", spans=[Span(0, 6, style="green")]),
Content("World,", spans=[Span(0, 6, style="green"), Span(0, 6, style="b")]),
Content("One", spans=[Span(0, 3, style="green"), Span(0, 3, style="b")]),
Content("two", spans=[Span(0, 3, style="green"), Span(0, 3, style="b")]),
Content("three", spans=[Span(0, 5, style="green"), Span(0, 5, style="b")]),
]
assert len(wrapped) == len(expected)
for line1, line2 in zip(wrapped, expected):
assert line1.is_same(line2)