mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Easier customization of Header title
This commit is contained in:
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added `OptionList.set_options` https://github.com/Textualize/textual/pull/6048
|
||||
- Added `TextArea.suggestion` https://github.com/Textualize/textual/pull/6048
|
||||
- Added `TextArea.placeholder` https://github.com/Textualize/textual/pull/6048
|
||||
- Added `Header.format_title` and `App.format_title` for easier customization of title in the Header
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ from textual.await_remove import AwaitRemove
|
||||
from textual.binding import Binding, BindingsMap, BindingType, Keymap
|
||||
from textual.command import CommandListItem, CommandPalette, Provider, SimpleProvider
|
||||
from textual.compose import compose
|
||||
from textual.content import Content
|
||||
from textual.css.errors import StylesheetError
|
||||
from textual.css.query import NoMatches
|
||||
from textual.css.stylesheet import RulesMap, Stylesheet
|
||||
@@ -963,6 +964,23 @@ class App(Generic[ReturnType], DOMNode):
|
||||
"""
|
||||
return self._clipboard
|
||||
|
||||
def format_title(self, title: str, sub_title: str) -> Content:
|
||||
"""Format the title for display.
|
||||
|
||||
Args:
|
||||
title: The title.
|
||||
sub_title: The sub title.
|
||||
|
||||
Returns:
|
||||
Content instance with title and subtitle.
|
||||
"""
|
||||
title_content = Content(title)
|
||||
sub_title_content = Content(sub_title)
|
||||
if sub_title_content:
|
||||
return Content.assemble(title_content, " — ", sub_title_content)
|
||||
else:
|
||||
return title_content
|
||||
|
||||
@contextmanager
|
||||
def batch_update(self) -> Generator[None, None, None]:
|
||||
"""A context manager to suspend all repaints until the end of the batch."""
|
||||
|
||||
@@ -7,10 +7,12 @@ from datetime import datetime
|
||||
from rich.text import Text
|
||||
|
||||
from textual.app import RenderResult
|
||||
from textual.content import Content
|
||||
from textual.dom import NoScreen
|
||||
from textual.events import Click, Mount
|
||||
from textual.reactive import Reactive
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class HeaderIcon(Widget):
|
||||
@@ -98,34 +100,18 @@ class HeaderClock(HeaderClockSpace):
|
||||
return Text(datetime.now().time().strftime(self.time_format))
|
||||
|
||||
|
||||
class HeaderTitle(Widget):
|
||||
class HeaderTitle(Static):
|
||||
"""Display the title / subtitle in the header."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
HeaderTitle {
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
content-align: center middle;
|
||||
width: 100%;
|
||||
}
|
||||
"""
|
||||
|
||||
text: Reactive[str] = Reactive("")
|
||||
"""The main title text."""
|
||||
|
||||
sub_text = Reactive("")
|
||||
"""The sub-title text."""
|
||||
|
||||
def render(self) -> RenderResult:
|
||||
"""Render the title and sub-title.
|
||||
|
||||
Returns:
|
||||
The value to render.
|
||||
"""
|
||||
text = Text(self.text, no_wrap=True, overflow="ellipsis")
|
||||
if self.sub_text:
|
||||
text.append(" — ")
|
||||
text.append(self.sub_text, "dim")
|
||||
return text
|
||||
|
||||
|
||||
class Header(Widget):
|
||||
"""A header widget with icon and clock."""
|
||||
@@ -196,6 +182,17 @@ class Header(Widget):
|
||||
def _on_click(self):
|
||||
self.toggle_class("-tall")
|
||||
|
||||
def format_title(self) -> Content:
|
||||
"""Format the title and subtitle.
|
||||
|
||||
Defers to [App.format_title][textual.app.App.format_title] by default.
|
||||
Override this method if you want to customize how the title is displayed in the header.
|
||||
|
||||
Returns:
|
||||
Content for title display.
|
||||
"""
|
||||
return self.app.format_title(self.screen_title, self.screen_sub_title)
|
||||
|
||||
@property
|
||||
def screen_title(self) -> str:
|
||||
"""The title that this header will display.
|
||||
@@ -221,17 +218,11 @@ class Header(Widget):
|
||||
def _on_mount(self, _: Mount) -> None:
|
||||
async def set_title() -> None:
|
||||
try:
|
||||
self.query_one(HeaderTitle).text = self.screen_title
|
||||
except NoScreen:
|
||||
pass
|
||||
|
||||
async def set_sub_title() -> None:
|
||||
try:
|
||||
self.query_one(HeaderTitle).sub_text = self.screen_sub_title
|
||||
self.query_one(HeaderTitle).update(self.format_title())
|
||||
except NoScreen:
|
||||
pass
|
||||
|
||||
self.watch(self.app, "title", set_title)
|
||||
self.watch(self.app, "sub_title", set_sub_title)
|
||||
self.watch(self.app, "sub_title", set_title)
|
||||
self.watch(self.screen, "title", set_title)
|
||||
self.watch(self.screen, "sub_title", set_sub_title)
|
||||
self.watch(self.screen, "sub_title", set_title)
|
||||
|
||||
@@ -25,7 +25,7 @@ class ScreenSplitApp(App[None]):
|
||||
background: $panel;
|
||||
}
|
||||
|
||||
Static {
|
||||
#scroll1 Static, #scroll2 Static {
|
||||
width: 1fr;
|
||||
content-align: center middle;
|
||||
background: $boost;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from textual.app import App
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Header
|
||||
from textual.widgets import Header, Static
|
||||
|
||||
|
||||
async def test_screen_title_none_is_ignored():
|
||||
@@ -16,7 +16,7 @@ async def test_screen_title_none_is_ignored():
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
assert app.screen.query_one("HeaderTitle").text == "app title"
|
||||
assert app.screen.query_one("HeaderTitle", Static).content == "app title"
|
||||
|
||||
|
||||
async def test_screen_title_overrides_app_title():
|
||||
@@ -34,7 +34,7 @@ async def test_screen_title_overrides_app_title():
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
assert app.screen.query_one("HeaderTitle").text == "screen title"
|
||||
assert app.screen.query_one("HeaderTitle", Static).content == "screen title"
|
||||
|
||||
|
||||
async def test_screen_title_reactive_updates_title():
|
||||
@@ -54,7 +54,7 @@ async def test_screen_title_reactive_updates_title():
|
||||
async with app.run_test() as pilot:
|
||||
app.screen.title = "new screen title"
|
||||
await pilot.pause()
|
||||
assert app.screen.query_one("HeaderTitle").text == "new screen title"
|
||||
assert app.screen.query_one("HeaderTitle", Static).content == "new screen title"
|
||||
|
||||
|
||||
async def test_app_title_reactive_does_not_update_title_when_screen_title_is_set():
|
||||
@@ -74,7 +74,7 @@ async def test_app_title_reactive_does_not_update_title_when_screen_title_is_set
|
||||
async with app.run_test() as pilot:
|
||||
app.title = "new app title"
|
||||
await pilot.pause()
|
||||
assert app.screen.query_one("HeaderTitle").text == "screen title"
|
||||
assert app.screen.query_one("HeaderTitle", Static).content == "screen title"
|
||||
|
||||
|
||||
async def test_screen_sub_title_none_is_ignored():
|
||||
@@ -90,7 +90,10 @@ async def test_screen_sub_title_none_is_ignored():
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
assert app.screen.query_one("HeaderTitle").sub_text == "app sub-title"
|
||||
assert (
|
||||
app.screen.query_one("HeaderTitle", Static).content
|
||||
== "MyApp — app sub-title"
|
||||
)
|
||||
|
||||
|
||||
async def test_screen_sub_title_overrides_app_sub_title():
|
||||
@@ -108,7 +111,10 @@ async def test_screen_sub_title_overrides_app_sub_title():
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
assert app.screen.query_one("HeaderTitle").sub_text == "screen sub-title"
|
||||
assert (
|
||||
app.screen.query_one("HeaderTitle", Static).content
|
||||
== "MyApp — screen sub-title"
|
||||
)
|
||||
|
||||
|
||||
async def test_screen_sub_title_reactive_updates_sub_title():
|
||||
@@ -128,24 +134,7 @@ async def test_screen_sub_title_reactive_updates_sub_title():
|
||||
async with app.run_test() as pilot:
|
||||
app.screen.sub_title = "new screen sub-title"
|
||||
await pilot.pause()
|
||||
assert app.screen.query_one("HeaderTitle").sub_text == "new screen sub-title"
|
||||
|
||||
|
||||
async def test_app_sub_title_reactive_does_not_update_sub_title_when_screen_sub_title_is_set():
|
||||
class MyScreen(Screen):
|
||||
SUB_TITLE = "screen sub-title"
|
||||
|
||||
def compose(self):
|
||||
yield Header()
|
||||
|
||||
class MyApp(App):
|
||||
SUB_TITLE = "app sub-title"
|
||||
|
||||
def on_mount(self):
|
||||
self.push_screen(MyScreen())
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test() as pilot:
|
||||
app.sub_title = "new app sub-title"
|
||||
await pilot.pause()
|
||||
assert app.screen.query_one("HeaderTitle").sub_text == "screen sub-title"
|
||||
assert (
|
||||
app.screen.query_one("HeaderTitle", Static).content
|
||||
== "MyApp — new screen sub-title"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user