# Widgets In this chapter we will explore widgets in more detail, and how you can create custom widgets of your own. ## What is a widget? A widget is a component of your UI responsible for managing a rectangular region of the screen. Widgets may respond to [events](./events.md) in much the same way as an app. In many respects, widgets are like mini-apps. !!! information Every widget runs in its own asyncio task. ## Custom widgets There is a growing collection of [builtin widgets](../widgets/index.md) in Textual, but you can build entirely custom widgets that work in the same way. The first step in building a widget is to import and extend a widget class. This can either be [Widget][textual.widget.Widget] which is the base class of all widgets, or one of its subclasses. Let's create a simple custom widget to display a greeting. ```python title="hello01.py" hl_lines="5-9" --8<-- "docs/examples/guide/widgets/hello01.py" ``` The highlighted lines define a custom widget class with just a [render()][textual.widget.Widget.render] method. Textual will display whatever is returned from render in the [content](./content.md) area of your widget. Note that the text contains tags in square brackets, i.e. `[b]`. This is [content markup](./content.md#markup) which allows you to embed various styles within your content. If you run this you will find that `World` is in bold. ```{.textual path="docs/examples/guide/widgets/hello01.py"} ``` This (very simple) custom widget may be [styled](./styles.md) in the same way as builtin widgets, and targeted with CSS. Let's add some CSS to this app. === "hello02.py" ```python title="hello02.py" hl_lines="13" --8<-- "docs/examples/guide/widgets/hello02.py" ``` === "hello02.tcss" ```css title="hello02.tcss" --8<-- "docs/examples/guide/widgets/hello02.tcss" ``` The addition of the CSS has completely transformed our custom widget. ```{.textual path="docs/examples/guide/widgets/hello02.py"} ``` ## Static widget While you can extend the Widget class, a subclass will typically be a better starting point. The [Static][textual.widgets.Static] class is a widget subclass which caches the result of render, and provides an [update()][textual.widgets.Static.update] method to update the content area. Let's use Static to create a widget which cycles through "hello" in various languages. === "hello03.py" ```python title="hello03.py" hl_lines="23-35" --8<-- "docs/examples/guide/widgets/hello03.py" ``` === "hello03.tcss" ```css title="hello03.tcss" --8<-- "docs/examples/guide/widgets/hello03.tcss" ``` === "Output" ```{.textual path="docs/examples/guide/widgets/hello03.py"} ``` Note that there is no `render()` method on this widget. The Static class is handling the render for us. Instead we call `update()` when we want to update the content within the widget. The `next_word` method updates the greeting. We call this method from the mount handler to get the first word, and from a click handler to cycle through the greetings when we click the widget. ### Default CSS When building an app it is best to keep your CSS in an external file. This allows you to see all your CSS in one place, and to enable live editing. However if you intend to distribute a widget (via PyPI for instance) it can be convenient to bundle the code and CSS together. You can do this by adding a `DEFAULT_CSS` class variable inside your widget class. Textual's builtin widgets bundle CSS in this way, which is why you can see nicely styled widgets without having to copy any CSS code. Here's the Hello example again, this time the widget has embedded default CSS: === "hello04.py" ```python title="hello04.py" hl_lines="26-35" --8<-- "docs/examples/guide/widgets/hello04.py" ``` === "hello04.tcss" ```css title="hello04.tcss" --8<-- "docs/examples/guide/widgets/hello04.tcss" ``` === "Output" ```{.textual path="docs/examples/guide/widgets/hello04.py"} ``` #### Scoped CSS Default CSS is *scoped* by default. All this means is that CSS defined in `DEFAULT_CSS` will affect the widget and potentially its children only. This is to prevent you from inadvertently breaking an unrelated widget. You can disable scoped CSS by setting the class var `SCOPED_CSS` to `False`. #### Default specificity CSS defined within `DEFAULT_CSS` has an automatically lower [specificity](./CSS.md#specificity) than CSS read from either the App's `CSS` class variable or an external stylesheet. In practice this means that your app's CSS will take precedence over any CSS bundled with widgets. ## Text links Text in a widget may be marked up with links which perform an action when clicked. Links in markup use the following format: ``` "Click [@click=app.bell]Me[/]" ``` The `@click` tag introduces a click handler, which runs the `app.bell` action. Let's use links in the hello example so that the greeting becomes a link which updates the widget. === "hello05.py" ```python title="hello05.py" hl_lines="23-32" --8<-- "docs/examples/guide/widgets/hello05.py" ``` === "hello05.tcss" ```css title="hello05.tcss" --8<-- "docs/examples/guide/widgets/hello05.tcss" ``` === "Output" ```{.textual path="docs/examples/guide/widgets/hello05.py"} ``` If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the `next_word` action which updates the next word. ## Border titles Every widget has a [`border_title`][textual.widgets.Widget.border_title] and [`border_subtitle`][textual.widgets.Widget.border_subtitle] attribute. Setting `border_title` will display text within the top border, and setting `border_subtitle` will display text within the bottom border. !!! note Border titles will only display if the widget has a [border](../styles/border.md) enabled. The default value for these attributes is empty string, which disables the title. You can change the default value for the title attributes with the [`BORDER_TITLE`][textual.widget.Widget.BORDER_TITLE] and [`BORDER_SUBTITLE`][textual.widget.Widget.BORDER_SUBTITLE] class variables. Let's demonstrate setting a title, both as a class variable and a instance variable: === "hello06.py" ```python title="hello06.py" hl_lines="26 30" --8<-- "docs/examples/guide/widgets/hello06.py" ``` 1. Setting the default for the `title` attribute via class variable. 2. Setting `subtitle` via an instance attribute. === "hello06.tcss" ```css title="hello06.tcss" --8<-- "docs/examples/guide/widgets/hello06.tcss" ``` === "Output" ```{.textual path="docs/examples/guide/widgets/hello06.py"} ``` Note that titles are limited to a single line of text. If the supplied text is too long to fit within the widget, it will be cropped (and an ellipsis added). There are a number of styles that influence how titles are displayed (color and alignment). See the [style reference](../styles/index.md) for details. ## Focus & keybindings Widgets can have a list of associated key [bindings](../guide/input.md#bindings), which let them call [actions](../guide/actions.md) in response to key presses. A widget is able to handle key presses if it or one of its descendants has [focus](../guide/input.md#input-focus). Widgets aren't focusable by default. To allow a widget to be focused, we need to set `can_focus=True` when defining a widget subclass. Here's an example of a simple focusable widget: === "counter01.py" ```python title="counter01.py" hl_lines="6" --8<-- "docs/examples/guide/widgets/counter01.py" ``` 1. Allow the widget to receive input focus. === "counter.tcss" ```css title="counter.tcss" hl_lines="6-11" --8<-- "docs/examples/guide/widgets/counter.tcss" ``` 1. These styles are applied only when the widget has focus. === "Output" ```{.textual path="docs/examples/guide/widgets/counter01.py"} ``` The app above contains three `Counter` widgets, which we can focus by clicking or using ++tab++ and ++shift+tab++. Now that our counter is focusable, let's add some keybindings to it to allow us to change the count using the keyboard. To do this, we add a `BINDINGS` class variable to `Counter`, with bindings for ++up++ and ++down++. These new bindings are linked to the `change_count` action, which updates the `count` reactive attribute. With our bindings in place, we can now change the count of the _currently focused_ counter using ++up++ and ++down++. === "counter02.py" ```python title="counter02.py" hl_lines="9-12 19-20" --8<-- "docs/examples/guide/widgets/counter02.py" ``` 1. Associates presses of ++up++ or ++k++ with the `change_count` action, passing `1` as the argument to increment the count. The final argument ("Increment") is a user-facing label displayed in the footer when this binding is active. 2. Called when the binding is triggered. Take care to add the `action_` prefix to the method name. === "counter.tcss" ```css title="counter.tcss" --8<-- "docs/examples/guide/widgets/counter.tcss" ``` 1. These styles are applied only when the widget has focus. === "Output" ```{.textual path="docs/examples/guide/widgets/counter02.py" press="up,tab,down,down"} ``` ## Rich renderables In previous examples we've set strings as content for Widgets. You can also use special objects called [renderables](https://rich.readthedocs.io/en/latest/protocol.html) for advanced visuals. You can use any renderable defined in [Rich](https://github.com/Textualize/rich) or third party libraries. Lets make a widget that uses a Rich table for its content. The following app is a solution to the classic [fizzbuzz](https://en.wikipedia.org/wiki/Fizz_buzz) problem often used to screen software engineers in job interviews. The problem is this: Count up from 1 to 100, when the number is divisible by 3, output "fizz"; when the number is divisible by 5, output "buzz"; and when the number is divisible by both 3 and 5 output "fizzbuzz". This app will "play" fizz buzz by displaying a table of the first 15 numbers and columns for fizz and buzz. === "fizzbuzz01.py" ```python title="fizzbuzz01.py" hl_lines="18" --8<-- "docs/examples/guide/widgets/fizzbuzz01.py" ``` === "fizzbuzz01.tcss" ```css title="fizzbuzz01.tcss" hl_lines="32-35" --8<-- "docs/examples/guide/widgets/fizzbuzz01.tcss" ``` === "Output" ```{.textual path="docs/examples/guide/widgets/fizzbuzz01.py"} ``` ## Content size Textual will auto-detect the dimensions of the content area from rich renderables if width or height is set to `auto`. You can override auto dimensions by implementing [get_content_width()][textual.widget.Widget.get_content_width] or [get_content_height()][textual.widget.Widget.get_content_height]. Let's modify the default width for the fizzbuzz example. By default, the table will be just wide enough to fix the columns. Let's force it to be 50 characters wide. === "fizzbuzz02.py" ```python title="fizzbuzz02.py" hl_lines="10 21-23" --8<-- "docs/examples/guide/widgets/fizzbuzz02.py" ``` === "fizzbuzz02.tcss" ```css title="fizzbuzz02.tcss" --8<-- "docs/examples/guide/widgets/fizzbuzz02.tcss" ``` === "Output" ```{.textual path="docs/examples/guide/widgets/fizzbuzz02.py"} ``` Note that we've added `expand=True` to tell the `Table` to expand beyond the optimal width, so that it fills the 50 characters returned by `get_content_width`. ## Tooltips Widgets can have *tooltips* which is content displayed when the user hovers the mouse over the widget. You can use tooltips to add supplementary information or help messages. !!! tip It is best not to rely on tooltips for essential information. Some users prefer to use the keyboard exclusively and may never see tooltips. To add a tooltip, assign to the widget's [`tooltip`][textual.widgets.Widget.tooltip] property. You can set text or any other [Rich](https://github.com/Textualize/rich) renderable. The following example adds a tooltip to a button: === "tooltip01.py" ```python title="tooltip01.py" --8<-- "docs/examples/guide/widgets/tooltip01.py" ``` === "Output (before hover)" ```{.textual path="docs/examples/guide/widgets/tooltip01.py"} ``` === "Output (after hover)" ```{.textual path="docs/examples/guide/widgets/tooltip01.py" hover="Button"} ``` ### Customizing the tooltip If you don't like the default look of the tooltips, you can customize them to your liking with CSS. Add a rule to your CSS that targets `Tooltip`. Here's an example: === "tooltip02.py" ```python title="tooltip02.py" hl_lines="15-19" --8<-- "docs/examples/guide/widgets/tooltip02.py" ``` === "Output (before hover)" ```{.textual path="docs/examples/guide/widgets/tooltip02.py"} ``` === "Output (after hover)" ```{.textual path="docs/examples/guide/widgets/tooltip02.py" hover="Button"} ``` ## Loading indicator Widgets have a [`loading`][textual.widget.Widget.loading] reactive which when set to `True` will temporarily replace your widget with a [`LoadingIndicator`](../widgets/loading_indicator.md). You can use this to indicate to the user that the app is currently working on getting data, and there will be content when that data is available. Let's look at an example of this. === "loading01.py" ```python title="loading01.py" --8<-- "docs/examples/guide/widgets/loading01.py" ``` 1. Shows the loading indicator in place of the data table. 2. Insert a random sleep to simulate a network request. 3. Show the new data. === "Output" ```{.textual path="docs/examples/guide/widgets/loading01.py"} ``` In this example we have four [DataTable](../widgets/data_table.md) widgets, which we put into a loading state by setting the widget's `loading` property to `True`. This will temporarily replace the widget with a loading indicator animation. When the (simulated) data has been retrieved, we reset the `loading` property to show the new data. !!! tip See the guide on [Workers](./workers.md) if you want to know more about the `@work` decorator. ## Line API A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size. If a widget is large enough to require scrolling, or updates frequently, then this redrawing can make your app feel less responsive. Textual offers an alternative API which reduces the amount of work required to refresh a widget, and makes it possible to update portions of a widget (as small as a single character) without a full redraw. This is known as the *line API*. !!! note The Line API requires a little more work that typical Rich renderables, but can produce powerful widgets such as the builtin [DataTable](./../widgets/data_table.md) which can handle thousands or even millions of rows. ### Render Line method To build a widget with the line API, implement a `render_line` method rather than a `render` method. The `render_line` method takes a single integer argument `y` which is an offset from the top of the widget, and should return a [Strip][textual.strip.Strip] object containing that line's content. Textual will call this method as required to get content for every row of characters in the widget.
Hello, **World**!
The `Strip` constructor has an optional second parameter, which should be the *cell length* of the strip. The strip above has a length of 13, so we could have constructed it like this:
```python
strip = Strip(segments, 13)
```
Note that the cell length parameter is _not_ the total number of characters in the string. It is the number of terminal "cells". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to omit the length parameter so that Textual calculates it automatically.
### Component classes
When applying styles to widgets we can use CSS to select the child widgets. Widgets rendered with the line API don't have children per-se, but we can still use CSS to apply styles to parts of our widget by defining *component classes*. Component classes are associated with a widget by defining a `COMPONENT_CLASSES` class variable which should be a `set` of strings containing CSS class names.
In the checkerboard example above we hard-coded the color of the squares to "white" and "black". But what if we want to create a checkerboard with different colors? We can do this by defining two component classes, one for the "white" squares and one for the "dark" squares. This will allow us to change the colors with CSS.
The following example replaces our hard-coded colors with component classes.
=== "checker02.py"
```python title="checker02.py" hl_lines="11-13 16-23 35-36"
--8<-- "docs/examples/guide/widgets/checker02.py"
```
=== "Output"
```{.textual path="docs/examples/guide/widgets/checker02.py"}
```
The `COMPONENT_CLASSES` class variable above adds two class names: `checkerboard--white-square` and `checkerboard--black-square`. These are set in the `DEFAULT_CSS` but can modified in the app's `CSS` class variable or external CSS.
!!! tip
Component classes typically begin with the name of the widget followed by *two* hyphens. This is a convention to avoid potential name clashes.
The `render_line` method calls [get_component_rich_style][textual.widget.Widget.get_component_rich_style] to get `Style` objects from the CSS, which we apply to the segments to create a more colorful looking checkerboard.
### Scrolling
A Line API widget can be made to scroll by extending the [ScrollView][textual.scroll_view.ScrollView] class (rather than `Widget`).
The `ScrollView` class will do most of the work, but we will need to manage the following details:
1. The `ScrollView` class requires a *virtual size*, which is the size of the scrollable content and should be set via the `virtual_size` property. If this is larger than the widget then Textual will add scrollbars.
2. We need to update the `render_line` method to generate strips for the visible area of the widget, taking into account the current position of the scrollbars.
Let's add scrolling to our checkerboard example. A standard 8 x 8 board isn't sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares.
=== "checker03.py"
```python title="checker03.py" hl_lines="4 26-30 35-36 52-53"
--8<-- "docs/examples/guide/widgets/checker03.py"
```
=== "Output"
```{.textual path="docs/examples/guide/widgets/checker03.py"}
```
The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). You can update the `virtual_size` attribute dynamically as required, but our checkerboard isn't going to change size so we only need to set it once.
The `render_line` method gets the *scroll offset* which is an [Offset][textual.geometry.Offset] containing the current position of the scrollbars. We add `scroll_offset.y` to the `y` argument because `y` is relative to the top of the widget, and we need a Y coordinate relative to the scrollable content.
We also need to compensate for the position of the horizontal scrollbar. This is done in the call to `strip.crop` which *crops* the strip to the visible area between `scroll_x` and `scroll_x + self.size.width`.
!!! tip
[Strip][textual.strip.Strip] objects are immutable, so methods will return a new Strip rather than modifying the original.