Skip to content

Commit b49eb9e

Browse files
committed
docs and tests
1 parent 3ec59ee commit b49eb9e

File tree

7 files changed

+234
-83
lines changed

7 files changed

+234
-83
lines changed
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from rich.syntax import Syntax
2+
3+
from textual.app import App, ComposeResult, RenderResult
4+
from textual.reactive import reactive
5+
from textual.widget import Widget
6+
7+
8+
class CodeView(Widget):
9+
"""Widget to display Python code."""
10+
11+
DEFAULT_CSS = """
12+
CodeView { height: auto; }
13+
"""
14+
15+
code = reactive("")
16+
17+
def render(self) -> RenderResult:
18+
# Syntax is a Rich renderable that displays syntax highlighted code
19+
syntax = Syntax(self.code, "python", line_numbers=True, indent_guides=True)
20+
return syntax
21+
22+
23+
class CodeApp(App):
24+
"""App to demonstrate Rich renderables in Textual."""
25+
26+
def compose(self) -> ComposeResult:
27+
with open(__file__) as self_file:
28+
code = self_file.read()
29+
code_view = CodeView()
30+
code_view.code = code
31+
yield code_view
32+
33+
34+
if __name__ == "__main__":
35+
app = CodeApp()
36+
app.run()

docs/examples/guide/screens/modal02.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class ModalApp(App):
3434
"""An app with a modal dialog."""
3535

3636
CSS_PATH = "modal01.tcss"
37-
BINDINGS = [("q", "request_quit", "Quit"), ("f", "foo", "asd")]
37+
BINDINGS = [("q", "request_quit", "Quit")]
3838

3939
def compose(self) -> ComposeResult:
4040
yield Header()

docs/guide/content.md

+102-16
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ Notice how the markup *tags* change the style in the first widget, but are left
2929
1. With `markup=False`, tags have no effect and left in the output.
3030

3131

32-
33-
3432
### Playground
3533

3634
Textual comes with a markup playground where you can enter Textual markup and see the result's live.
@@ -42,11 +40,14 @@ python -m textual.markup
4240

4341
You can experiment with markup by entering it in to the textarea at the top of the terminal, and seeing the results in the lower pane:
4442

45-
```{.textual path="docs/examples/guide/content/playground.py", type="[i]Hello!"] lines=15}
43+
```{.textual path="docs/examples/guide/content/playground.py", type="[i]Hello!"] lines=16}
4644
```
4745

4846
You might find it helpful to try out some of the examples from this guide in the playground.
4947

48+
!!! note "What are Variables?"
49+
You may have noticed the "Variables" tab. This allows you to experiment with [variable substitution](#markup-variables).
50+
5051
### Tags
5152

5253
There are two types of tag: an *opening* tag which starts a style change, and a *closing* tag which ends a style change.
@@ -75,7 +76,7 @@ For example, the following makes just the first word in "Hello, World!" bold:
7576

7677
Note how the tags change the style but are removed from the output:
7778

78-
```{.textual path="docs/examples/guide/content/playground.py", type="[bold]Hello[/bold], World!" lines=15}
79+
```{.textual path="docs/examples/guide/content/playground.py", type="[bold]Hello[/bold], World!" lines=16}
7980
```
8081

8182
You can use any number of tags.
@@ -88,7 +89,7 @@ For instance, the following combines the bold and italic styles:
8889

8990
Here's the output:
9091

91-
```{.textual path="docs/examples/guide/content/playground.py", type="[bold]Bold [italic]Bold and italic[/italic][/bold]" lines=15}
92+
```{.textual path="docs/examples/guide/content/playground.py", type="[bold]Bold [italic]Bold and italic[/italic][/bold]" lines=16}
9293
```
9394

9495
#### Auto-closing tags
@@ -142,7 +143,7 @@ However, the `[not bold]` tag disables bold until the corresponding `[/not bold]
142143

143144
Here's what this markup will produce:
144145

