Skip to content

Commit bfbc6fb

Browse files
authored
added howto (#3854)
* added howto * don't need this * don't need this * move title * docstring
1 parent dbdcf27 commit bfbc6fb

File tree

8 files changed

+155
-20
lines changed

8 files changed

+155
-20
lines changed

docs/api/renderables.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
A collection of Rich renderables which may be returned from a widget's `render()` method.
2+
3+
::: textual.renderables.bar
4+
::: textual.renderables.blank
5+
::: textual.renderables.digits
6+
::: textual.renderables.gradient
7+
::: textual.renderables.sparkline
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from time import time
2+
3+
from textual.app import App, ComposeResult, RenderableType
4+
from textual.containers import Container
5+
from textual.renderables.gradient import LinearGradient
6+
from textual.widgets import Static
7+
8+
COLORS = [
9+
"#881177",
10+
"#aa3355",
11+
"#cc6666",
12+
"#ee9944",
13+
"#eedd00",
14+
"#99dd55",
15+
"#44dd88",
16+
"#22ccbb",
17+
"#00bbcc",
18+
"#0099cc",
19+
"#3366bb",
20+
"#663399",
21+
]
22+
STOPS = [(i / (len(COLORS) - 1), color) for i, color in enumerate(COLORS)]
23+
24+
25+
class Splash(Container):
26+
"""Custom widget that extends Container."""
27+
28+
DEFAULT_CSS = """
29+
Splash {
30+
align: center middle;
31+
}
32+
Static {
33+
width: 40;
34+
padding: 2 4;
35+
}
36+
"""
37+
38+
def on_mount(self) -> None:
39+
self.auto_refresh = 1 / 30 # (1)!
40+
41+
def compose(self) -> ComposeResult:
42+
yield Static("Making a splash with Textual!") # (2)!
43+
44+
def render(self) -> RenderableType:
45+
return LinearGradient(time() * 90, STOPS) # (3)!
46+
47+
48+
class SplashApp(App):
49+
"""Simple app to show our custom widget."""
50+
51+
def compose(self) -> ComposeResult:
52+
yield Splash()
53+
54+
55+
if __name__ == "__main__":
56+
app = SplashApp()
57+
app.run()

docs/how-to/render-and-compose.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Render and compose
2+
3+
A common question that comes up on the [Textual Discord server](https://discord.gg/Enf6Z3qhVr) is what is the difference between [`render`][textual.widget.Widget.render] and [`compose`][textual.widget.Widget.compose] methods on a widget?
4+
In this article we will clarify the differences, and use both these methods to build something fun.
5+
6+
<div class="video-wrapper">
7+
<iframe width="1280" height="922" src="https://www.youtube.com/embed/dYU7jHyabX8" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
8+
</div>
9+
10+
## Which method to use?
11+
12+
Render and compose are easy to confuse because they both ultimately define what a widget will look like, but they have quite different uses.
13+
14+
The `render` method on a widget returns a [Rich](https://rich.readthedocs.io/en/latest/) renderable, which is anything you could print with Rich.
15+
The simplest renderable is just text; so `render()` methods often return a string to display inside the widget, but could equally return a [`Text`](https://rich.readthedocs.io/en/latest/text.html) instance, a [`Table`](https://rich.readthedocs.io/en/latest/tables.html), or anything else from Rich (or third party library).
16+
17+
The `compose` method is used to build [*compound* widgets](../guide/widgets.md#compound-widgets) (widgets composed of other widgets).
18+
19+
A general rule of thumb, is that if you implement a `compose` method, there is no need for a `render` method because it is the widgets yielded from `compose` which define how the custom widget will look.
20+
However, you *can* mix these two methods.
21+
If you implement both, the `render` method will set the custom widget's *background* and `compose` will add widgets on top of that background.
22+
23+
## Combining render and compose
24+
25+
Let's look at an example that combines both these methods.
26+
We will create a custom widget with a [linear gradient][textual.renderables.gradient.LinearGradient] as a background.
27+
The background will be animated (I did promise *fun*)!
28+
29+
=== "render_compose.py"
30+
31+
```python
32+
--8<-- "docs/examples/how-to/render_compose.py"
33+
```
34+
35+
1. Refresh the widget 30 times a second.
36+
2. Compose our compound widget, which contains a single Static.
37+
3. Render a linear gradient in the background.
38+
39+
=== "Output"
40+
41+
```{.textual path="docs/examples/how-to/render_compose.py" columns="100" lines="40"}
42+
```
43+
44+
The `Splash` custom widget has a `compose` method which adds a simple `Static` widget to display a message.
45+
Additionally there is a `render` method which returns a renderable to fill the background with a gradient.
46+
47+
!!! tip
48+
49+
As fun as this is, spinning animated gradients may be too distracting for most apps!
50+
51+
## Summary
52+
53+
Keep the following in mind when building [custom widgets](../guide/widgets.md).
54+
55+
1. Use `render` to return simple text, or a Rich renderable.
56+
2. Use `compose` to create a widget out of other widgets.
57+
3. If you define both, then `render` will be used as a *background*.
58+
59+
60+
---
61+
62+
We are here to [help](../help.md)!

mkdocs-nav.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ nav:
195195
- "api/pilot.md"
196196
- "api/query.md"
197197
- "api/reactive.md"
198+
- "api/renderables.md"
198199
- "api/screen.md"
199200
- "api/scrollbar.md"
200201
- "api/scroll_view.md"
@@ -213,6 +214,7 @@ nav:
213214
- "how-to/index.md"
214215
- "how-to/center-things.md"
215216
- "how-to/design-a-layout.md"
217+
- "how-to/render-and-compose.md"
216218
- "FAQ.md"
217219
- "roadmap.md"
218220
- "Blog":
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__all__ = ["bar", "blank", "digits", "gradient", "sparkline"]

src/textual/renderables/gradient.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from functools import lru_cache
44
from math import cos, pi, sin
5+
from typing import Sequence
56

67
from rich.color import Color as RichColor
78
from rich.console import Console, ConsoleOptions, RenderResult
@@ -42,17 +43,22 @@ def __rich_console__(
4243

4344

4445
class LinearGradient:
45-
"""Render a linear gradient with a rotation."""
46+
"""Render a linear gradient with a rotation.
4647
47-
def __init__(self, angle: float, stops: list[tuple[float, Color]]) -> None:
48-
"""
48+
Args:
49+
angle: Angle of rotation in degrees.
50+
stops: List of stop consisting of pairs of offset (between 0 and 1) and color.
4951
50-
Args:
51-
angle: Angle of rotation in degrees.
52-
stops: List of stop consisting of pairs of offset (between 0 and 1) and colors.
53-
"""
52+
"""
53+
54+
def __init__(
55+
self, angle: float, stops: Sequence[tuple[float, Color | str]]
56+
) -> None:
5457
self.angle = angle
55-
self._stops = stops[:]
58+
self._stops = [
59+
(stop, Color.parse(color) if isinstance(color, str) else color)
60+
for stop, color in stops
61+
]
5662

5763
def __rich_console__(
5864
self, console: Console, options: ConsoleOptions
@@ -75,7 +81,7 @@ def __rich_console__(
7581
get_color = color_gradient.get_color
7682
from_color = Style.from_color
7783

78-
@lru_cache(maxsize=None)
84+
@lru_cache(maxsize=1024)
7985
def get_rich_color(color_offset: int) -> RichColor:
8086
"""Get a Rich color in the gradient.
8187

src/textual/scrollbar.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,6 @@ class MyScrollBarRender(ScrollBarRender): ...
221221
```
222222
"""
223223

224-
DEFAULT_CSS = """
225-
ScrollBar {
226-
link-color-hover: ;
227-
link-background-hover:;
228-
link-style-hover: ;
229-
link-color: transparent;
230-
link-background: transparent;
231-
}
232-
"""
233-
234224
def __init__(
235225
self, vertical: bool = True, name: str | None = None, *, thickness: int = 1
236226
) -> None:

src/textual/widget.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3161,7 +3161,17 @@ def remove_children(self) -> AwaitRemove:
31613161
return await_remove
31623162

31633163
def render(self) -> RenderableType:
3164-
"""Get renderable for widget.
3164+
"""Get text or Rich renderable for this widget.
3165+
3166+
Implement this for custom widgets.
3167+
3168+
Example:
3169+
```python
3170+
from textual.app import RenderableType
3171+
3172+
def render(self) -> RenderableType:
3173+
return "Welcome to [bold red]Textual[/]!"
3174+
```
31653175
31663176
Returns:
31673177
Any renderable.

0 commit comments

Comments
 (0)