From 295f95b430931f2a86cbcafc53256b77ddc94b23 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 11 Sep 2024 13:18:42 +0100 Subject: [PATCH 01/19] Add section on "focusable widgets" to "Input" page of guide --- docs/guide/input.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/guide/input.md b/docs/guide/input.md index 4e997cc1c2..1c083c28b6 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -121,6 +121,12 @@ Textual will handle keyboard focus automatically, but you can tell Textual to fo When a widget receives focus, it is sent a [Focus](../events/focus.md) event. When a widget loses focus it is sent a [Blur](../events/blur.md) event. +### 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`. + ## Bindings Keys may be associated with [actions](../guide/actions.md) for a given widget. This association is known as a key _binding_. From fde23733fc644a7f8df5a60761cc7fa2318db6dd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 11 Sep 2024 14:01:40 +0100 Subject: [PATCH 02/19] Mention `can_focus` in docs, note on how bindings are checked --- docs/guide/input.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index 1c083c28b6..30713bfb1b 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -113,20 +113,21 @@ 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 When a widget receives focus, it is sent a [Focus](../events/focus.md) event. When a widget loses focus it is sent a [Blur](../events/blur.md) event. -### 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`. - ## Bindings Keys may be associated with [actions](../guide/actions.md) for a given widget. This association is known as a key _binding_. @@ -160,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. From 6f6c987f12816854658347b37413298cda2cb36d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Sep 2024 13:35:33 +0100 Subject: [PATCH 03/19] Docs example --- docs/guide/widgets.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index add6080b3f..f076f6471c 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -51,6 +51,32 @@ The addition of the CSS has completely transformed our custom widget. ```{.textual path="docs/examples/guide/widgets/hello02.py"} ``` +### Responding to key presses + +Widgets can have a list of key [bindings](../guide/input.md#bindings) associated with them. +This enables a widget to call [actions](../guide/actions.md) in response to key presses. + +A widget's bindings will only be checked if it or one of its descendants has focus. + +Let's look at Textual's builtin [Button](../widgets/button.md) widget to see an example of how widget bindings work. +The `Button` widget has a single binding for the `enter` key. +When a button is focused, and the user presses ++enter++, the `action_press` method inside button is called. + +```python +class Button(Widget, can_focus=True): # (1)! + BINDINGS = [Binding("enter", "press", "Press Button", show=False)] # (2)! + # ... + def action_press(self) -> None: # (3)! + self.press() +``` + +1. The `Button` has `can_focus=True` to ensure it can be focused and therefore handle bindings. +2. It has a binding which associates the ++enter++ key with the `action_press` method. +3. `action_press` will be called when the user presses ++enter++ while the button is focused. + +Note that widgets cannot be focused by default. +Setting `can_focus=True` is required to make a widget focusable. + ## 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. From 3e1b4131f00aac40d00fb904cdc334d09fb16e22 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 12 Sep 2024 13:37:08 +0100 Subject: [PATCH 04/19] typo --- docs/guide/widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index f076f6471c..94c1c6f214 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -60,7 +60,7 @@ A widget's bindings will only be checked if it or one of its descendants has foc Let's look at Textual's builtin [Button](../widgets/button.md) widget to see an example of how widget bindings work. The `Button` widget has a single binding for the `enter` key. -When a button is focused, and the user presses ++enter++, the `action_press` method inside button is called. +When a button is focused, and the user presses ++enter++, the `action_press` method inside `Button` is called. ```python class Button(Widget, can_focus=True): # (1)! From 0fb0e2b7f7e28fd4feeb995d802a3b352855215c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 24 Sep 2024 14:08:08 +0100 Subject: [PATCH 05/19] Simplify wording, add counter example --- docs/examples/guide/widgets/bindings.py | 32 +++++++++++++++++++++++++ docs/guide/widgets.md | 17 ++----------- 2 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 docs/examples/guide/widgets/bindings.py diff --git a/docs/examples/guide/widgets/bindings.py b/docs/examples/guide/widgets/bindings.py new file mode 100644 index 0000000000..82096ff2ab --- /dev/null +++ b/docs/examples/guide/widgets/bindings.py @@ -0,0 +1,32 @@ +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.reactive import reactive +from textual.widgets import Static + + +class Counter(Static, can_focus=True): + """A counter that can be incremented and decremented by pressing keys.""" + + BINDINGS = [ + Binding("up", "change_count(1)", "Increment"), + Binding("down", "change_count(-1)", "Decrement"), + ] + + count = reactive(0) + + def render(self) -> str: + return f"Count: {self.count}" + + def action_change_count(self, amount: int) -> None: + self.count += amount + + +class CalculatorApp(App[None]): + def compose(self) -> ComposeResult: + yield Counter() + yield Counter() + + +if __name__ == "__main__": + app = CalculatorApp() + app.run() diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 94c1c6f214..b53e7596ca 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -53,8 +53,8 @@ The addition of the CSS has completely transformed our custom widget. ### Responding to key presses -Widgets can have a list of key [bindings](../guide/input.md#bindings) associated with them. -This enables a widget to call [actions](../guide/actions.md) in response to key presses. +Widgets can have a list of associated key [bindings](../guide/input.md#bindings), +which enable a it to call [actions](../guide/actions.md) in response to key presses. A widget's bindings will only be checked if it or one of its descendants has focus. @@ -62,20 +62,7 @@ Let's look at Textual's builtin [Button](../widgets/button.md) widget to see an The `Button` widget has a single binding for the `enter` key. When a button is focused, and the user presses ++enter++, the `action_press` method inside `Button` is called. -```python -class Button(Widget, can_focus=True): # (1)! - BINDINGS = [Binding("enter", "press", "Press Button", show=False)] # (2)! - # ... - def action_press(self) -> None: # (3)! - self.press() -``` - -1. The `Button` has `can_focus=True` to ensure it can be focused and therefore handle bindings. -2. It has a binding which associates the ++enter++ key with the `action_press` method. -3. `action_press` will be called when the user presses ++enter++ while the button is focused. -Note that widgets cannot be focused by default. -Setting `can_focus=True` is required to make a widget focusable. ## Static widget From 8fc2bd1ccd08429c7be125af2aef755f9204f3c2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 24 Sep 2024 14:15:34 +0100 Subject: [PATCH 06/19] Basic CSS for counter example --- .../guide/widgets/{bindings.py => calculator.py} | 9 ++++++--- docs/examples/guide/widgets/calculator.tcss | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) rename docs/examples/guide/widgets/{bindings.py => calculator.py} (74%) create mode 100644 docs/examples/guide/widgets/calculator.tcss diff --git a/docs/examples/guide/widgets/bindings.py b/docs/examples/guide/widgets/calculator.py similarity index 74% rename from docs/examples/guide/widgets/bindings.py rename to docs/examples/guide/widgets/calculator.py index 82096ff2ab..d27b17c858 100644 --- a/docs/examples/guide/widgets/bindings.py +++ b/docs/examples/guide/widgets/calculator.py @@ -1,15 +1,15 @@ from textual.app import App, ComposeResult from textual.binding import Binding from textual.reactive import reactive -from textual.widgets import Static +from textual.widgets import Footer, Static class Counter(Static, can_focus=True): """A counter that can be incremented and decremented by pressing keys.""" BINDINGS = [ - Binding("up", "change_count(1)", "Increment"), - Binding("down", "change_count(-1)", "Decrement"), + Binding("up,k", "change_count(1)", "Increment"), + Binding("down,j", "change_count(-1)", "Decrement"), ] count = reactive(0) @@ -22,9 +22,12 @@ def action_change_count(self, amount: int) -> None: class CalculatorApp(App[None]): + CSS_PATH = "calculator.tcss" + def compose(self) -> ComposeResult: yield Counter() yield Counter() + yield Footer() if __name__ == "__main__": diff --git a/docs/examples/guide/widgets/calculator.tcss b/docs/examples/guide/widgets/calculator.tcss new file mode 100644 index 0000000000..06f7660004 --- /dev/null +++ b/docs/examples/guide/widgets/calculator.tcss @@ -0,0 +1,12 @@ +Counter { + background: $panel-darken-1; + padding: 1 2; + color: $text-muted; + + &:focus { + background: $primary; + color: $text; + text-style: bold; + outline-left: thick $accent; + } +} From 715080cd1d1b53c8f79d7271f222494ecb84c599 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 24 Sep 2024 14:16:08 +0100 Subject: [PATCH 07/19] Renaming calculator to counter in example --- docs/examples/guide/widgets/{calculator.py => counter.py} | 6 +++--- .../guide/widgets/{calculator.tcss => counter.tcss} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename docs/examples/guide/widgets/{calculator.py => counter.py} (89%) rename docs/examples/guide/widgets/{calculator.tcss => counter.tcss} (100%) diff --git a/docs/examples/guide/widgets/calculator.py b/docs/examples/guide/widgets/counter.py similarity index 89% rename from docs/examples/guide/widgets/calculator.py rename to docs/examples/guide/widgets/counter.py index d27b17c858..29a038e2c8 100644 --- a/docs/examples/guide/widgets/calculator.py +++ b/docs/examples/guide/widgets/counter.py @@ -21,8 +21,8 @@ def action_change_count(self, amount: int) -> None: self.count += amount -class CalculatorApp(App[None]): - CSS_PATH = "calculator.tcss" +class CounterApp(App[None]): + CSS_PATH = "counter.tcss" def compose(self) -> ComposeResult: yield Counter() @@ -31,5 +31,5 @@ def compose(self) -> ComposeResult: if __name__ == "__main__": - app = CalculatorApp() + app = CounterApp() app.run() diff --git a/docs/examples/guide/widgets/calculator.tcss b/docs/examples/guide/widgets/counter.tcss similarity index 100% rename from docs/examples/guide/widgets/calculator.tcss rename to docs/examples/guide/widgets/counter.tcss From 176cd5ebf56113576de89b1d3dd829aab2dc71ed Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 24 Sep 2024 14:56:28 +0100 Subject: [PATCH 08/19] More docs --- docs/examples/guide/widgets/counter01.py | 26 ++++++++++ .../widgets/{counter.py => counter02.py} | 7 ++- docs/guide/widgets.md | 48 ++++++++++++++----- 3 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 docs/examples/guide/widgets/counter01.py rename docs/examples/guide/widgets/{counter.py => counter02.py} (75%) diff --git a/docs/examples/guide/widgets/counter01.py b/docs/examples/guide/widgets/counter01.py new file mode 100644 index 0000000000..e6451f0e93 --- /dev/null +++ b/docs/examples/guide/widgets/counter01.py @@ -0,0 +1,26 @@ +from textual.app import App, ComposeResult +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.""" + + count = reactive(0) + + def render(self) -> str: + return f"Count: {self.count}" + + +class CounterApp(App[None]): + CSS_PATH = "counter.tcss" + + def compose(self) -> ComposeResult: + yield Counter() + yield Counter() + yield Footer() + + +if __name__ == "__main__": + app = CounterApp() + app.run() diff --git a/docs/examples/guide/widgets/counter.py b/docs/examples/guide/widgets/counter02.py similarity index 75% rename from docs/examples/guide/widgets/counter.py rename to docs/examples/guide/widgets/counter02.py index 29a038e2c8..6794ab60cf 100644 --- a/docs/examples/guide/widgets/counter.py +++ b/docs/examples/guide/widgets/counter02.py @@ -1,5 +1,4 @@ from textual.app import App, ComposeResult -from textual.binding import Binding from textual.reactive import reactive from textual.widgets import Footer, Static @@ -8,8 +7,8 @@ class Counter(Static, can_focus=True): """A counter that can be incremented and decremented by pressing keys.""" BINDINGS = [ - Binding("up,k", "change_count(1)", "Increment"), - Binding("down,j", "change_count(-1)", "Decrement"), + ("up,k", "change_count(1)", "Increment"), + ("down,j", "change_count(-1)", "Decrement"), ] count = reactive(0) @@ -17,7 +16,7 @@ class Counter(Static, can_focus=True): def render(self) -> str: return f"Count: {self.count}" - def action_change_count(self, amount: int) -> None: + def action_change_count(self, amount: int): self.count += amount diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index b53e7596ca..8a77d1a6b3 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -51,19 +51,6 @@ The addition of the CSS has completely transformed our custom widget. ```{.textual path="docs/examples/guide/widgets/hello02.py"} ``` -### Responding to key presses - -Widgets can have a list of associated key [bindings](../guide/input.md#bindings), -which enable a it to call [actions](../guide/actions.md) in response to key presses. - -A widget's bindings will only be checked if it or one of its descendants has focus. - -Let's look at Textual's builtin [Button](../widgets/button.md) widget to see an example of how widget bindings work. -The `Button` widget has a single binding for the `enter` key. -When a button is focused, and the user presses ++enter++, the `action_press` method inside `Button` is called. - - - ## 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. @@ -203,6 +190,41 @@ 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. +## Interacting with widgets + +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 only be able to handle key presses if it or one of its descendants has [focus](./guide/input.md#input-focus). + +Let's design a simple interactive `Counter` widget. +We'll set `can_focus=True` to allow the widget to receive focus, and give it some simple to highlight it when it has focus: + +=== "counter01.py" + + ```python title="counter01.py" hl_lines="6" + --8<-- "docs/examples/guide/widgets/counter01.py" + ``` + +=== "counter.tcss" + + ```css title="counter.tcss" + --8<-- "docs/examples/guide/widgets/counter.tcss" + ``` + +=== "Output" + + ```{.textual path="docs/examples/guide/widgets/counter01.py"} + ``` + +Textual will automatically focus the first counter, and you'll notice that it's been highlighted with a blue background thanks to the CSS we applied using the `:focus` pseudo-selector! ++tab++ and ++shift+tab++ moves focus between the two counters. + +Now that we have a focusable widget, let's add some key bindings to it for incrementing and decrementing the counter. +To do this, we'll add a `BINDINGS` class variable to the `Counter`. + + + + ## 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. From 86a6fcc18dcce2f85abbe052d456a60207fd3ad5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 25 Sep 2024 11:42:14 +0100 Subject: [PATCH 09/19] Counter example --- docs/examples/guide/widgets/counter01.py | 1 + docs/examples/guide/widgets/counter02.py | 1 + docs/guide/widgets.md | 31 +++++++++++++++++++----- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/docs/examples/guide/widgets/counter01.py b/docs/examples/guide/widgets/counter01.py index e6451f0e93..15bbf8b10f 100644 --- a/docs/examples/guide/widgets/counter01.py +++ b/docs/examples/guide/widgets/counter01.py @@ -16,6 +16,7 @@ class CounterApp(App[None]): CSS_PATH = "counter.tcss" def compose(self) -> ComposeResult: + yield Counter() yield Counter() yield Counter() yield Footer() diff --git a/docs/examples/guide/widgets/counter02.py b/docs/examples/guide/widgets/counter02.py index 6794ab60cf..879c87b752 100644 --- a/docs/examples/guide/widgets/counter02.py +++ b/docs/examples/guide/widgets/counter02.py @@ -24,6 +24,7 @@ class CounterApp(App[None]): CSS_PATH = "counter.tcss" def compose(self) -> ComposeResult: + yield Counter() yield Counter() yield Counter() yield Footer() diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 8a77d1a6b3..ac09f1f4da 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -197,8 +197,9 @@ which let them call [actions](../guide/actions.md) in response to key presses. A widget is only be able to handle key presses if it or one of its descendants has [focus](./guide/input.md#input-focus). -Let's design a simple interactive `Counter` widget. -We'll set `can_focus=True` to allow the widget to receive focus, and give it some simple to highlight it when it has focus: +To demonstrate, let's design a simple interactive counter widget which can be incremented and decremented using the keyboard. + +In the following example, we define a simple `Counter` widget with `can_focus=True`, and some CSS to make it stand out when focused. === "counter01.py" @@ -208,7 +209,7 @@ We'll set `can_focus=True` to allow the widget to receive focus, and give it som === "counter.tcss" - ```css title="counter.tcss" + ```css title="counter.tcss" hl_lines="6-11" --8<-- "docs/examples/guide/widgets/counter.tcss" ``` @@ -217,12 +218,30 @@ We'll set `can_focus=True` to allow the widget to receive focus, and give it som ```{.textual path="docs/examples/guide/widgets/counter01.py"} ``` -Textual will automatically focus the first counter, and you'll notice that it's been highlighted with a blue background thanks to the CSS we applied using the `:focus` pseudo-selector! ++tab++ and ++shift+tab++ moves focus between the two counters. +Notice that Textual automatically focused the first widget, and that pressing ++tab++ and ++shift+tab++ will move focus between widgets. + +Now that our counter is focusable, let's add some keybindings for incrementing and decrementing the counter. +To do this, we add a `BINDINGS` class variable to `Counter`, with bindings for incrementing and decrementing the counter using ++up++ and ++down++ respectively. +These new bindings are linked to the `change_count` action, which updates the `count` reactive attribute. + +=== "counter02.py" + + ```python title="counter02.py" hl_lines="9-12 19-20" + --8<-- "docs/examples/guide/widgets/counter02.py" + ``` + +=== "counter.tcss" -Now that we have a focusable widget, let's add some key bindings to it for incrementing and decrementing the counter. -To do this, we'll add a `BINDINGS` class variable to the `Counter`. + ```css title="counter.tcss" + --8<-- "docs/examples/guide/widgets/counter.tcss" + ``` +=== "Output" + + ```{.textual path="docs/examples/guide/widgets/counter02.py"} + ``` +With our bindings in place, we can now change the count of the _currently focused counter_ using ++up++ and ++down++. ## Rich renderables From 3c8f5672f44e016ebda725f42966d06e38b4c520 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 25 Sep 2024 12:17:09 +0100 Subject: [PATCH 10/19] Bindings and focus example Counter --- docs/examples/guide/widgets/counter.tcss | 2 +- docs/examples/guide/widgets/counter01.py | 2 +- docs/guide/widgets.md | 20 ++++++++++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/examples/guide/widgets/counter.tcss b/docs/examples/guide/widgets/counter.tcss index 06f7660004..4214fbf92d 100644 --- a/docs/examples/guide/widgets/counter.tcss +++ b/docs/examples/guide/widgets/counter.tcss @@ -3,7 +3,7 @@ Counter { padding: 1 2; color: $text-muted; - &:focus { + &:focus { /* (1)! */ background: $primary; color: $text; text-style: bold; diff --git a/docs/examples/guide/widgets/counter01.py b/docs/examples/guide/widgets/counter01.py index 15bbf8b10f..0277adc6bb 100644 --- a/docs/examples/guide/widgets/counter01.py +++ b/docs/examples/guide/widgets/counter01.py @@ -3,7 +3,7 @@ from textual.widgets import Footer, Static -class Counter(Static, can_focus=True): +class Counter(Static, can_focus=True): # (1)! """A counter that can be incremented and decremented by pressing keys.""" count = reactive(0) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index ac09f1f4da..d24031c8b4 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -200,6 +200,7 @@ A widget is only be able to handle key presses if it or one of its descendants h To demonstrate, let's design a simple interactive counter widget which can be incremented and decremented using the keyboard. In the following example, we define a simple `Counter` widget with `can_focus=True`, and some CSS to make it stand out when focused. +Our app contains three `Counter` widgets, which we can move focus between using ++tab++ and ++shift+tab++. === "counter01.py" @@ -207,12 +208,16 @@ In the following example, we define a simple `Counter` widget with `can_focus=Tr --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"} @@ -220,10 +225,16 @@ In the following example, we define a simple `Counter` widget with `can_focus=Tr Notice that Textual automatically focused the first widget, and that pressing ++tab++ and ++shift+tab++ will move focus between widgets. -Now that our counter is focusable, let's add some keybindings for incrementing and decrementing the counter. -To do this, we add a `BINDINGS` class variable to `Counter`, with bindings for incrementing and decrementing the counter using ++up++ and ++down++ respectively. +!!! note + + You can also move focus to a widget by clicking on it. + +Now that our counter is focusable, let's make it interactive by adding some key bindings and actions to it. +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" @@ -238,12 +249,9 @@ These new bindings are linked to the `change_count` action, which updates the `c === "Output" - ```{.textual path="docs/examples/guide/widgets/counter02.py"} + ```{.textual path="docs/examples/guide/widgets/counter02.py" press="up,tab,down,down"} ``` -With our bindings in place, we can now change the count of the _currently focused counter_ using ++up++ and ++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. From 6382685502c18ac56de5f089600a0a76b1587733 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 25 Sep 2024 12:31:46 +0100 Subject: [PATCH 11/19] Simplify and use RenderResult over str --- docs/examples/guide/widgets/counter01.py | 4 ++-- docs/examples/guide/widgets/counter02.py | 4 ++-- docs/guide/widgets.md | 8 +------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/examples/guide/widgets/counter01.py b/docs/examples/guide/widgets/counter01.py index 0277adc6bb..71763d2831 100644 --- a/docs/examples/guide/widgets/counter01.py +++ b/docs/examples/guide/widgets/counter01.py @@ -1,4 +1,4 @@ -from textual.app import App, ComposeResult +from textual.app import App, ComposeResult, RenderResult from textual.reactive import reactive from textual.widgets import Footer, Static @@ -8,7 +8,7 @@ class Counter(Static, can_focus=True): # (1)! count = reactive(0) - def render(self) -> str: + def render(self) -> RenderResult: return f"Count: {self.count}" diff --git a/docs/examples/guide/widgets/counter02.py b/docs/examples/guide/widgets/counter02.py index 879c87b752..e810b6d795 100644 --- a/docs/examples/guide/widgets/counter02.py +++ b/docs/examples/guide/widgets/counter02.py @@ -1,4 +1,4 @@ -from textual.app import App, ComposeResult +from textual.app import App, ComposeResult, RenderResult from textual.reactive import reactive from textual.widgets import Footer, Static @@ -13,7 +13,7 @@ class Counter(Static, can_focus=True): count = reactive(0) - def render(self) -> str: + def render(self) -> RenderResult: return f"Count: {self.count}" def action_change_count(self, amount: int): diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index d24031c8b4..37a266b8f3 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -200,7 +200,7 @@ A widget is only be able to handle key presses if it or one of its descendants h To demonstrate, let's design a simple interactive counter widget which can be incremented and decremented using the keyboard. In the following example, we define a simple `Counter` widget with `can_focus=True`, and some CSS to make it stand out when focused. -Our app contains three `Counter` widgets, which we can move focus between using ++tab++ and ++shift+tab++. +Our app contains three `Counter` widgets, which we can focus by clicking or using ++tab++ and ++shift+tab++. === "counter01.py" @@ -223,12 +223,6 @@ Our app contains three `Counter` widgets, which we can move focus between using ```{.textual path="docs/examples/guide/widgets/counter01.py"} ``` -Notice that Textual automatically focused the first widget, and that pressing ++tab++ and ++shift+tab++ will move focus between widgets. - -!!! note - - You can also move focus to a widget by clicking on it. - Now that our counter is focusable, let's make it interactive by adding some key bindings and actions to it. 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. From ed5864dfe9a89019dfc225f634ec9a32800754e5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 25 Sep 2024 12:45:34 +0100 Subject: [PATCH 12/19] Add notes to code --- docs/examples/guide/widgets/counter02.py | 4 ++-- docs/guide/widgets.md | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/examples/guide/widgets/counter02.py b/docs/examples/guide/widgets/counter02.py index e810b6d795..0c1746feca 100644 --- a/docs/examples/guide/widgets/counter02.py +++ b/docs/examples/guide/widgets/counter02.py @@ -7,7 +7,7 @@ class Counter(Static, can_focus=True): """A counter that can be incremented and decremented by pressing keys.""" BINDINGS = [ - ("up,k", "change_count(1)", "Increment"), + ("up,k", "change_count(1)", "Increment"), # (1)! ("down,j", "change_count(-1)", "Decrement"), ] @@ -16,7 +16,7 @@ class Counter(Static, can_focus=True): def render(self) -> RenderResult: return f"Count: {self.count}" - def action_change_count(self, amount: int): + def action_change_count(self, amount: int) -> None: # (2)! self.count += amount diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 37a266b8f3..4714efe964 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -190,7 +190,7 @@ 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. -## Interacting with widgets +## 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. @@ -227,7 +227,7 @@ Now that our counter is focusable, let's make it interactive by adding some key 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++. +With our bindings in place, we can now change the count of the _currently focused_ counter using ++up++ and ++down++. === "counter02.py" @@ -235,6 +235,9 @@ With our bindings in place, we can now change the count of the _currently focuse --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" From c6a0aba38a5d5dd6c7061b636ba19d1d4bcaa952 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 25 Sep 2024 15:08:14 +0100 Subject: [PATCH 13/19] Fix formatting --- docs/guide/widgets.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 4714efe964..d7b4289879 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -244,6 +244,9 @@ With our bindings in place, we can now change the count of the _currently focuse --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"} From 5dbb5ff2b23cbe2108578054a9305e601ecc6174 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 25 Sep 2024 15:46:36 +0100 Subject: [PATCH 14/19] Remove newline --- docs/guide/widgets.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index d7b4289879..fec36c652b 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -246,7 +246,6 @@ With our bindings in place, we can now change the count of the _currently focuse 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"} From aa13ab4a34926c1957d67adbc8441e1acdf16e94 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 30 Sep 2024 10:21:36 +0100 Subject: [PATCH 15/19] Fix a typo --- docs/guide/widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index fec36c652b..7cc6955b06 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -195,7 +195,7 @@ See the [style reference](../styles/index.md) for details. 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 only be able to handle key presses if it or one of its descendants has [focus](./guide/input.md#input-focus). +A widget is only able to handle key presses if it or one of its descendants has [focus](./guide/input.md#input-focus). To demonstrate, let's design a simple interactive counter widget which can be incremented and decremented using the keyboard. From f03f0386faba539323309724a380bb437625c2ac Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 30 Sep 2024 10:23:10 +0100 Subject: [PATCH 16/19] Fix link to focus docs --- docs/guide/widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 7cc6955b06..54efe605a2 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -195,7 +195,7 @@ See the [style reference](../styles/index.md) for details. 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 only able to handle key presses if it or one of its descendants has [focus](./guide/input.md#input-focus). +A widget is only able to handle key presses if it or one of its descendants has [focus](../guide/input.md#input-focus). To demonstrate, let's design a simple interactive counter widget which can be incremented and decremented using the keyboard. From e12bbd6e862e47802cefdb1b63f00d430ba57dd8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 30 Sep 2024 15:32:44 +0100 Subject: [PATCH 17/19] Update docs/guide/widgets.md Co-authored-by: Will McGugan --- docs/guide/widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 54efe605a2..d93262ec87 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -195,7 +195,7 @@ See the [style reference](../styles/index.md) for details. 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 only able to handle key presses if it or one of its descendants has [focus](../guide/input.md#input-focus). +A widget is able to handle key presses if it or one of its descendants has [focus](../guide/input.md#input-focus). To demonstrate, let's design a simple interactive counter widget which can be incremented and decremented using the keyboard. From 8b99b3fae40008adb86d3988c9990a95c030c4be Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 30 Sep 2024 15:48:19 +0100 Subject: [PATCH 18/19] PR feedback --- docs/guide/widgets.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 54efe605a2..f0a5a7b8d1 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -197,10 +197,9 @@ which let them call [actions](../guide/actions.md) in response to key presses. A widget is only able to handle key presses if it or one of its descendants has [focus](../guide/input.md#input-focus). -To demonstrate, let's design a simple interactive counter widget which can be incremented and decremented using the keyboard. - -In the following example, we define a simple `Counter` widget with `can_focus=True`, and some CSS to make it stand out when focused. -Our app contains three `Counter` widgets, which we can focus by clicking or using ++tab++ and ++shift+tab++. +Widgets aren't focusable by default. +In order to allow a widget to be focused, we need to set `can_focus=True` when defining a widget subclass. +Let's look at an example of defining a `Counter` widget which can be focused. === "counter01.py" @@ -223,6 +222,9 @@ Our app contains three `Counter` widgets, which we can focus by clicking or usin ```{.textual path="docs/examples/guide/widgets/counter01.py"} ``` + +Our app contains three `Counter` widgets, which we can focus by clicking or using ++tab++ and ++shift+tab++. + Now that our counter is focusable, let's make it interactive by adding some key bindings and actions to it. 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. From cd3e2387160f0230123840c3d81b385d8fcc5675 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 30 Sep 2024 15:56:25 +0100 Subject: [PATCH 19/19] Wording --- docs/guide/widgets.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index f0a5a7b8d1..c462263fa4 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -198,8 +198,8 @@ which let them call [actions](../guide/actions.md) in response to key presses. A widget is only 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. -In order to allow a widget to be focused, we need to set `can_focus=True` when defining a widget subclass. -Let's look at an example of defining a `Counter` widget which can be focused. +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" @@ -223,9 +223,9 @@ Let's look at an example of defining a `Counter` widget which can be focused. ``` -Our app contains three `Counter` widgets, which we can focus by clicking or using ++tab++ and ++shift+tab++. +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 make it interactive by adding some key bindings and actions to it. +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.