145-
```{.textual path="docs/examples/guide/content/playground.py" lines=15 type="[bold]This is bold [not bold]This is not bold[/not bold] This is bold."]}
146+
```{.textual path="docs/examples/guide/content/playground.py" lines=16 type="[bold]This is bold [not bold]This is not bold[/not bold] This is bold."]}
146147
```
147148

148149
### Colors
@@ -176,7 +177,7 @@ In the following example we have an alpha of 0.5, which will produce a color hal
176177

177178
Here's the output:
178179

179-
```{.textual path="docs/examples/guide/content/playground.py", type="[rgba(0, 255, 0, 0.5)]Faded green (and probably hard to read)[/]" lines=15}
180+
```{.textual path="docs/examples/guide/content/playground.py", type="[rgba(0, 255, 0, 0.5)]Faded green (and probably hard to read)[/]" lines=16}
180181
```
181182

182183
!!! warning
@@ -250,7 +251,7 @@ The following displays text in the 'warning' style on a muted 'warning' backgrou
250251

251252
Here's the result of that markup:
252253

253-
```{.textual path="docs/examples/guide/content/playground.py" lines=15 type="[$warning on $warning-muted]This is a warning![/]"]}
254+
```{.textual path="docs/examples/guide/content/playground.py" lines=16 type="[$warning on $warning-muted]This is a warning![/]"]}
254255
```
255256

256257
### Links
@@ -267,23 +268,61 @@ For instance, the following create a clickable link:
267268
This will produce the following output:
268269
<code><pre><a href="https://www.willmcgugan.com">Visit my blog!</a></pre></code>
269270

271+
### Actions
272+
273+
In addition to links, you can also markup content that runs [actions](./actions.md) when clicked.
274+
To do this create a style that starts with `@click=` and is followed by the action you wish to run.
275+
276+
For instance, the following will highlight the word "bell", which plays the terminal bell sound when click:
277+
278+
```
279+
Play the [@click=app.bell]bell[/]
280+
```
281+
282+
Here's what it looks like:
283+
284+
```{.textual path="docs/examples/guide/content/playground.py" lines=16 type="Play the [@click=app.bell]bell[/]"]}
285+
```
286+
287+
We've used an [auto-closing](#auto-closing-tags) to close the click action here.
288+
If you do need to close the tag explicitly, you can omit the action:
289+
290+
```
291+
Play the [@click=app.bell]bell[/@click=]
292+
```
293+
294+
Actions may be combined with other styles, so you could set the style of the clickable link:
295+
296+
```
297+
Play the [on $success 30% @click=app.bell]bell[/]
298+
```
299+
300+
Here's what that looks like:
301+
302+
```{.textual path="docs/examples/guide/content/playground.py" lines=16 type="Play the [on $success 30% @click=app.bell]bell[/]"]}
303+
```
304+
305+
270306
## Content class
271307

272308
Under the hood, Textual will convert markup into a [Content][textual.content.Content] instance.
273-
You can also return Content directly from `render()`, which you may want to do if you require more advanced formatting beyond simple markup.
309+
You can also return a Content object directly from `render()`.
310+
This can give you more flexibility beyond the markup.
274311

275312
To clarify, here's a render method that returns a string with markup:
276313

277314
```python
278-
def render(self) -> RenderResult:
279-
return "[b]Hello, World![/b]"
315+
class WelcomeWidget(Widget):
316+
def render(self) -> RenderResult:
317+
return "[b]Hello, World![/b]"
280318
```
281319

282320
This is roughly the equivalent to the following code:
283321

284322
```python
285-
def render(self) -> RenderResult:
286-
return Content.from_markup("[b]Hello, World![/b]")
323+
class WelcomeWidget(Widget):
324+
def render(self) -> RenderResult:
325+
return Content.from_markup("[b]Hello, World![/b]")
287326
```
288327

289328
### Constructing content
@@ -318,9 +357,56 @@ content = content.stylize(7, 12, "bold")
318357
Note that `Content` is *immutable* and methods will return new instances rather than updating the current instance.
319358

320359

360+
### Markup variables
361+
362+
You may be tempted to combine markup with Python's f-strings (or other string template system).
363+
Something along these lines:
364+
365+
```python
366+
class WelcomeWidget(Widget):
367+
def render(self) -> RenderResult:
368+
name = "Will"
369+
return f"Hello [bold]{name}[/bold]!"
370+
```
371+
372+
While this is straightforward and intuitive, it can potentially break in subtle ways.
373+
If the 'name' variable contains square brackets, these may be interpreted as markup.
374+
For instance if the user entered their name at some point as "[magenta italic underline]Will is Cool!" then your app will display those styles where you didn't intend them to be.
375+
376+
We can avoid this problem by relying on the [Content.from_markup][textual.content.Content.from_markup] method to insert the variables for us.
377+
If you supply variables as keyword arguments, these will be substituted in the markup using the same syntax as [string.Template](https://docs.python.org/3/library/string.html#template-strings).
378+
Any square brackets in the variables will be present in the output, but won't change the styles.
379+
380+
Here's how we can fix the previous example:
381+
382+
```python
383+
return Content.from_markup("hello [bold]$name[/bold]!", name=name)
384+
```
385+
386+
You can experiment with this feature by entering a dictionary of variables in the variables text-area.
387+
388+
Here's what that looks like:
389+
390+
```{.textual path="docs/examples/guide/content/playground.py" lines=16 type="hello [bold]$name[/bold]!\t{'name': '[magenta italic underline]Will is Cool!'}"]}
391+
```
392+
321393
## Rich renderables
322394

323-
Textual supports Rich renderables, which means you can return any object that works with Rich, such as Rich's [Text](https://rich.readthedocs.io/en/latest/text.html) object.
395+
Textual supports Rich *renderables*, which means you can display any object that works with Rich, such as Rich's [Text](https://rich.readthedocs.io/en/latest/text.html) object.
396+
397+
The Content class is preferred for simple text, as it supports more of Textual's features.
398+
But you can display any of the objects in the [Rich library](https://github.com/Textualize/rich) (or ecosystem) within a widget.
399+
400+
Here's an example which displays its own code using Rich's [Syntax](https://rich.readthedocs.io/en/latest/syntax.html) object.
401+
402+
=== "Output"
403+
404+
```{.textual path="docs/examples/guide/content/renderables.py"}
405+
```
406+
407+
=== "renderables.py"
324408

325-
The Content class is generally preferred, as it supports more of Textual's features.
326-
If you already have a Text object and your code is working, there is no need to change it -- Textual won't be dropping Rich support.
409+
```python
410+
--8<-- "docs/examples/guide/content/renderables.py"
411+
```
412+

src/textual/_doc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
2727
_type = attrs.get("type", None)
2828
press = [*_press.split(",")] if _press else []
2929
if _type is not None:
30-
press.extend(_type)
30+
press.extend(_type.replace("\\t", "\t"))
3131
title = attrs.get("title")
3232

3333
print(f"screenshotting {path!r}")

src/textual/_markup_playground.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ class MarkupPlayground(App):
1717
}
1818
layout: vertical;
1919
#editor {
20+
width: 2fr;
2021
height: 1fr;
2122
border: tab $primary;
2223
padding: 1;
2324
margin: 1 1 0 0;
25+
2426
}
2527
#variables {
28+
width: 1fr;
2629
height: 1fr;
2730
border: tab $primary;
2831
padding: 1;
@@ -44,13 +47,14 @@ class MarkupPlayground(App):
4447
}
4548
}
4649
"""
50+
AUTO_FOCUS = "#editor"
4751

4852
variables: reactive[dict[str, object]] = reactive({})
4953

5054
def compose(self) -> ComposeResult:
5155
with containers.HorizontalScroll():
5256
yield (editor := TextArea(id="editor"))
53-
yield (variables := TextArea(id="variables", language="json"))
57+
yield (variables := TextArea("", id="variables", language="json"))
5458
editor.border_title = "Markup"
5559
variables.border_title = "Variables (JSON)"
5660

@@ -92,7 +96,5 @@ def on_variables_change(self, event: TextArea.Changed) -> None:
9296
self.notify(f"Bad JSON: ${error}", title="Variables", severity="error")
9397
variables_text_area.add_class("-bad-json")
9498
else:
95-
if variables_text_area.has_class("-bad-json"):
96-
variables_text_area.remove_class("-bad-json")
97-
self.notify("JSON parsed correctly", title="Variables")
99+
variables_text_area.remove_class("-bad-json")
98100
self.variables = variables

0 commit comments

Comments
 (0)