Skip to content

Commit a0be460

Browse files
authored
border classvars (#3097)
* border classvars * changelog * copy * remove whitespace * copy
1 parent 156e4c8 commit a0be460

File tree

6 files changed

+130
-2
lines changed

6 files changed

+130
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1616
- `MouseMove` events bubble up from widgets. `App` and `Screen` receive `MouseMove` events even if there's no Widget under the cursor. https://github.com/Textualize/textual/issues/2905
1717

1818
### Added
19-
- Added an interface for replacing prompt of an individual option in an `OptionList` https://github.com/Textualize/textual/issues/2603
19+
- Added an interface for replacing prompt of an individual option in an `OptionList` https://github.com/Textualize/textual/issues/2603
2020
- Added `DirectoryTree.reload_node` method https://github.com/Textualize/textual/issues/2757
2121
- Added widgets.Digit https://github.com/Textualize/textual/pull/3073
22+
- Added `BORDER_TITLE` and `BORDER_SUBTITLE` classvars to Widget https://github.com/Textualize/textual/pull/3097
2223

2324
## [0.32.0] - 2023-08-03
2425

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Screen {
2+
align: center middle;
3+
}
4+
5+
Hello {
6+
width: 40;
7+
height: 9;
8+
padding: 1 2;
9+
background: $panel;
10+
border: $secondary tall;
11+
content-align: center middle;
12+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from itertools import cycle
2+
3+
from textual.app import App, ComposeResult
4+
from textual.widgets import Static
5+
6+
hellos = cycle(
7+
[
8+
"Hola",
9+
"Bonjour",
10+
"Guten tag",
11+
"Salve",
12+
"Nǐn hǎo",
13+
"Olá",
14+
"Asalaam alaikum",
15+
"Konnichiwa",
16+
"Anyoung haseyo",
17+
"Zdravstvuyte",
18+
"Hello",
19+
]
20+
)
21+
22+
23+
class Hello(Static):
24+
"""Display a greeting."""
25+
26+
BORDER_TITLE = "Hello Widget" # (1)!
27+
28+
def on_mount(self) -> None:
29+
self.action_next_word()
30+
self.border_subtitle = "Click for next hello" # (2)!
31+
32+
def action_next_word(self) -> None:
33+
"""Get a new hello and update the content area."""
34+
hello = next(hellos)
35+
self.update(f"[@click='next_word']{hello}[/], [b]World[/b]!")
36+
37+
38+
class CustomApp(App):
39+
CSS_PATH = "hello05.css"
40+
41+
def compose(self) -> ComposeResult:
42+
yield Hello()
43+
44+
45+
if __name__ == "__main__":
46+
app = CustomApp()
47+
app.run()

docs/guide/widgets.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,47 @@ Let's use markup links in the hello example so that the greeting becomes a link
142142
If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the `next_word` action which updates the next word.
143143

144144

145+
## Border titles
146+
147+
Every widget has a [`border_title`][textual.widgets.Widget.border_title] and [`border_subtitle`][textual.widgets.Widget.border_subtitle] attribute.
148+
Setting `border_title` will display text within the top border, and setting `border_subtitle` will display text within the bottom border.
149+
150+
!!! note
151+
152+
Border titles will only display if the widget has a [border](../styles/border.md) enabled.
153+
154+
The default value for these attributes is empty string, which disables the title.
155+
You can change the default value for the title attributes with the [`BORDER_TITLE`][textual.widget.Widget.BORDER_TITLE] and [`BORDER_SUBTITLE`][textual.widget.Widget.BORDER_SUBTITLE] class variables.
156+
157+
Let's demonstrate setting a title, both as a class variable and a instance variable:
158+
159+
160+
=== "hello06.py"
161+
162+
```python title="hello06.py" hl_lines="26 30"
163+
--8<-- "docs/examples/guide/widgets/hello06.py"
164+
```
165+
166+
1. Setting the default for the `title` attribute via class variable.
167+
2. Setting `subtitle` via an instance attribute.
168+
169+
=== "hello06.css"
170+
171+
```sass title="hello06.css"
172+
--8<-- "docs/examples/guide/widgets/hello06.css"
173+
```
174+
175+
=== "Output"
176+
177+
```{.textual path="docs/examples/guide/widgets/hello06.py"}
178+
```
179+
180+
Note that titles are limited to a single line of text.
181+
If the supplied text is too long to fit within the widget, it will be cropped (and an ellipsis added).
182+
183+
There are a number of styles that influence how titles are displayed (color and alignment).
184+
See the [style reference](../styles/index.md) for details.
185+
145186
## Rich renderables
146187

147188
In previous examples we've set strings as content for Widgets. You can also use special objects called [renderables](https://rich.readthedocs.io/en/latest/protocol.html) for advanced visuals. You can use any renderable defined in [Rich](https://github.com/Textualize/rich) or third party libraries.

src/textual/widget.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
from ._arrange import DockArrangeResult, arrange
4646
from ._asyncio import create_task
4747
from ._cache import FIFOCache
48-
from ._callback import invoke
4948
from ._compose import compose
5049
from ._context import NoActiveAppError, active_app
5150
from ._easing import DEFAULT_SCROLL_EASING
@@ -256,6 +255,12 @@ class Widget(DOMNode):
256255
"""
257256
COMPONENT_CLASSES: ClassVar[set[str]] = set()
258257

258+
BORDER_TITLE: ClassVar[str] = ""
259+
"""Initial value for border_title attribute."""
260+
261+
BORDER_SUBTITLE: ClassVar[str] = ""
262+
"""Initial value for border_subtitle attribute."""
263+
259264
can_focus: bool = False
260265
"""Widget may receive focus."""
261266
can_focus_children: bool = True
@@ -349,6 +354,10 @@ def __init__(
349354

350355
self._add_children(*children)
351356
self.disabled = disabled
357+
if self.BORDER_TITLE:
358+
self.border_title = self.BORDER_TITLE
359+
if self.BORDER_SUBTITLE:
360+
self.border_subtitle = self.BORDER_SUBTITLE
352361

353362
virtual_size: Reactive[Size] = Reactive(Size(0, 0), layout=True)
354363
"""The virtual (scrollable) [size][textual.geometry.Size] of the widget."""

tests/test_border_subtitle.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from textual.app import App, ComposeResult
2+
from textual.widget import Widget
3+
4+
5+
async def test_border_subtitle():
6+
class BorderWidget(Widget):
7+
BORDER_TITLE = "foo"
8+
BORDER_SUBTITLE = "bar"
9+
10+
class SimpleApp(App):
11+
def compose(self) -> ComposeResult:
12+
yield BorderWidget()
13+
14+
empty_app = SimpleApp()
15+
async with empty_app.run_test() as pilot:
16+
widget = empty_app.query_one(BorderWidget)
17+
assert widget.border_title == "foo"
18+
assert widget.border_subtitle == "bar"

0 commit comments

Comments
 (0)