Skip to content

Commit

Permalink
Implement border (sub)title. (#2064)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
rodrigogiraoserrao and willmcgugan authored Mar 22, 2023
1 parent 2969273 commit 2a810f8
Show file tree
Hide file tree
Showing 24 changed files with 1,370 additions and 67 deletions.
22 changes: 10 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

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

### Changed

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

### Added

- Added `HorizontalScroll` https://github.com/Textualize/textual/issues/1957
- Added `Center` https://github.com/Textualize/textual/issues/1957
- Added `Middle` https://github.com/Textualize/textual/issues/1957
- Added `VerticalScroll` (mimicking the old behaviour of `Vertical`) https://github.com/Textualize/textual/issues/1957
- Added `TabbedContent` widget https://github.com/Textualize/textual/pull/2059
- Added `get_child_by_type` method to widgets / app https://github.com/Textualize/textual/pull/2059
- Added `Widget.render_str` method https://github.com/Textualize/textual/pull/2059
- Added TEXTUAL_DRIVER environment variable


## [0.15.1] - 2023-03-14

### Fixed
Expand Down
5 changes: 2 additions & 3 deletions docs/examples/guide/styles/border01.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -13,7 +12,7 @@

class BorderApp(App):
def compose(self) -> ComposeResult:
self.widget = Static(TEXT)
self.widget = Label(TEXT)
yield self.widget

def on_mount(self) -> None:
Expand Down
29 changes: 29 additions & 0 deletions docs/examples/guide/styles/border_title.py
Original file line number Diff line number Diff line change
@@ -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()
64 changes: 64 additions & 0 deletions docs/examples/styles/border_sub_title_align_all.css
Original file line number Diff line number Diff line change
@@ -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;
}
74 changes: 74 additions & 0 deletions docs/examples/styles/border_sub_title_align_all.py
Original file line number Diff line number Diff line change
@@ -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()
23 changes: 23 additions & 0 deletions docs/examples/styles/border_subtitle_align.css
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 20 additions & 0 deletions docs/examples/styles/border_subtitle_align.py
Original file line number Diff line number Diff line change
@@ -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")
23 changes: 23 additions & 0 deletions docs/examples/styles/border_title_align.css
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 20 additions & 0 deletions docs/examples/styles/border_title_align.py
Original file line number Diff line number Diff line change
@@ -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")
22 changes: 22 additions & 0 deletions docs/guide/styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,28 @@ There are many other border types. Run the following from the command prompt to
textual borders
```


#### Title alignment

Widgets have two attributes, `border_title` and `border_subtitle` which (if set) will be displayed within the border.
The `border_title` attribute is displayed in the top border, and `border_subtitle` is displayed in the bottom border.

There are two styles to set the alignment of these border labels, which may be set to "left", "right", or "center".

- [`border-title-align`](../styles/border_title_align.md) sets the alignment of the title, which defaults to "left".
- [`border-subtitle-align`](../styles/border_subtitle_align.md) sets the alignment of the subtitle, which defaults to "right".

The following example sets both titles and changes the alignment of the title (top) to "center".

```py hl_lines="22-24"
--8<-- "docs/examples/guide/styles/border_title.py"
```

Note the addition of the titles and their alignments:

```{.textual path="docs/examples/guide/styles/border_title.py"}
```

### Outline

[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:
Expand Down
Empty file.
28 changes: 6 additions & 22 deletions docs/styles/_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,7 @@ rule-name: <a href="../../css_types/type_one">&lt;type-one&gt;</a>;
### Values

<!--
If this rule only needs one type, include it directly:
--8<-- "docs/snippets/type_syntax/only_type.md"
-->

<!--
If this rule needs two or more types:
### &lt;first-type&gt;
--8<-- "docs/snippets/type_syntax/first_type.md"
### &lt;second-type&gt;
--8<-- "docs/snippets/type_syntax/second_type.md"
...
For enum-like styles that don't warrant a dedicated type.
-->

### Defaults
Expand Down Expand Up @@ -120,12 +104,12 @@ Copy the same examples as the ones shown in the CSS above.
If the programmatic way of setting the rule differs significantly from the CSS way, make note of that here.
```py
rule_name = value1
rule_name = value2
rule_name = (different_syntax_value, shown_here)
widget.styles.rule_name = value1
widget.styles.rule_name = value2
widget.styles.rule_name = (different_syntax_value, shown_here)
rule_name_variant = value3
rule_name_variant = value4
widget.styles.rule_name_variant = value3
widget.styles.rule_name_variant = value4
```
-->
Loading

0 comments on commit 2a810f8

Please sign in to comment.