Skip to content

Commit bda91a4

Browse files
authored
Merge pull request #6051 from Textualize/title-format
Easier customization of Header title
2 parents 8d78427 + cca7fdd commit bda91a4

File tree

7 files changed

+228
-57
lines changed

7 files changed

+228
-57
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2020
- Added `OptionList.set_options` https://github.com/Textualize/textual/pull/6048
2121
- Added `TextArea.suggestion` https://github.com/Textualize/textual/pull/6048
2222
- Added `TextArea.placeholder` https://github.com/Textualize/textual/pull/6048
23+
- Added `Header.format_title` and `App.format_title` for easier customization of title in the Header https://github.com/Textualize/textual/pull/6051
2324
- Added `Widget.get_line_filters` and `App.get_line_filters` https://github.com/Textualize/textual/pull/6057
2425

2526
### Changed
2627

2728
- Breaking change: The `renderable` property on the `Static` widget has been changed to `content`. https://github.com/Textualize/textual/pull/6041
29+
- Breaking change: `HeaderTitle` widget is now a static, with no `text` and `sub_text` reactives https://github.com/Textualize/textual/pull/6051
2830
- Breaking change: Renamed `Label` constructor argument `renderable` to `content` for consistency https://github.com/Textualize/textual/pull/6045
2931
- Breaking change: Optimization to line API to avoid applying background styles to widget content. In practice this means that you can no longer rely on blank Segments automatically getting the background color.
3032

src/textual/app.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
from textual.binding import Binding, BindingsMap, BindingType, Keymap
9595
from textual.command import CommandListItem, CommandPalette, Provider, SimpleProvider
9696
from textual.compose import compose
97+
from textual.content import Content
9798
from textual.css.errors import StylesheetError
9899
from textual.css.query import NoMatches
99100
from textual.css.stylesheet import RulesMap, Stylesheet
@@ -971,6 +972,27 @@ def clipboard(self) -> str:
971972
"""
972973
return self._clipboard
973974

975+
def format_title(self, title: str, sub_title: str) -> Content:
976+
"""Format the title for display.
977+
978+
Args:
979+
title: The title.
980+
sub_title: The sub title.
981+
982+
Returns:
983+
Content instance with title and subtitle.
984+
"""
985+
title_content = Content(title)
986+
sub_title_content = Content(sub_title)
987+
if sub_title_content:
988+
return Content.assemble(
989+
title_content,
990+
(" — ", "dim"),
991+
sub_title_content.stylize("dim"),
992+
)
993+
else:
994+
return title_content
995+
974996
@contextmanager
975997
def batch_update(self) -> Generator[None, None, None]:
976998
"""A context manager to suspend all repaints until the end of the batch."""

src/textual/widgets/_header.py

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
from rich.text import Text
88

99
from textual.app import RenderResult
10+
from textual.content import Content
1011
from textual.dom import NoScreen
1112
from textual.events import Click, Mount
1213
from textual.reactive import Reactive
1314
from textual.widget import Widget
15+
from textual.widgets import Static
1416

1517

1618
class HeaderIcon(Widget):
@@ -98,34 +100,18 @@ def render(self) -> RenderResult:
98100
return Text(datetime.now().time().strftime(self.time_format))
99101

100102

101-
class HeaderTitle(Widget):
103+
class HeaderTitle(Static):
102104
"""Display the title / subtitle in the header."""
103105

104106
DEFAULT_CSS = """
105107
HeaderTitle {
108+
text-wrap: nowrap;
109+
text-overflow: ellipsis;
106110
content-align: center middle;
107111
width: 100%;
108112
}
109113
"""
110114

111-
text: Reactive[str] = Reactive("")
112-
"""The main title text."""
113-
114-
sub_text = Reactive("")
115-
"""The sub-title text."""
116-
117-
def render(self) -> RenderResult:
118-
"""Render the title and sub-title.
119-
120-
Returns:
121-
The value to render.
122-
"""
123-
text = Text(self.text, no_wrap=True, overflow="ellipsis")
124-
if self.sub_text:
125-
text.append(" — ")
126-
text.append(self.sub_text, "dim")
127-
return text
128-
129115

130116
class Header(Widget):
131117
"""A header widget with icon and clock."""
@@ -196,6 +182,17 @@ def watch_tall(self, tall: bool) -> None:
196182
def _on_click(self):
197183
self.toggle_class("-tall")
198184

185+
def format_title(self) -> Content:
186+
"""Format the title and subtitle.
187+
188+
Defers to [App.format_title][textual.app.App.format_title] by default.
189+
Override this method if you want to customize how the title is displayed in the header.
190+
191+
Returns:
192+
Content for title display.
193+
"""
194+
return self.app.format_title(self.screen_title, self.screen_sub_title)
195+
199196
@property
200197
def screen_title(self) -> str:
201198
"""The title that this header will display.
@@ -221,17 +218,11 @@ def screen_sub_title(self) -> str:
221218
def _on_mount(self, _: Mount) -> None:
222219
async def set_title() -> None:
223220
try:
224-
self.query_one(HeaderTitle).text = self.screen_title
225-
except NoScreen:
226-
pass
227-
228-
async def set_sub_title() -> None:
229-
try:
230-
self.query_one(HeaderTitle).sub_text = self.screen_sub_title
221+
self.query_one(HeaderTitle).update(self.format_title())
231222
except NoScreen:
232223
pass
233224

234225
self.watch(self.app, "title", set_title)
235-
self.watch(self.app, "sub_title", set_sub_title)
226+
self.watch(self.app, "sub_title", set_title)
236227
self.watch(self.screen, "title", set_title)
237-
self.watch(self.screen, "sub_title", set_sub_title)
228+
self.watch(self.screen, "sub_title", set_title)
Lines changed: 151 additions & 0 deletions
Loading

tests/snapshot_tests/snapshot_apps/fr_with_min.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class ScreenSplitApp(App[None]):
2525
background: $panel;
2626
}
2727
28-
Static {
28+
#scroll1 Static, #scroll2 Static {
2929
width: 1fr;
3030
content-align: center middle;
3131
background: $boost;

tests/snapshot_tests/test_snapshots.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4610,3 +4610,19 @@ def compose(self) -> ComposeResult:
46104610
yield TextArea(placeholder="Your text here")
46114611

46124612
assert snap_compare(TextApp())
4613+
4614+
4615+
def test_header_format(snap_compare):
4616+
"""Test title and sub-title are formatted as expected.
4617+
4618+
You should see "Title - Sub-title" in the header. Where sub-title is dimmed.
4619+
"""
4620+
4621+
class HeaderApp(App):
4622+
TITLE = "Title"
4623+
SUB_TITLE = "Sub-title"
4624+
4625+
def compose(self) -> ComposeResult:
4626+
yield Header()
4627+
4628+
assert snap_compare(HeaderApp())

0 commit comments

Comments
 (0)