mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
content docs
This commit is contained in:
36
docs/examples/guide/content/content01.py
Normal file
36
docs/examples/guide/content/content01.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Static
|
||||
|
||||
TEXT1 = """\
|
||||
Hello, [bold $text on $primary]World[/]!
|
||||
|
||||
[@click=app.notify('Hello, World!')]Click me[/]
|
||||
"""
|
||||
|
||||
TEXT2 = """\
|
||||
Markup will [bold]not[/bold] be displayed.
|
||||
|
||||
Tags will be left in the output.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ContentApp(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
Static {
|
||||
height: 1fr;
|
||||
}
|
||||
#text1 { background: $primary-muted; }
|
||||
#text2 { background: $error-muted; }
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(TEXT1, id="text1")
|
||||
yield Static(TEXT2, id="text2", markup=False) # (1)!
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = ContentApp()
|
||||
app.run()
|
||||
5
docs/examples/guide/content/playground.py
Normal file
5
docs/examples/guide/content/playground.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from textual._markup_playground import MarkupPlayground
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MarkupPlayground()
|
||||
app.run()
|
||||
@@ -1 +1,322 @@
|
||||
# Content
|
||||
|
||||
Content may be returned from
|
||||
|
||||
## Markup
|
||||
|
||||
When building a custom widget you can embed color and style information in the string returned from the Widget's [`render()`][textual.widget.Widget.render] method.
|
||||
Text enclosed in square brackets (`[]`) won't appear in the output, but will modify the style of the text that follows.
|
||||
This is known as *Textual markup*.
|
||||
|
||||
Before we explore Textual markup in detail, let's first demonstrate some of what it can do.
|
||||
In the following example, we have two widgets.
|
||||
The top has Textual markup enabled, and the bottom has Textual markup *disabled*.
|
||||
|
||||
Notice how the markup *tags* change the style in the first widget, but are left unaltered in the second:
|
||||
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/guide/content/content01.py"}
|
||||
```
|
||||
|
||||
=== "content01.py"
|
||||
|
||||
```python
|
||||
--8<-- "docs/examples/guide/content/content01.py"
|
||||
```
|
||||
|
||||
1. With `markup=False`, tags have no effect and left in the output.
|
||||
|
||||
|
||||
|
||||
|
||||
### Playground
|
||||
|
||||
Textual comes with a markup playground where you can enter Textual markup and see the result's live.
|
||||
To launch the playground, run the following command:
|
||||
|
||||
```
|
||||
python -m textual.markup
|
||||
```
|
||||
|
||||
The interface looks something like this:
|
||||
|
||||
```{.textual path="docs/examples/guide/content/playground.py", type="[i]Hello!"] lines=15}
|
||||
```
|
||||
|
||||
If you are trying Textual Markup for the first time, it is worthwhile to test what you read with he Playground app!
|
||||
|
||||
### Tags
|
||||
|
||||
There are two types of tag: an *opening* tag which starts a style change, and a *closing* tag which ends a style change.
|
||||
An opening tag looks something like the following:
|
||||
|
||||
```
|
||||
[bold]
|
||||
```
|
||||
|
||||
This will make following text bold. For instance, the following would result in the text "Hello, World!" in bold:
|
||||
|
||||
|
||||
```
|
||||
[bold]Hello, World!
|
||||
```
|
||||
|
||||
The second type of tag, a *closing* tag, is almost identical, but starts with a forward slash inside the first square bracket.
|
||||
A closing tag ends a style, started by a previous opening tag.
|
||||
|
||||
!!! note
|
||||
|
||||
Without a closing tag, the style will persist to the end of the string. This can be a convenient shortcut if you want to style the entire string.
|
||||
|
||||
|
||||
For instance, the following is a closing tag to end the bold style:
|
||||
|
||||
```
|
||||
[/bold]
|
||||
```
|
||||
|
||||
A closing tag must match a previous opening tag, so that Textual knows which style is being closed.
|
||||
Let's use this to embolden just the first word in our example:
|
||||
|
||||
```
|
||||
[bold]Hello[/bold], World!
|
||||
```
|
||||
|
||||
This will make "Hello" bold, but the rest of the text will be non-bold.
|
||||
|
||||
You can use any number of tags, and they may overlap which combines their styles.
|
||||
For instance, the following combines the bold and italic styles:
|
||||
|
||||
```
|
||||
[bold]Bold [italic]Bold and italic[/italic][/bold]
|
||||
```
|
||||
|
||||
#### Auto-closing tags
|
||||
|
||||
A closing tag without any style information (i.e. `[/]`) is an *auto-closing* tag.
|
||||
Auto-closing tags will close the last tag, regardless of it's style.
|
||||
|
||||
The following uses an auto-closing tag to end the bold style:
|
||||
|
||||
```
|
||||
[bold]Hello[/], World!
|
||||
```
|
||||
|
||||
This is equivalent to the following (but saves typing a few characters):
|
||||
|
||||
```
|
||||
[bold]Hello[/bold], World!
|
||||
```
|
||||
|
||||
Auto-closing tags recommended when it is clear which tag they are intended to close.
|
||||
|
||||
### Styles
|
||||
|
||||
Tags may contain any number of the following tags:
|
||||
|
||||
| Style | Abbreviation | Description |
|
||||
| ----------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `bold` | `b` | **Bold text** |
|
||||
| `dim` | `d` | <span style="opacity: 0.6;">Dim text </span> (slightly transparent) |
|
||||
| `italic` | `i` | *Italic text* |
|
||||
| `underline` | `u` | <u>Underlined text</u> |
|
||||
| `strike` | `s` | <strike>Strikethrough text<strile> |
|
||||
| `reverse` | `r` | <span style="background: var(--md-primary-bg-color); color: var(--md-primary-fg-color);">Reversed colors text</span> (background swapped with foreground) |
|
||||
|
||||
These styles can be abbreviate to save typing.
|
||||
For example `[bold]` and `[b]` are equivalent.
|
||||
|
||||
Styles can also be combined within the same tag, so `[bold italic]` produces text that is both bold *and* italic.
|
||||
|
||||
#### Inverting styles
|
||||
|
||||
You can invert a style by preceding it with the word `not`.
|
||||
This is useful if you have text with a given style, but you temporarily want to disable it.
|
||||
|
||||
For instance, the following markup starts with `[bold]` which makes the text bold until it reaches `[not bold]` which disables the bold style, until the corresponding `[/not bold]`.
|
||||
|
||||
```
|
||||
[bold]This is bold [not bold]This is not bold[/not bold] This is bold.
|
||||
```
|
||||
|
||||
Here's what this markup will produce:
|
||||
|
||||
```{.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."]}
|
||||
```
|
||||
|
||||
### Colors
|
||||
|
||||
Colors may specified in the same way as a CSS [<color>](/css_types/color).
|
||||
Here are a few examples:
|
||||
|
||||
```
|
||||
[#ff0000]HTML hex style[/]
|
||||
[rgba(0,255,0)]HTML RGB style[/]
|
||||
|
||||
```
|
||||
|
||||
You can also any of the [named colors](/css_types/color).
|
||||
|
||||
```
|
||||
[chartreuse]This is a green color[/]
|
||||
[sienna]This is a kind of yellow-brown.[/]
|
||||
```
|
||||
|
||||
Colors may also include an *alpha* component, which makes the color fade in to the background.
|
||||
For instance, if we specify the color with `rgba(...)`, then we can add an alpha component between 0 and 1.
|
||||
An alpha of zero is fully transparent (and therefore invisible). An alpha of 1 is fully opaque, and equivalent to a color without an alpha component.
|
||||
A value between 0 and 1 results in a faded color.
|
||||
|
||||
In the following example we have an alpha of 0.5, which will produce a color half way between the background and solid green:
|
||||
|
||||
```
|
||||
[rgba(0, 255, 0, 0.5)]Faded green (and probably hard to read)[/]
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
Be careful when using colors with an alpha component. Text that is blended too much with the background may become hard to read.
|
||||
|
||||
|
||||
#### Auto colors
|
||||
|
||||
You can specify a color as "auto", which is a special value that tells Textual to pick either white or black text -- whichever has the best contrast.
|
||||
|
||||
For example, the following will produce either white or black text (I haven't checked) on a sienna background:
|
||||
|
||||
```
|
||||
[auto on sienna]This should be fairly readable.
|
||||
```
|
||||
|
||||
|
||||
#### Opacity
|
||||
|
||||
While you can set the opacity in the color itself, you can also add a percentage which will modify the alpha component of the previous color.
|
||||
|
||||
For example, the addition of `50%` will result in a color half way between the background and "red":
|
||||
|
||||
```
|
||||
[red 50%]This is in faded red[/]
|
||||
```
|
||||
|
||||
|
||||
#### Background colors
|
||||
|
||||
Background colors may be specified by preceding a color with the world `on`.
|
||||
Here's an example:
|
||||
|
||||
```
|
||||
[on #ff0000]Background is bright red.
|
||||
```
|
||||
|
||||
Background colors may also have an alpha component (either in the color itself or with a percentage).
|
||||
This will result in a color that is blended with the widget's parent (or Screen).
|
||||
|
||||
Here's an example that tints the background with 20% red:
|
||||
|
||||
```
|
||||
[on #ff0000 20%]The background has a red tint.[/]
|
||||
```
|
||||
|
||||
|
||||
```{.textual path="docs/examples/guide/content/playground.py" lines=15 type="[on #ff0000 20%]The background has a red tint.[/]"]}
|
||||
```
|
||||
|
||||
|
||||
### CSS variables
|
||||
|
||||
You can also use CSS variables in markup, such as those specified in the [design](./design.md#base-colors) guide.
|
||||
|
||||
To use any of the theme colors, simple use the name of the color including the `$` at the first position.
|
||||
For example, this will display text in the *accent* color:
|
||||
|
||||
```
|
||||
[$accent]Accent color[/]
|
||||
```
|
||||
|
||||
You may also use a color variable in the background position.
|
||||
The following displays text in the 'warning' style on a muted 'warning' background for emphasis:
|
||||
|
||||
```
|
||||
[$warning on $warning-muted]This is a warning![/]
|
||||
```
|
||||
|
||||
```{.textual path="docs/examples/guide/content/playground.py" lines=15 type="[$warning on $warning-muted]This is a warning![/]"]}
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
Styles may contain links which will create clickable links that launch your web browser, if supported by your terminal.
|
||||
|
||||
To create a link add `link=` followed by your link in quotes.
|
||||
For instance, the following create a clickable link:
|
||||
|
||||
```
|
||||
[link="https://www.willmcgugan.com"]Visit my blog![/link]
|
||||
```
|
||||
|
||||
This will produce the following output:
|
||||
<code><pre><a href="https://www.willmcgugan.com">Visit my blog!</a></pre></code>
|
||||
|
||||
|
||||
|
||||
## Content class
|
||||
|
||||
Under the hood, Textual will convert markup into a [Content][textual.content.Content] instance.
|
||||
You can also return Content directly from `render()`, which you may want to do if you require more advanced formatting beyond simple markup.
|
||||
|
||||
To clarify, here's a render method that returns a string with markup:
|
||||
|
||||
```python
|
||||
def render(self) -> RenderResult:
|
||||
return "[b]Hello, World![/b]"
|
||||
```
|
||||
|
||||
This is roughly the equivalent to the following code:
|
||||
|
||||
```python
|
||||
def render(self) -> RenderResult:
|
||||
return Content.from_markup("[b]Hello, World![/b]")
|
||||
```
|
||||
|
||||
### Constructing content
|
||||
|
||||
The [Content][textual.content.Content] class accepts a default string in it's constructor.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```python
|
||||
Content("hello, World!")
|
||||
```
|
||||
|
||||
Note that if you construct Content in this way, it *won't* process markup (any square brackets will be displayed literally).
|
||||
|
||||
If you want markup, you can create a `Content` with the [Content.from_markup][textual.content.Content.from_markup] alternative constructor:
|
||||
|
||||
```python
|
||||
Content.from_markup("hello, [bold]World[/bold]!")
|
||||
```
|
||||
|
||||
### Styling content
|
||||
|
||||
You can add styles to content with the [stylize][textual.content.Content.stylize] or [stylize_before][textual.content.Content.stylize] methods.
|
||||
|
||||
For instance, in the following code we create content with the text "Hello, World!" and style "World" to be bold:
|
||||
|
||||
```python
|
||||
content = Content("Hello, World!")
|
||||
content = content.stylize(7, 12, "bold")
|
||||
```
|
||||
|
||||
Note that `Content` is *immutable* and methods will return new instances rather than updating the current instance.
|
||||
|
||||
|
||||
## Rich renderables
|
||||
|
||||
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.
|
||||
|
||||
The Content class is generally preferred, as it supports more of Textual's features.
|
||||
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.
|
||||
But we recommend the [Content class](#content-class) for newer code.
|
||||
|
||||
@@ -11,6 +11,7 @@ nav:
|
||||
- "guide/styles.md"
|
||||
- "guide/CSS.md"
|
||||
- "guide/design.md"
|
||||
- "guide/content.md"
|
||||
- "guide/queries.md"
|
||||
- "guide/layout.md"
|
||||
- "guide/events.md"
|
||||
|
||||
@@ -24,7 +24,10 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
|
||||
path = cmd[0]
|
||||
|
||||
_press = attrs.get("press", None)
|
||||
_type = attrs.get("type", None)
|
||||
press = [*_press.split(",")] if _press else []
|
||||
if _type is not None:
|
||||
press.extend(_type)
|
||||
title = attrs.get("title")
|
||||
|
||||
print(f"screenshotting {path!r}")
|
||||
|
||||
55
src/textual/_markup_playground.py
Normal file
55
src/textual/_markup_playground.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from rich.highlighter import ReprHighlighter
|
||||
|
||||
from textual import containers, on
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Static, TextArea
|
||||
|
||||
|
||||
class MarkupPlayground(App):
|
||||
|
||||
TITLE = "Markup Playground"
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: vertical;
|
||||
#editor {
|
||||
height: 1fr;
|
||||
border: tab $primary;
|
||||
padding: 1;
|
||||
margin: 1 1 0 1;
|
||||
}
|
||||
#results-container {
|
||||
margin: 0 1;
|
||||
border: tab $success;
|
||||
&.-error {
|
||||
border: tab $error;
|
||||
}
|
||||
}
|
||||
#results {
|
||||
height: 1fr;
|
||||
padding: 1 1;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield (text_area := TextArea(id="editor"))
|
||||
text_area.border_title = "Markup"
|
||||
|
||||
with containers.VerticalScroll(id="results-container") as container:
|
||||
yield Static(id="results")
|
||||
container.border_title = "Output"
|
||||
|
||||
@on(TextArea.Changed)
|
||||
def on_markup_changed(self, event: TextArea.Changed) -> None:
|
||||
results = self.query_one("#results", Static)
|
||||
try:
|
||||
results.update(event.text_area.text)
|
||||
except Exception as error:
|
||||
highlight = ReprHighlighter()
|
||||
# results.update(highlight(str(error)))
|
||||
from rich.traceback import Traceback
|
||||
|
||||
results.update(Traceback())
|
||||
self.query_one("#results-container").add_class("-error")
|
||||
else:
|
||||
self.query_one("#results-container").remove_class("-error")
|
||||
@@ -845,7 +845,9 @@ class Content(Visual):
|
||||
start: int = 0,
|
||||
end: int | None = None,
|
||||
) -> Content:
|
||||
"""Apply a style to the text, or a portion of the text. Styles will be applied before other styles already present.
|
||||
"""Apply a style to the text, or a portion of the text.
|
||||
|
||||
Styles applies with this method will be applied *before* other styles already present.
|
||||
|
||||
Args:
|
||||
style (Union[str, Style]): Style instance or style definition to apply.
|
||||
@@ -875,6 +877,20 @@ class Content(Visual):
|
||||
end: str = "\n",
|
||||
parse_style: Callable[[str], Style] | None = None,
|
||||
) -> Iterable[tuple[str, Style]]:
|
||||
"""Render Content in to an iterable of strings and styles.
|
||||
|
||||
This is typically called by Textual when displaying Content, but may be used if you want to do more advanced
|
||||
pricessing of the output.
|
||||
|
||||
Args:
|
||||
base_style (_type_, optional): The style used as a base. This will typically be the style of the widget underneath the content.
|
||||
end (_type_, optional): Text to end the output, such as a new line.
|
||||
parse_style: Method to parse a style. Use App.parse_style to apply CSS variables in styles.
|
||||
|
||||
Returns:
|
||||
An iterable of string and styles, which make up the content.
|
||||
|
||||
"""
|
||||
|
||||
if not self._spans:
|
||||
yield (self._text, base_style)
|
||||
@@ -886,6 +902,7 @@ class Content(Visual):
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def get_style(style: str, /) -> Style:
|
||||
"""The default get_style method."""
|
||||
try:
|
||||
visual_style = Style.parse(style)
|
||||
except Exception:
|
||||
|
||||
@@ -338,60 +338,7 @@ def _to_content(markup: str, style: str | Style = "") -> Content:
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
from rich.highlighter import ReprHighlighter
|
||||
from textual._markup_playground import MarkupPlayground
|
||||
|
||||
from textual import containers, on
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Static, TextArea
|
||||
|
||||
class MarkupApp(App):
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: horizontal;
|
||||
#editor {
|
||||
width: 1fr;
|
||||
border: tab $primary;
|
||||
padding: 1;
|
||||
margin: 1 1 0 1;
|
||||
}
|
||||
#results-container {
|
||||
margin: 1 1 0 1;
|
||||
border: tab $success;
|
||||
&.-error {
|
||||
border: tab $error;
|
||||
}
|
||||
}
|
||||
#results {
|
||||
width: 1fr;
|
||||
padding: 1 1;
|
||||
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield (text_area := TextArea(id="editor"))
|
||||
text_area.border_title = "Markup"
|
||||
|
||||
with containers.VerticalScroll(id="results-container") as container:
|
||||
yield Static(id="results")
|
||||
container.border_title = "Output"
|
||||
|
||||
@on(TextArea.Changed)
|
||||
def on_markup_changed(self, event: TextArea.Changed) -> None:
|
||||
results = self.query_one("#results", Static)
|
||||
try:
|
||||
results.update(event.text_area.text)
|
||||
except Exception as error:
|
||||
highlight = ReprHighlighter()
|
||||
# results.update(highlight(str(error)))
|
||||
from rich.traceback import Traceback
|
||||
|
||||
results.update(Traceback())
|
||||
self.query_one("#results-container").add_class("-error")
|
||||
else:
|
||||
self.query_one("#results-container").remove_class("-error")
|
||||
|
||||
app = MarkupApp()
|
||||
app = MarkupPlayground()
|
||||
app.run()
|
||||
|
||||
@@ -399,6 +399,7 @@ class Widget(DOMNode):
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True,
|
||||
) -> None:
|
||||
"""Initialize a Widget.
|
||||
|
||||
@@ -409,6 +410,7 @@ class Widget(DOMNode):
|
||||
classes: The CSS classes for the widget.
|
||||
disabled: Whether the widget is disabled or not.
|
||||
"""
|
||||
self._render_markup = markup
|
||||
_null_size = NULL_SIZE
|
||||
self._size = _null_size
|
||||
self._container_size = _null_size
|
||||
@@ -4034,22 +4036,23 @@ class Widget(DOMNode):
|
||||
yield
|
||||
|
||||
def render(self) -> RenderResult:
|
||||
"""Get text or Rich renderable for this widget.
|
||||
"""Get [content](./guide/content) for the widget.
|
||||
|
||||
Implement this for custom widgets.
|
||||
This method should return a string, a [Content][textual.content.Content] object, or a [Rich](https://github.com/Textualize/rich) renderable.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from textual.app import RenderableType
|
||||
from textual.app import RenderResult
|
||||
from textual.widget import Widget
|
||||
|
||||
class CustomWidget(Widget):
|
||||
def render(self) -> RenderableType:
|
||||
def render(self) -> RenderResult:
|
||||
return "Welcome to [bold red]Textual[/]!"
|
||||
```
|
||||
|
||||
Returns:
|
||||
Any renderable.
|
||||
A string or object to render as the widget's content.
|
||||
"""
|
||||
|
||||
if self.is_container:
|
||||
@@ -4070,7 +4073,7 @@ class Widget(DOMNode):
|
||||
if cached_visual is not None:
|
||||
assert isinstance(cached_visual, Visual)
|
||||
return cached_visual
|
||||
visual = visualize(self, self.render())
|
||||
visual = visualize(self, self.render(), markup=self._render_markup)
|
||||
self._layout_cache[cache_key] = visual
|
||||
return visual
|
||||
|
||||
|
||||
@@ -64,17 +64,18 @@ class Static(Widget, inherit_bindings=False):
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
) -> None:
|
||||
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
|
||||
super().__init__(
|
||||
name=name, id=id, classes=classes, disabled=disabled, markup=markup
|
||||
)
|
||||
self.expand = expand
|
||||
self.shrink = shrink
|
||||
self.markup = markup
|
||||
self._content = content
|
||||
self._visual: Visual | None = None
|
||||
|
||||
@property
|
||||
def visual(self) -> Visual:
|
||||
if self._visual is None:
|
||||
self._visual = visualize(self, self._content, markup=self.markup)
|
||||
self._visual = visualize(self, self._content, markup=self._render_markup)
|
||||
return self._visual
|
||||
|
||||
@property
|
||||
@@ -84,7 +85,7 @@ class Static(Widget, inherit_bindings=False):
|
||||
@renderable.setter
|
||||
def renderable(self, renderable: RenderableType | SupportsVisual) -> None:
|
||||
if isinstance(renderable, str):
|
||||
if self.markup:
|
||||
if self._render_markup:
|
||||
self._renderable = Text.from_markup(renderable)
|
||||
else:
|
||||
self._renderable = Text(renderable)
|
||||
@@ -109,5 +110,5 @@ class Static(Widget, inherit_bindings=False):
|
||||
"""
|
||||
|
||||
self._content = content
|
||||
self._visual = visualize(self, content, markup=self.markup)
|
||||
self._visual = visualize(self, content, markup=self._render_markup)
|
||||
self.refresh(layout=True)
|
||||
|
||||
Reference in New Issue
Block a user