Skip to content

Commit

Permalink
Merge pull request #4985 from Textualize/docs-updates-11sep24
Browse files Browse the repository at this point in the history
Add `can_focus` to guide, mention how `BINDINGS` are checked
  • Loading branch information
willmcgugan authored Oct 3, 2024
2 parents 6c3b8a9 + 2c28b31 commit 08f85e9
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 0 deletions.
12 changes: 12 additions & 0 deletions docs/examples/guide/widgets/counter.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Counter {
background: $panel-darken-1;
padding: 1 2;
color: $text-muted;

&:focus { /* (1)! */
background: $primary;
color: $text;
text-style: bold;
outline-left: thick $accent;
}
}
27 changes: 27 additions & 0 deletions docs/examples/guide/widgets/counter01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from textual.app import App, ComposeResult, RenderResult
from textual.reactive import reactive
from textual.widgets import Footer, Static


class Counter(Static, can_focus=True): # (1)!
"""A counter that can be incremented and decremented by pressing keys."""

count = reactive(0)

def render(self) -> RenderResult:
return f"Count: {self.count}"


class CounterApp(App[None]):
CSS_PATH = "counter.tcss"

def compose(self) -> ComposeResult:
yield Counter()
yield Counter()
yield Counter()
yield Footer()


if __name__ == "__main__":
app = CounterApp()
app.run()
35 changes: 35 additions & 0 deletions docs/examples/guide/widgets/counter02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from textual.app import App, ComposeResult, RenderResult
from textual.reactive import reactive
from textual.widgets import Footer, Static


class Counter(Static, can_focus=True):
"""A counter that can be incremented and decremented by pressing keys."""

BINDINGS = [
("up,k", "change_count(1)", "Increment"), # (1)!
("down,j", "change_count(-1)", "Decrement"),
]

count = reactive(0)

def render(self) -> RenderResult:
return f"Count: {self.count}"

def action_change_count(self, amount: int) -> None: # (2)!
self.count += amount


class CounterApp(App[None]):
CSS_PATH = "counter.tcss"

def compose(self) -> ComposeResult:
yield Counter()
yield Counter()
yield Counter()
yield Footer()


if __name__ == "__main__":
app = CounterApp()
app.run()
10 changes: 10 additions & 0 deletions docs/guide/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,16 @@ The app splits the screen in to quarters, with a `RichLog` widget in each quarte

You can move focus by pressing the ++tab++ key to focus the next widget. Pressing ++shift+tab++ moves the focus in the opposite direction.

### Focusable widgets

Each widget has a boolean `can_focus` attribute which determines if it is capable of receiving focus.
Note that `can_focus=True` does not mean the widget will _always_ be focusable.
For example, a disabled widget cannot receive focus even if `can_focus` is `True`.

### Controlling focus

Textual will handle keyboard focus automatically, but you can tell Textual to focus a widget by calling the widget's [focus()][textual.widget.Widget.focus] method.
By default, Textual will focus the first focusable widget when the app starts.

### Focus events

Expand Down Expand Up @@ -154,6 +161,9 @@ Note how the footer displays bindings and makes them clickable.
Multiple keys can be bound to a single action by comma-separating them.
For example, `("r,t", "add_bar('red')", "Add Red")` means both ++r++ and ++t++ are bound to `add_bar('red')`.

When you press a key, Textual will first check for a matching binding in the `BINDINGS` list of the currently focused widget.
If no match is found, it will search upwards through the DOM all the way up to the `App` looking for a match.

### Binding class

The tuple of three strings may be enough for simple bindings, but you can also replace the tuple with a [Binding][textual.binding.Binding] instance which exposes a few more options.
Expand Down
63 changes: 63 additions & 0 deletions docs/guide/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,69 @@ If the supplied text is too long to fit within the widget, it will be cropped (a
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.
Expand Down

0 comments on commit 08f85e9

Please sign in to comment.