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 <willmcgugan@gmail.com>
This commit is contained in:
Rodrigo Girão Serrão
2023-03-22 11:07:38 +00:00
committed by GitHub
parent 29692736d0
commit 2a810f8c87
24 changed files with 1370 additions and 67 deletions

View File

@@ -1,6 +1,5 @@
from textual.app import App, ComposeResult
from textual.widgets import Static
from textual.widgets import Label
TEXT = """I must not fear.
Fear is the mind-killer.
@@ -13,7 +12,7 @@ Where the fear has gone there will be nothing. Only I will remain."""
class BorderApp(App):
def compose(self) -> ComposeResult:
self.widget = Static(TEXT)
self.widget = Label(TEXT)
yield self.widget
def on_mount(self) -> None:

View File

@@ -0,0 +1,29 @@
from textual.app import App, ComposeResult
from textual.widgets import Static
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 BorderTitleApp(App[None]):
def compose(self) -> ComposeResult:
self.widget = Static(TEXT)
yield self.widget
def on_mount(self) -> None:
self.widget.styles.background = "darkblue"
self.widget.styles.width = "50%"
self.widget.styles.border = ("heavy", "yellow")
self.widget.border_title = "Litany Against Fear"
self.widget.border_subtitle = "by Frank Herbert, in “Dune”"
self.widget.styles.border_title_align = "center"
if __name__ == "__main__":
app = BorderTitleApp()
app.run()

View File

@@ -0,0 +1,64 @@
Grid {
grid-size: 3 3;
align: center middle;
}
Container {
width: 100%;
height: 100%;
align: center middle;
}
#lbl1 { /* (1)! */
border: vkey $secondary;
}
#lbl2 { /* (2)! */
border: round $secondary;
border-title-align: right;
border-subtitle-align: right;
}
#lbl3 {
border: wide $secondary;
border-title-align: center;
border-subtitle-align: center;
}
#lbl4 {
border: ascii $success;
border-title-align: center; /* (3)! */
border-subtitle-align: left;
}
#lbl5 { /* (4)! */
/* No border = no (sub)title. */
border: none $success;
border-title-align: center;
border-subtitle-align: center;
}
#lbl6 { /* (5)! */
border-top: solid $success;
border-bottom: solid $success;
}
#lbl7 { /* (6)! */
border-top: solid $error;
border-bottom: solid $error;
padding: 1 2;
border-subtitle-align: left;
}
#lbl8 {
border-top: solid $error;
border-bottom: solid $error;
border-title-align: center;
border-subtitle-align: center;
}
#lbl9 {
border-top: solid $error;
border-bottom: solid $error;
border-title-align: right;
}

View File

@@ -0,0 +1,74 @@
from textual.app import App
from textual.containers import Container, Grid
from textual.widgets import Label
def make_label_container( # (11)!
text: str, id: str, border_title: str, border_subtitle: str
) -> Container:
lbl = Label(text, id=id)
lbl.border_title = border_title
lbl.border_subtitle = border_subtitle
return Container(lbl)
class BorderSubTitleAlignAll(App[None]):
def compose(self):
with Grid():
yield make_label_container( # (1)!
"This is the story of",
"lbl1",
"[b]Border [i]title[/i][/]",
"[u][r]Border[/r] subtitle[/]",
)
yield make_label_container( # (2)!
"a Python",
"lbl2",
"[b red]Left, but it's loooooooooooong",
"[reverse]Center, but it's loooooooooooong",
)
yield make_label_container( # (3)!
"developer that",
"lbl3",
"[b i on purple]Left[/]",
"[r u white on black]@@@[/]",
)
yield make_label_container(
"had to fill up",
"lbl4",
"", # (4)!
"[link=https://textual.textualize.io]Left[/]", # (5)!
)
yield make_label_container( # (6)!
"nine labels", "lbl5", "Title", "Subtitle"
)
yield make_label_container( # (7)!
"and ended up redoing it",
"lbl6",
"Title",
"Subtitle",
)
yield make_label_container( # (8)!
"because the first try",
"lbl7",
"Title, but really loooooooooong!",
"Subtitle, but really loooooooooong!",
)
yield make_label_container( # (9)!
"had some labels",
"lbl8",
"Title, but really loooooooooong!",
"Subtitle, but really loooooooooong!",
)
yield make_label_container( # (10)!
"that were too long.",
"lbl9",
"Title, but really loooooooooong!",
"Subtitle, but really loooooooooong!",
)
app = BorderSubTitleAlignAll(css_path="border_sub_title_align_all.css")
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,23 @@
#label1 {
border: solid $secondary;
border-subtitle-align: left;
}
#label2 {
border: dashed $secondary;
border-subtitle-align: center;
}
#label3 {
border: tall $secondary;
border-subtitle-align: right;
}
Screen > Label {
width: 100%;
height: 5;
content-align: center middle;
color: white;
margin: 1;
box-sizing: border-box;
}

View File

@@ -0,0 +1,20 @@
from textual.app import App
from textual.widgets import Label
class BorderSubtitleAlignApp(App):
def compose(self):
lbl = Label("My subtitle is on the left.", id="label1")
lbl.border_subtitle = "< Left"
yield lbl
lbl = Label("My subtitle is centered", id="label2")
lbl.border_subtitle = "Centered!"
yield lbl
lbl = Label("My subtitle is on the right", id="label3")
lbl.border_subtitle = "Right >"
yield lbl
app = BorderSubtitleAlignApp(css_path="border_subtitle_align.css")

View File

@@ -0,0 +1,23 @@
#label1 {
border: solid $secondary;
border-title-align: left;
}
#label2 {
border: dashed $secondary;
border-title-align: center;
}
#label3 {
border: tall $secondary;
border-title-align: right;
}
Screen > Label {
width: 100%;
height: 5;
content-align: center middle;
color: white;
margin: 1;
box-sizing: border-box;
}

View File

@@ -0,0 +1,20 @@
from textual.app import App
from textual.widgets import Label
class BorderTitleAlignApp(App):
def compose(self):
lbl = Label("My title is on the left.", id="label1")
lbl.border_title = "< Left"
yield lbl
lbl = Label("My title is centered", id="label2")
lbl.border_title = "Centered!"
yield lbl
lbl = Label("My title is on the right", id="label3")
lbl.border_title = "Right >"
yield lbl
app = BorderTitleAlignApp(css_path="border_title_align.css")