Skip to content

Commit 2a810f8

Browse files
Implement border (sub)title. (#2064)
* Add Widget.border_title and border_subtitle. Related issues: #1864 * Test setting border_(sub)title. * Add border (sub)title references to StylesCache. These internal references will make it easier for the instance of 'StylesCache' to know which border (sub)title to use, if/when needed. * Add method to render border label. * Add styles to align border (sub)title. * Render border labels. * Update styles template. * Make new 'render_row' parameters optional. * Add (sub)title border snapshot tests. * Document border (sub)title and styles. * Pass (sub)title directly as arguments. Get rid of the watchers to make data flow easier to follow. Related comment: https://github.com/Textualize/textual/pull/2064/files\#r1137746697 * Tweak example. * Fix render_border_label. This was wrong because border labels can be composed of multiple segments if they contain multiple styles. Additionally, we want to render a single blank space of padding around the title. * Ensure we get no label when there's no space. * Add tests for border label rendering. * 'render_border_label' now returns iterable of segments. * Add label to render_row. * Fix calling signature in tests. * Add padding to snapshot tests. * Fix changelog. * Update snapshot tests. * Update snapshot tests. * Border labels expand if there's no corners. * Update CHANGELOG.md * Fix docs. * Remove irrelevant line. * Fix snapshot tests. * Don't share Console among tests. * Simplify example in styles guide. * Avoid expensive function call when possible. * rewording * positive branch first * remove wasteful indirection * fix changelog --------- Co-authored-by: Will McGugan <[email protected]>
1 parent 2969273 commit 2a810f8

24 files changed

+1370
-67
lines changed

CHANGELOG.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
99

1010
### Added
1111
- Added `parser_factory` argument to `Markdown` and `MarkdownViewer` constructors https://github.com/Textualize/textual/pull/2075
12+
- Added `HorizontalScroll` https://github.com/Textualize/textual/issues/1957
13+
- Added `Center` https://github.com/Textualize/textual/issues/1957
14+
- Added `Middle` https://github.com/Textualize/textual/issues/1957
15+
- Added `VerticalScroll` (mimicking the old behaviour of `Vertical`) https://github.com/Textualize/textual/issues/1957
16+
- Added `Widget.border_title` and `Widget.border_subtitle` to set border (sub)title for a widget https://github.com/Textualize/textual/issues/1864
17+
- Added CSS styles `border_title_align` and `border_subtitle_align`.
18+
- Added `TabbedContent` widget https://github.com/Textualize/textual/pull/2059
19+
- Added `get_child_by_type` method to widgets / app https://github.com/Textualize/textual/pull/2059
20+
- Added `Widget.render_str` method https://github.com/Textualize/textual/pull/2059
21+
- Added TEXTUAL_DRIVER environment variable
1222

1323
### Changed
1424

@@ -27,18 +37,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2737
- Fixed borders not rendering correctly. https://github.com/Textualize/textual/pull/2074
2838
- Fix for error when removing nodes. https://github.com/Textualize/textual/issues/2079
2939

30-
### Added
31-
32-
- Added `HorizontalScroll` https://github.com/Textualize/textual/issues/1957
33-
- Added `Center` https://github.com/Textualize/textual/issues/1957
34-
- Added `Middle` https://github.com/Textualize/textual/issues/1957
35-
- Added `VerticalScroll` (mimicking the old behaviour of `Vertical`) https://github.com/Textualize/textual/issues/1957
36-
- Added `TabbedContent` widget https://github.com/Textualize/textual/pull/2059
37-
- Added `get_child_by_type` method to widgets / app https://github.com/Textualize/textual/pull/2059
38-
- Added `Widget.render_str` method https://github.com/Textualize/textual/pull/2059
39-
- Added TEXTUAL_DRIVER environment variable
40-
41-
4240
## [0.15.1] - 2023-03-14
4341

4442
### Fixed

docs/examples/guide/styles/border01.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from textual.app import App, ComposeResult
2-
from textual.widgets import Static
3-
2+
from textual.widgets import Label
43

54
TEXT = """I must not fear.
65
Fear is the mind-killer.
@@ -13,7 +12,7 @@
1312

1413
class BorderApp(App):
1514
def compose(self) -> ComposeResult:
16-
self.widget = Static(TEXT)
15+
self.widget = Label(TEXT)
1716
yield self.widget
1817

1918
def on_mount(self) -> None:
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from textual.app import App, ComposeResult
2+
from textual.widgets import Static
3+
4+
TEXT = """I must not fear.
5+
Fear is the mind-killer.
6+
Fear is the little-death that brings total obliteration.
7+
I will face my fear.
8+
I will permit it to pass over me and through me.
9+
And when it has gone past, I will turn the inner eye to see its path.
10+
Where the fear has gone there will be nothing. Only I will remain."""
11+
12+
13+
class BorderTitleApp(App[None]):
14+
def compose(self) -> ComposeResult:
15+
self.widget = Static(TEXT)
16+
yield self.widget
17+
18+
def on_mount(self) -> None:
19+
self.widget.styles.background = "darkblue"
20+
self.widget.styles.width = "50%"
21+
self.widget.styles.border = ("heavy", "yellow")
22+
self.widget.border_title = "Litany Against Fear"
23+
self.widget.border_subtitle = "by Frank Herbert, in “Dune”"
24+
self.widget.styles.border_title_align = "center"
25+
26+
27+
if __name__ == "__main__":
28+
app = BorderTitleApp()
29+
app.run()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
Grid {
2+
grid-size: 3 3;
3+
align: center middle;
4+
}
5+
6+
Container {
7+
width: 100%;
8+
height: 100%;
9+
align: center middle;
10+
}
11+
12+
#lbl1 { /* (1)! */
13+
border: vkey $secondary;
14+
}
15+
16+
#lbl2 { /* (2)! */
17+
border: round $secondary;
18+
border-title-align: right;
19+
border-subtitle-align: right;
20+
}
21+
22+
#lbl3 {
23+
border: wide $secondary;
24+
border-title-align: center;
25+
border-subtitle-align: center;
26+
}
27+
28+
#lbl4 {
29+
border: ascii $success;
30+
border-title-align: center; /* (3)! */
31+
border-subtitle-align: left;
32+
}
33+
34+
#lbl5 { /* (4)! */
35+
/* No border = no (sub)title. */
36+
border: none $success;
37+
border-title-align: center;
38+
border-subtitle-align: center;
39+
}
40+
41+
#lbl6 { /* (5)! */
42+
border-top: solid $success;
43+
border-bottom: solid $success;
44+
}
45+
46+
#lbl7 { /* (6)! */
47+
border-top: solid $error;
48+
border-bottom: solid $error;
49+
padding: 1 2;
50+
border-subtitle-align: left;
51+
}
52+
53+
#lbl8 {
54+
border-top: solid $error;
55+
border-bottom: solid $error;
56+
border-title-align: center;
57+
border-subtitle-align: center;
58+
}
59+
60+
#lbl9 {
61+
border-top: solid $error;
62+
border-bottom: solid $error;
63+
border-title-align: right;
64+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from textual.app import App
2+
from textual.containers import Container, Grid
3+
from textual.widgets import Label
4+
5+
6+
def make_label_container( # (11)!
7+
text: str, id: str, border_title: str, border_subtitle: str
8+
) -> Container:
9+
lbl = Label(text, id=id)
10+
lbl.border_title = border_title
11+
lbl.border_subtitle = border_subtitle
12+
return Container(lbl)
13+
14+
15+
class BorderSubTitleAlignAll(App[None]):
16+
def compose(self):
17+
with Grid():
18+
yield make_label_container( # (1)!
19+
"This is the story of",
20+
"lbl1",
21+
"[b]Border [i]title[/i][/]",
22+
"[u][r]Border[/r] subtitle[/]",
23+
)
24+
yield make_label_container( # (2)!
25+
"a Python",
26+
"lbl2",
27+
"[b red]Left, but it's loooooooooooong",
28+
"[reverse]Center, but it's loooooooooooong",
29+
)
30+
yield make_label_container( # (3)!
31+
"developer that",
32+
"lbl3",
33+
"[b i on purple]Left[/]",
34+
"[r u white on black]@@@[/]",
35+
)
36+
yield make_label_container(
37+
"had to fill up",
38+
"lbl4",
39+
"", # (4)!
40+
"[link=https://textual.textualize.io]Left[/]", # (5)!
41+
)
42+
yield make_label_container( # (6)!
43+
"nine labels", "lbl5", "Title", "Subtitle"
44+
)
45+
yield make_label_container( # (7)!
46+
"and ended up redoing it",
47+
"lbl6",
48+
"Title",
49+
"Subtitle",
50+
)
51+
yield make_label_container( # (8)!
52+
"because the first try",
53+
"lbl7",
54+
"Title, but really loooooooooong!",
55+
"Subtitle, but really loooooooooong!",
56+
)
57+
yield make_label_container( # (9)!
58+
"had some labels",
59+
"lbl8",
60+
"Title, but really loooooooooong!",
61+
"Subtitle, but really loooooooooong!",
62+
)
63+
yield make_label_container( # (10)!
64+
"that were too long.",
65+
"lbl9",
66+
"Title, but really loooooooooong!",
67+
"Subtitle, but really loooooooooong!",
68+
)
69+
70+
71+
app = BorderSubTitleAlignAll(css_path="border_sub_title_align_all.css")
72+
73+
if __name__ == "__main__":
74+
app.run()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#label1 {
2+
border: solid $secondary;
3+
border-subtitle-align: left;
4+
}
5+
6+
#label2 {
7+
border: dashed $secondary;
8+
border-subtitle-align: center;
9+
}
10+
11+
#label3 {
12+
border: tall $secondary;
13+
border-subtitle-align: right;
14+
}
15+
16+
Screen > Label {
17+
width: 100%;
18+
height: 5;
19+
content-align: center middle;
20+
color: white;
21+
margin: 1;
22+
box-sizing: border-box;
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from textual.app import App
2+
from textual.widgets import Label
3+
4+
5+
class BorderSubtitleAlignApp(App):
6+
def compose(self):
7+
lbl = Label("My subtitle is on the left.", id="label1")
8+
lbl.border_subtitle = "< Left"
9+
yield lbl
10+
11+
lbl = Label("My subtitle is centered", id="label2")
12+
lbl.border_subtitle = "Centered!"
13+
yield lbl
14+
15+
lbl = Label("My subtitle is on the right", id="label3")
16+
lbl.border_subtitle = "Right >"
17+
yield lbl
18+
19+
20+
app = BorderSubtitleAlignApp(css_path="border_subtitle_align.css")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#label1 {
2+
border: solid $secondary;
3+
border-title-align: left;
4+
}
5+
6+
#label2 {
7+
border: dashed $secondary;
8+
border-title-align: center;
9+
}
10+
11+
#label3 {
12+
border: tall $secondary;
13+
border-title-align: right;
14+
}
15+
16+
Screen > Label {
17+
width: 100%;
18+
height: 5;
19+
content-align: center middle;
20+
color: white;
21+
margin: 1;
22+
box-sizing: border-box;
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from textual.app import App
2+
from textual.widgets import Label
3+
4+
5+
class BorderTitleAlignApp(App):
6+
def compose(self):
7+
lbl = Label("My title is on the left.", id="label1")
8+
lbl.border_title = "< Left"
9+
yield lbl
10+
11+
lbl = Label("My title is centered", id="label2")
12+
lbl.border_title = "Centered!"
13+
yield lbl
14+
15+
lbl = Label("My title is on the right", id="label3")
16+
lbl.border_title = "Right >"
17+
yield lbl
18+
19+
20+
app = BorderTitleAlignApp(css_path="border_title_align.css")

docs/guide/styles.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,28 @@ There are many other border types. Run the following from the command prompt to
257257
textual borders
258258
```
259259

260+
261+
#### Title alignment
262+
263+
Widgets have two attributes, `border_title` and `border_subtitle` which (if set) will be displayed within the border.
264+
The `border_title` attribute is displayed in the top border, and `border_subtitle` is displayed in the bottom border.
265+
266+
There are two styles to set the alignment of these border labels, which may be set to "left", "right", or "center".
267+
268+
- [`border-title-align`](../styles/border_title_align.md) sets the alignment of the title, which defaults to "left".
269+
- [`border-subtitle-align`](../styles/border_subtitle_align.md) sets the alignment of the subtitle, which defaults to "right".
270+
271+
The following example sets both titles and changes the alignment of the title (top) to "center".
272+
273+
```py hl_lines="22-24"
274+
--8<-- "docs/examples/guide/styles/border_title.py"
275+
```
276+
277+
Note the addition of the titles and their alignments:
278+
279+
```{.textual path="docs/examples/guide/styles/border_title.py"}
280+
```
281+
260282
### Outline
261283

262284
[Outline](../styles/outline.md) is similar to border and is set in the same way. The difference is that outline will not change the size of the widget, and may overlap the content area. The following example sets an outline on a widget:

0 commit comments

Comments
 (0)