Skip to content

Textual 0.12.0 adds syntactical sugar and batch updates

It's been just 9 days since the previous release, but we have a few interesting enhancements to the Textual API to talk about.

Better compose

We've added a little syntactical sugar to Textual's compose methods, which aids both readability and editability (that might not be a word).

First, let's look at the old way of building compose methods. This snippet is taken from the textual colors command.

for color_name in ColorSystem.COLOR_NAMES:

    items: list[Widget] = [ColorLabel(f'"{color_name}"')]
    for level in LEVELS:
        color = f"{color_name}-{level}" if level else color_name
        item = ColorItem(
            ColorBar(f"${color}", classes="text label"),
            ColorBar("$text-muted", classes="muted"),
            ColorBar("$text-disabled", classes="disabled"),
            classes=color,
        )
        items.append(item)

    yield ColorGroup(*items, id=f"group-{color_name}")

This code composes the following color swatches:

ColorsApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ primary ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁ secondary"primary" ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ background$primary-darken-3$text-muted$text-disabled ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ primary-background$primary-darken-2$text-muted$text-disabled ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ secondary-background$primary-darken-1$text-muted$text-disabled ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ surface$primary$text-muted$text-disabled ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ panel$primary-lighten-1$text-muted$text-disabled ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ boost$primary-lighten-2$text-muted$text-disabled ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ warning$primary-lighten-3$text-muted$text-disabled ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ error ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ success ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ accent"secondary" ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁  D  Toggle dark mode 

Tip

You can see this by running textual colors from the command line.

The old way was not all that bad, but it did make it hard to see the structure of your app at-a-glance, and editing compose methods always felt a little laborious.

Here's the new syntax, which uses context managers to add children to containers:

for color_name in ColorSystem.COLOR_NAMES:
    with ColorGroup(id=f"group-{color_name}"):
        yield Label(f'"{color_name}"')
        for level in LEVELS:
            color = f"{color_name}-{level}" if level else color_name
            with ColorItem(classes=color):
                yield ColorBar(f"${color}", classes="text label")
                yield ColorBar("$text-muted", classes="muted")
                yield ColorBar("$text-disabled", classes="disabled")

The context manager approach generally results in fewer lines of code, and presents attributes on the same line as containers themselves. Additionally, adding widgets to a container can be as simple is indenting them.

You can still construct widgets and containers with positional arguments, but this new syntax is preferred. It's not documented yet, but you can start using it now. We will be updating our examples in the next few weeks.

Batch updates

Textual is smart about performing updates to the screen. When you make a change that might repaint the screen, those changes don't happen immediately. Textual makes a note of them, and repaints the screen a short time later (around a 1/60th of a second). Multiple updates are combined so that Textual does less work overall, and there is none of the flicker you might get with multiple repaints.

Although this works very well, it is possible to introduce a little flicker if you make changes across multiple widgets. And especially if you add or remove many widgets at once. To combat this we have added a batch_update context manager which tells Textual to disable screen updates until the end of the with block.

The new Markdown widget uses this context manager when it updates its content. Here's the code:

with self.app.batch_update():
    await self.query("MarkdownBlock").remove()
    await self.mount_all(output)

Without the batch update there are a few frames where the old markdown blocks are removed and the new blocks are added (which would be perceived as a brief flicker). With the update, the update appears instant.

Disabled widgets

A few widgets (such as Button) had a disabled attribute which would fade the widget a little and make it unselectable. We've extended this to all widgets. Although it is particularly applicable to input controls, anything may be disabled. Disabling a container makes its children disabled, so you could use this for disabling a form, for example.

Tip

Disabled widgets may be styled with the :disabled CSS pseudo-selector.

Preventing messages

Also in this release is another context manager, which will disable specified Message types. This doesn't come up as a requirement very often, but it can be very useful when it does. This one is documented, see Preventing events for details.

Full changelog

As always see the release page for additional changes and bug fixes.

Join us!

We're having fun on our Discord server. Join us there to talk to Textualize developers and share ideas.