Skip to content

Commit cb57d70

Browse files
authored
App focus (#3767)
* global focus * change name to app focus * app focus * refactor * changelog
1 parent 2f86ee4 commit cb57d70

File tree

5 files changed

+64
-7
lines changed

5 files changed

+64
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2121
- Widgets can now have an ALLOW_CHILDREN (bool) classvar to disallow adding children to a widget https://github.com/Textualize/textual/pull/3758
2222
- Added the ability to set the `label` property of a `Checkbox` https://github.com/Textualize/textual/pull/3765
2323
- Added the ability to set the `label` property of a `RadioButton` https://github.com/Textualize/textual/pull/3765
24+
- Added app focus/blur for textual-web https://github.com/Textualize/textual/pull/3767
2425

2526
### Changed
2627

src/textual/app.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,12 @@ class MyApp(App[None]):
367367
self.app.dark = not self.app.dark # Toggle dark mode
368368
```
369369
"""
370+
app_focus = Reactive(True, compute=False)
371+
"""Indicates if the app has focus.
372+
373+
When run in the terminal, the app always has focus. When run in the web, the app will
374+
get focus when the terminal widget has focus.
375+
"""
370376

371377
def __init__(
372378
self,
@@ -2672,6 +2678,8 @@ async def on_event(self, event: events.Event) -> None:
26722678
await super().on_event(event)
26732679

26742680
elif isinstance(event, events.InputEvent) and not event.is_forwarded:
2681+
if not self.app_focus and isinstance(event, (events.Key, events.MouseDown)):
2682+
self.app_focus = True
26752683
if isinstance(event, events.MouseEvent):
26762684
# Record current mouse position on App
26772685
self.mouse_position = Offset(event.x, event.y)
@@ -2819,6 +2827,16 @@ async def _on_resize(self, event: events.Resize) -> None:
28192827
for screen in self._background_screens:
28202828
screen.post_message(event)
28212829

2830+
async def _on_app_focus(self, event: events.AppFocus) -> None:
2831+
"""App has focus."""
2832+
# Required by textual-web to manage focus in a web page.
2833+
self.app_focus = True
2834+
2835+
async def _on_app_blur(self, event: events.AppBlur) -> None:
2836+
"""App has lost focus."""
2837+
# Required by textual-web to manage focus in a web page.
2838+
self.app_focus = False
2839+
28222840
def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]:
28232841
"""Detach a list of widgets from the DOM.
28242842
@@ -2976,6 +2994,15 @@ async def _prune_node(self, root: Widget) -> None:
29762994
await root._close_messages(wait=True)
29772995
self._unregister(root)
29782996

2997+
def _watch_app_focus(self, focus: bool) -> None:
2998+
"""Respond to changes in app focus."""
2999+
if focus:
3000+
focused = self.screen.focused
3001+
self.screen.set_focus(None)
3002+
self.screen.set_focus(focused)
3003+
else:
3004+
self.screen.set_focus(None)
3005+
29793006
async def action_check_bindings(self, key: str) -> None:
29803007
"""An [action](/guide/actions) to handle a key press using the binding system.
29813008

src/textual/drivers/web_driver.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def do_exit() -> None:
142142
self._enable_bracketed_paste()
143143
self.flush()
144144
self._key_thread.start()
145+
self._app.post_message(events.AppBlur())
145146

146147
def disable_input(self) -> None:
147148
"""Disable further input."""
@@ -188,7 +189,7 @@ def _on_meta(self, packet_type: str, payload: bytes) -> None:
188189
payload: Meta payload (JSON encoded as bytes).
189190
"""
190191
payload_map = json.loads(payload)
191-
_type = payload_map.get("type")
192+
_type = payload_map.get("type", {})
192193
if isinstance(payload_map, dict):
193194
self.on_meta(_type, payload_map)
194195

@@ -203,6 +204,10 @@ def on_meta(self, packet_type: str, payload: dict) -> None:
203204
self._size = (payload["width"], payload["height"])
204205
size = Size(*self._size)
205206
self._app.post_message(events.Resize(size, size))
207+
elif packet_type == "focus":
208+
self._app.post_message(events.AppFocus())
209+
elif packet_type == "blur":
210+
self._app.post_message(events.AppBlur())
206211
elif packet_type == "quit":
207212
self._app.post_message(messages.ExitApp())
208213
elif packet_type == "exit":

src/textual/events.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,26 @@ class Blur(Event, bubble=False):
561561
"""
562562

563563

564+
class AppFocus(Event, bubble=False):
565+
"""Sent when the app has focus.
566+
567+
Used by textual-web.
568+
569+
- [ ] Bubbles
570+
- [ ] Verbose
571+
"""
572+
573+
574+
class AppBlur(Event, bubble=False):
575+
"""Sent when the app loses focus.
576+
577+
Used by textual-web.
578+
579+
- [ ] Bubbles
580+
- [ ] Verbose
581+
"""
582+
583+
564584
@dataclass
565585
class DescendantFocus(Event, bubble=True, verbose=True):
566586
"""Sent when a child widget is focussed.

src/textual/screen.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -819,12 +819,16 @@ def _on_screen_resume(self) -> None:
819819
size = self.app.size
820820
self._refresh_layout(size, full=True)
821821
self.refresh()
822-
auto_focus = self.app.AUTO_FOCUS if self.AUTO_FOCUS is None else self.AUTO_FOCUS
823-
if auto_focus and self.focused is None:
824-
for widget in self.query(auto_focus):
825-
if widget.focusable:
826-
self.set_focus(widget)
827-
break
822+
# Only auto-focus when the app has focus (textual-web only)
823+
if self.app.app_focus:
824+
auto_focus = (
825+
self.app.AUTO_FOCUS if self.AUTO_FOCUS is None else self.AUTO_FOCUS
826+
)
827+
if auto_focus and self.focused is None:
828+
for widget in self.query(auto_focus):
829+
if widget.focusable:
830+
self.set_focus(widget)
831+
break
828832

829833
def _on_screen_suspend(self) -> None:
830834
"""Screen has suspended."""

0 commit comments

Comments
 (0)