Skip to content

Commit 08f85e9

Browse files
authored
Merge pull request #4985 from Textualize/docs-updates-11sep24
Add `can_focus` to guide, mention how `BINDINGS` are checked
2 parents 6c3b8a9 + 2c28b31 commit 08f85e9

File tree

5 files changed

+147
-0
lines changed

5 files changed

+147
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Counter {
2+
background: $panel-darken-1;
3+
padding: 1 2;
4+
color: $text-muted;
5+
6+
&:focus { /* (1)! */
7+
background: $primary;
8+
color: $text;
9+
text-style: bold;
10+
outline-left: thick $accent;
11+
}
12+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from textual.app import App, ComposeResult, RenderResult
2+
from textual.reactive import reactive
3+
from textual.widgets import Footer, Static
4+
5+
6+
class Counter(Static, can_focus=True): # (1)!
7+
"""A counter that can be incremented and decremented by pressing keys."""
8+
9+
count = reactive(0)
10+
11+
def render(self) -> RenderResult:
12+
return f"Count: {self.count}"
13+
14+
15+
class CounterApp(App[None]):
16+
CSS_PATH = "counter.tcss"
17+
18+
def compose(self) -> ComposeResult:
19+
yield Counter()
20+
yield Counter()
21+
yield Counter()
22+
yield Footer()
23+
24+
25+
if __name__ == "__main__":
26+
app = CounterApp()
27+
app.run()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from textual.app import App, ComposeResult, RenderResult
2+
from textual.reactive import reactive
3+
from textual.widgets import Footer, Static
4+
5+
6+
class Counter(Static, can_focus=True):
7+
"""A counter that can be incremented and decremented by pressing keys."""
8+
9+
BINDINGS = [
10+
("up,k", "change_count(1)", "Increment"), # (1)!
11+
("down,j", "change_count(-1)", "Decrement"),
12+
]
13+
14+
count = reactive(0)
15+
16+
def render(self) -> RenderResult:
17+
return f"Count: {self.count}"
18+
19+
def action_change_count(self, amount: int) -> None: # (2)!
20+
self.count += amount
21+
22+
23+
class CounterApp(App[None]):
24+
CSS_PATH = "counter.tcss"
25+
26+
def compose(self) -> ComposeResult:
27+
yield Counter()
28+
yield Counter()
29+
yield Counter()
30+
yield Footer()
31+
32+
33+
if __name__ == "__main__":
34+
app = CounterApp()
35+
app.run()

docs/guide/input.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,16 @@ The app splits the screen in to quarters, with a `RichLog` widget in each quarte
113113

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

116+
### Focusable widgets
117+
118+
Each widget has a boolean `can_focus` attribute which determines if it is capable of receiving focus.
119+
Note that `can_focus=True` does not mean the widget will _always_ be focusable.
120+
For example, a disabled widget cannot receive focus even if `can_focus` is `True`.
121+
116122
### Controlling focus
117123

118124
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.
125+
By default, Textual will focus the first focusable widget when the app starts.
119126

120127
### Focus events
121128

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

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

159169
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.

docs/guide/widgets.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,69 @@ If the supplied text is too long to fit within the widget, it will be cropped (a
190190
There are a number of styles that influence how titles are displayed (color and alignment).
191191
See the [style reference](../styles/index.md) for details.
192192

193+
## Focus & keybindings
194+
195+
Widgets can have a list of associated key [bindings](../guide/input.md#bindings),
196+
which let them call [actions](../guide/actions.md) in response to key presses.
197+
198+
A widget is able to handle key presses if it or one of its descendants has [focus](../guide/input.md#input-focus).
199+
200+
Widgets aren't focusable by default.
201+
To allow a widget to be focused, we need to set `can_focus=True` when defining a widget subclass.
202+
Here's an example of a simple focusable widget:
203+
204+
=== "counter01.py"
205+
206+
```python title="counter01.py" hl_lines="6"
207+
--8<-- "docs/examples/guide/widgets/counter01.py"
208+
```
209+
210+
1. Allow the widget to receive input focus.
211+
212+
=== "counter.tcss"
213+
214+
```css title="counter.tcss" hl_lines="6-11"
215+
--8<-- "docs/examples/guide/widgets/counter.tcss"
216+
```
217+
218+
1. These styles are applied only when the widget has focus.
219+
220+
=== "Output"
221+
222+
```{.textual path="docs/examples/guide/widgets/counter01.py"}
223+
```
224+
225+
226+
The app above contains three `Counter` widgets, which we can focus by clicking or using ++tab++ and ++shift+tab++.
227+
228+
Now that our counter is focusable, let's add some keybindings to it to allow us to change the count using the keyboard.
229+
To do this, we add a `BINDINGS` class variable to `Counter`, with bindings for ++up++ and ++down++.
230+
These new bindings are linked to the `change_count` action, which updates the `count` reactive attribute.
231+
232+
With our bindings in place, we can now change the count of the _currently focused_ counter using ++up++ and ++down++.
233+
234+
=== "counter02.py"
235+
236+
```python title="counter02.py" hl_lines="9-12 19-20"
237+
--8<-- "docs/examples/guide/widgets/counter02.py"
238+
```
239+
240+
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.
241+
2. Called when the binding is triggered. Take care to add the `action_` prefix to the method name.
242+
243+
=== "counter.tcss"
244+
245+
```css title="counter.tcss"
246+
--8<-- "docs/examples/guide/widgets/counter.tcss"
247+
```
248+
249+
1. These styles are applied only when the widget has focus.
250+
251+
=== "Output"
252+
253+
```{.textual path="docs/examples/guide/widgets/counter02.py" press="up,tab,down,down"}
254+
```
255+
193256
## Rich renderables
194257

195258
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.

0 commit comments

Comments
 (0)