diff --git a/sandbox/uber.css b/sandbox/uber.css index 51b6aa021..d392f2f65 100644 --- a/sandbox/uber.css +++ b/sandbox/uber.css @@ -1,3 +1,7 @@ +App.-show-focus *:focus { + tint: #8bc34a 50%; +} + #uber1 { layout: vertical; background: green; @@ -6,8 +10,12 @@ text-style: underline; } +#uber1:focus-within { + background: darkslateblue; +} + .list-item { - height: 8; + height: 10; color: #12a0; background: #ffffff00; } diff --git a/sandbox/uber.py b/sandbox/uber.py index 4d1d9a761..96f70fd0d 100644 --- a/sandbox/uber.py +++ b/sandbox/uber.py @@ -22,8 +22,9 @@ class BasicApp(App): async def on_mount(self): """Build layout here.""" + first_child = Placeholder(id="child1", classes="list-item") uber1 = Widget( - Placeholder(id="child1", classes="list-item"), + first_child, Placeholder(id="child2", classes="list-item"), Placeholder(id="child3", classes="list-item"), Placeholder(classes="list-item"), @@ -32,6 +33,8 @@ class BasicApp(App): ) self.mount(uber1=uber1) uber1.focus() + self.first_child = first_child + self.uber = uber1 async def on_key(self, event: events.Key) -> None: await self.dispatch_key(event) @@ -50,8 +53,7 @@ class BasicApp(App): "Focused widget is:", self.focused, ) - print(1234, 5678) - sys.stdout.write("abcdef") + self.app.set_focus(None) def action_modify_focussed(self): """Increment height of focussed child, randomise border and bg color""" diff --git a/src/textual/app.py b/src/textual/app.py index 84b689dd0..9d91cbca1 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -540,16 +540,19 @@ class App(Generic[ReturnType], DOMNode): # No focus, so blur currently focused widget if it exists if self.focused is not None: self.focused.post_message_no_wait(events.Blur(self)) + self.focused.emit_no_wait(events.DescendantBlur(self)) self.focused = None elif widget.can_focus: if self.focused != widget: if self.focused is not None: # Blur currently focused widget self.focused.post_message_no_wait(events.Blur(self)) + self.focused.emit_no_wait(events.DescendantBlur(self)) # Change focus self.focused = widget # Send focus event widget.post_message_no_wait(events.Focus(self)) + widget.emit_no_wait(events.DescendantFocus(self)) async def _set_mouse_over(self, widget: Widget | None) -> None: """Called when the mouse is over another widget. diff --git a/src/textual/events.py b/src/textual/events.py index 5525a123a..20bf7033a 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -393,3 +393,11 @@ class Focus(Event, bubble=False): class Blur(Event, bubble=False): pass + + +class DescendantFocus(Event, bubble=True): + pass + + +class DescendantBlur(Event, bubble=True): + pass diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4da07f2c8..66ed98721 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -246,7 +246,9 @@ class MessagePump: if self._message_queue.empty(): if not self._closed: event = events.Idle(self) - for method in self._get_dispatch_methods("on_idle", event): + for _cls, method in self._get_dispatch_methods( + "on_idle", event + ): await invoke(method, event) log("CLOSED", self) @@ -264,18 +266,25 @@ class MessagePump: def _get_dispatch_methods( self, method_name: str, message: Message - ) -> Iterable[Callable[[Message], Awaitable]]: + ) -> Iterable[tuple[type, Callable[[Message], Awaitable]]]: for cls in self.__class__.__mro__: if message._no_default_action: break method = cls.__dict__.get(method_name, None) if method is not None: - yield method.__get__(self, cls) + yield cls, method.__get__(self, cls) async def on_event(self, event: events.Event) -> None: _rich_traceback_guard = True - for method in self._get_dispatch_methods(f"on_{event.name}", event): - log(event, ">>>", self, verbosity=event.verbosity) + + for cls, method in self._get_dispatch_methods(f"on_{event.name}", event): + log( + event, + ">>>", + self, + f"method=<{cls.__name__}.{method.__func__.__name__}>", + verbosity=event.verbosity, + ) await invoke(method, event) if event.bubble and self._parent and not event._stop_propagation: diff --git a/src/textual/widget.py b/src/textual/widget.py index e6cb952f3..b644ac3a6 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -99,6 +99,7 @@ class Widget(DOMNode): auto_width = Reactive(True) auto_height = Reactive(True) has_focus = Reactive(False) + descendant_has_focus = Reactive(False) mouse_over = Reactive(False) scroll_x = Reactive(0.0, repaint=False) scroll_y = Reactive(0.0, repaint=False) @@ -451,6 +452,8 @@ class Widget(DOMNode): yield "hover" if self.has_focus: yield "focus" + if self.descendant_has_focus: + yield "focus-within" def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None: watch(self, attribute_name, callback) @@ -753,11 +756,19 @@ class Widget(DOMNode): self.mouse_over = True def on_focus(self, event: events.Focus) -> None: + self.emit_no_wait(events.DescendantFocus(self)) self.has_focus = True def on_blur(self, event: events.Blur) -> None: + self.emit_no_wait(events.DescendantBlur(self)) self.has_focus = False + def on_descendant_focus(self, event: events.DescendantFocus) -> None: + self.descendant_has_focus = True + + def on_descendant_blur(self, event: events.DescendantBlur) -> None: + self.descendant_has_focus = False + def on_mouse_scroll_down(self, event) -> None: if self.is_container: self.scroll_down(animate=False)