From b3eb543e380c904768b59c051e2415e233d8516f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 10 Aug 2022 13:09:38 +0100 Subject: [PATCH] unified events and messages --- docs/examples/light_dark.py | 4 +- sandbox/buttons.py | 2 +- sandbox/darren/buttons.py | 2 +- sandbox/darren/file_search.py | 2 +- sandbox/input.py | 2 +- sandbox/will/add_remove.py | 2 +- sandbox/will/scroll.py | 4 +- src/textual/_callback.py | 1 - src/textual/app.py | 9 ++- src/textual/devtools/borders.py | 2 +- src/textual/events.py | 5 -- src/textual/message.py | 24 ++++-- src/textual/message_pump.py | 117 +++++++++++++++++++--------- src/textual/messages.py | 3 +- src/textual/screen.py | 16 ++-- src/textual/views/__init__.py | 3 - src/textual/views/_dock_view.py | 72 ----------------- src/textual/views/_document_view.py | 3 - src/textual/views/_grid_view.py | 9 --- src/textual/views/_window_view.py | 65 ---------------- src/textual/widget.py | 15 ++-- src/textual/widgets/_button.py | 3 + src/textual/widgets/_data_table.py | 2 +- 23 files changed, 137 insertions(+), 230 deletions(-) delete mode 100644 src/textual/views/__init__.py delete mode 100644 src/textual/views/_dock_view.py delete mode 100644 src/textual/views/_document_view.py delete mode 100644 src/textual/views/_grid_view.py delete mode 100644 src/textual/views/_window_view.py diff --git a/docs/examples/light_dark.py b/docs/examples/light_dark.py index 8cd3cd78c..5e2e9229f 100644 --- a/docs/examples/light_dark.py +++ b/docs/examples/light_dark.py @@ -5,17 +5,15 @@ from textual.widgets import Button class ButtonApp(App): CSS = """ - Button { width: 100%; } - """ def compose(self): yield Button("Lights off") - def handle_pressed(self, event): + def on_button_pressed(self, event): self.dark = not self.dark self.bell() event.button.label = "Lights ON" if self.dark else "Lights OFF" diff --git a/sandbox/buttons.py b/sandbox/buttons.py index daf1047a1..4ffd55dd8 100644 --- a/sandbox/buttons.py +++ b/sandbox/buttons.py @@ -12,7 +12,7 @@ class ButtonsApp(App[str]): Button.error("error", id="baz"), ) - def handle_pressed(self, event: Button.Pressed) -> None: + def on_button_pressed(self, event: Button.Pressed) -> None: self.app.bell() async def on_key(self, event: events.Key) -> None: diff --git a/sandbox/darren/buttons.py b/sandbox/darren/buttons.py index daf1047a1..4ffd55dd8 100644 --- a/sandbox/darren/buttons.py +++ b/sandbox/darren/buttons.py @@ -12,7 +12,7 @@ class ButtonsApp(App[str]): Button.error("error", id="baz"), ) - def handle_pressed(self, event: Button.Pressed) -> None: + def on_button_pressed(self, event: Button.Pressed) -> None: self.app.bell() async def on_key(self, event: events.Key) -> None: diff --git a/sandbox/darren/file_search.py b/sandbox/darren/file_search.py index ee0abaa72..a1fbcefb1 100644 --- a/sandbox/darren/file_search.py +++ b/sandbox/darren/file_search.py @@ -58,7 +58,7 @@ class FileSearchApp(App): self.mount(file_table_wrapper=Widget(self.file_table)) self.mount(search_bar=self.search_bar) - def handle_changed(self, event: TextWidgetBase.Changed) -> None: + def on_text_widget_base_changed(self, event: TextWidgetBase.Changed) -> None: self.file_table.filter = event.value diff --git a/sandbox/input.py b/sandbox/input.py index aa516812c..8df51a03d 100644 --- a/sandbox/input.py +++ b/sandbox/input.py @@ -48,7 +48,7 @@ class InputApp(App[str]): ) self.mount(text_area=TextArea()) - def handle_changed(self, event: TextWidgetBase.Changed) -> None: + def on_text_widget_base_changed(self, event: TextWidgetBase.Changed) -> None: try: value = float(event.value) except ValueError: diff --git a/sandbox/will/add_remove.py b/sandbox/will/add_remove.py index e6a6943e7..27de0c03f 100644 --- a/sandbox/will/add_remove.py +++ b/sandbox/will/add_remove.py @@ -46,7 +46,7 @@ class AddRemoveApp(App): layout.Vertical(id="items"), ) - def handle_pressed(self, event: Button.Pressed) -> None: + def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == "add": self.count += 1 self.query("#items").first().mount( diff --git a/sandbox/will/scroll.py b/sandbox/will/scroll.py index 2494b419f..57017a6eb 100644 --- a/sandbox/will/scroll.py +++ b/sandbox/will/scroll.py @@ -27,7 +27,7 @@ class ButtonsApp(App[str]): Button("There can be only one"), ) - def handle_pressed(self, event: Button.Pressed) -> None: + def on_button_pressed(self, event: Button.Pressed) -> None: self.app.bell() async def on_key(self, event: events.Key) -> None: @@ -38,7 +38,7 @@ class ButtonsApp(App[str]): app = ButtonsApp( - log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=2 + log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=3 ) if __name__ == "__main__": diff --git a/src/textual/_callback.py b/src/textual/_callback.py index ad3b56e44..0d52effab 100644 --- a/src/textual/_callback.py +++ b/src/textual/_callback.py @@ -23,7 +23,6 @@ async def invoke(callback: Callable, *params: object) -> Any: """ _rich_traceback_guard = True parameter_count = count_parameters(callback) - result = callback(*params[:parameter_count]) if isawaitable(result): result = await result diff --git a/src/textual/app.py b/src/textual/app.py index c7f566f4b..bb01179bb 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1035,7 +1035,8 @@ class App(Generic[ReturnType], DOMNode): # Forward the event to the view await self.screen.forward_event(event) elif isinstance(event, events.Paste): - await self.focused.forward_event(event) + if self.focused is not None: + await self.focused.forward_event(event) else: await super().on_event(event) @@ -1111,11 +1112,11 @@ class App(Generic[ReturnType], DOMNode): return False return True - async def handle_update(self, message: messages.Update) -> None: + async def on_update(self, message: messages.Update) -> None: message.stop() self._paint() - async def handle_layout(self, message: messages.Layout) -> None: + async def on_layout(self, message: messages.Layout) -> None: message.stop() self._paint() @@ -1167,7 +1168,7 @@ class App(Generic[ReturnType], DOMNode): async def action_toggle_class(self, selector: str, class_name: str) -> None: self.screen.query(selector).toggle_class(class_name) - def handle_terminal_supports_synchronized_output( + def on_terminal_supports_synchronized_output( self, message: messages.TerminalSupportsSynchronizedOutput ) -> None: log("[b green]SynchronizedOutput mode is supported") diff --git a/src/textual/devtools/borders.py b/src/textual/devtools/borders.py index ac1dea6e7..27813cdfa 100644 --- a/src/textual/devtools/borders.py +++ b/src/textual/devtools/borders.py @@ -50,7 +50,7 @@ class BorderApp(App): self.text = Static(TEXT) yield self.text - def handle_pressed(self, event): + def on_button_pressed(self, event: Button.Pressed) -> None: self.text.styles.border = ( event.button.id, self.stylesheet.variables["primary"], diff --git a/src/textual/events.py b/src/textual/events.py index b4ff68c54..687a2490b 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -28,11 +28,6 @@ class Event(Message): super().__init_subclass__(bubble=bubble, verbosity=verbosity) -class Null(Event, verbosity=3): - def can_replace(self, message: Message) -> bool: - return isinstance(message, Null) - - @rich.repr.auto class Callback(Event, bubble=False, verbosity=3): def __init__( diff --git a/src/textual/message.py b/src/textual/message.py index 13bd9c537..0e0692171 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -20,15 +20,14 @@ class Message: "_forwarded", "_no_default_action", "_stop_propagation", - "__done_event", + "_handler_name", ] sender: MessageTarget bubble: ClassVar[bool] = True # Message will bubble to parent verbosity: ClassVar[int] = 1 # Verbosity (higher the more verbose) - system: ClassVar[ - bool - ] = False # Message is system related and may not be handled by client code + no_dispatch: ClassVar[bool] = False # Message may not be handled by client code + namespace: ClassVar[str] = "" # Namespace to disambiguate messages def __init__(self, sender: MessageTarget) -> None: """ @@ -43,6 +42,9 @@ class Message: self._forwarded = False self._no_default_action = False self._stop_propagation = False + self._handler_name = ( + f"on_{self.namespace}_{self.name}" if self.namespace else f"on_{self.name}" + ) super().__init__() def __rich_repr__(self) -> rich.repr.Result: @@ -52,20 +54,28 @@ class Message: cls, bubble: bool | None = True, verbosity: int | None = 1, - system: bool | None = False, + no_dispatch: bool | None = False, + namespace: str | None = None, ) -> None: super().__init_subclass__() if bubble is not None: cls.bubble = bubble if verbosity is not None: cls.verbosity = verbosity - if system is not None: - cls.system = system + if no_dispatch is not None: + cls.no_dispatch = no_dispatch + if namespace is not None: + cls.namespace = namespace @property def is_forwarded(self) -> bool: return self._forwarded + @property + def handler_name(self) -> str: + # Property to make it read only + return self._handler_name + def set_forwarded(self) -> None: """Mark this event as being forwarded.""" self._forwarded = True diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 038078051..ac3c27f10 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +from typing import Any import inspect from asyncio import CancelledError from asyncio import PriorityQueue, QueueEmpty, Task @@ -10,10 +11,12 @@ from weakref import WeakSet from . import events from . import log +from .case import camel_to_snake from ._timer import Timer, TimerCallback from ._callback import invoke from ._context import active_app, NoActiveAppError from .message import Message +from .events import Event from . import messages if TYPE_CHECKING: @@ -51,7 +54,27 @@ class MessagePriority: return self.priority > other.priority -class MessagePump: +class MessagePumpMeta(type): + """Metaclass for message pump. This exists to populate an Event class of a Widget with the + parent classes' name. + + """ + + def __new__( + cls, name: str, bases: tuple[type, ...], class_dict: dict[str, Any], **kwargs + ): + namespace = camel_to_snake(name) + isclass = inspect.isclass + for value in class_dict.values(): + if isclass(value) and issubclass(value, Message): + if not value.namespace: + value.namespace = namespace + + class_obj = super().__new__(cls, name, bases, class_dict, **kwargs) + return class_obj + + +class MessagePump(metaclass=MessagePumpMeta): def __init__(self, parent: MessagePump | None = None) -> None: self._message_queue: PriorityQueue[MessagePriority] = PriorityQueue() self._parent = parent @@ -204,9 +227,9 @@ class MessagePump: message = messages.InvokeLater(self, partial(callback, *args, **kwargs)) self.post_message_no_wait(message) - def handle_invoke_later(self, message: messages.InvokeLater) -> None: + def on_invoke_later(self, message: messages.InvokeLater) -> None: # Forward InvokeLater message to the Screen - self.app.screen.post_message_no_wait(message) + self.app.screen._invoke_later(message.callback) def close_messages_no_wait(self) -> None: """Request the message queue to exit.""" @@ -290,20 +313,32 @@ class MessagePump: log("CLOSED", self) - async def dispatch_message(self, message: Message) -> bool | None: + async def dispatch_message(self, message: Message) -> None: + """Dispatch a message received form the message queue. + + Args: + message (Message): A message object + """ _rich_traceback_guard = True - if message.system: - return False - if isinstance(message, events.Event): - if not isinstance(message, events.Null): - await self.on_event(message) + if message.no_dispatch: + return + + # Allow apps to treat events and messages separately + if isinstance(message, Event): + await self.on_event(message) else: - return await self.on_message(message) - return False + await self.on_message(message) def _get_dispatch_methods( self, method_name: str, message: Message ) -> Iterable[tuple[type, Callable[[Message], Awaitable]]]: + """Gets handlers from the MRO + + Args: + method_name (str): Handler method name. + message (Message): Message object. + + """ for cls in self.__class__.__mro__: if message._no_default_action: break @@ -312,47 +347,57 @@ class MessagePump: yield cls, method.__get__(self, cls) async def on_event(self, event: events.Event) -> None: - _rich_traceback_guard = True + """Called to process an event. - 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: - if event.sender == self._parent: - # parent is sender, so we stop propagation after parent - event.stop() - if self.is_parent_active: - await self._parent.post_message(event) + Args: + event (events.Event): An Event object. + """ + await self.on_message(event) async def on_message(self, message: Message) -> None: - _rich_traceback_guard = True - method_name = f"handle_{message.name}" - method = getattr(self, method_name, None) + """Called to process a message. - if method is not None: - log(message, ">>>", self, verbosity=message.verbosity) + Args: + message (Message): A Message object. + """ + _rich_traceback_guard = True + handler_name = message._handler_name + + # Look through the MRO to find a handler + for cls, method in self._get_dispatch_methods(handler_name, message): + log( + message, + ">>>", + self, + f"method=<{cls.__name__}.{handler_name}>", + verbosity=message.verbosity, + ) await invoke(method, message) + # Bubble messages up the DOM (if enabled on the message) if message.bubble and self._parent and not message._stop_propagation: if message.sender == self._parent: # parent is sender, so we stop propagation after parent message.stop() - if not self._parent._closed and not self._parent._closing: + if self.is_parent_active and not self._parent._closing: await self._parent.post_message(message) - def check_idle(self): + def check_idle(self) -> None: """Prompt the message pump to call idle if the queue is empty.""" if self._message_queue.empty(): self.post_message_no_wait(messages.Prompt(sender=self)) async def post_message(self, message: Message) -> bool: + """Post a message or an event to this message pump. + + Args: + message (Message): A message object. + + Returns: + bool: True if the messages was posted successfully, False if the message was not posted + (because the message pump was in the process of closing). + """ + if self._closing or self._closed: return False if not self.check_message_enabled(message): @@ -374,6 +419,7 @@ class MessagePump: Returns: bool: True if the messages was processed. """ + if self._closing or self._closed: return False if not self.check_message_enabled(message): @@ -382,6 +428,7 @@ class MessagePump: return True def post_message_no_wait(self, message: Message) -> bool: + if self._closing or self._closed: return False if not self.check_message_enabled(message): diff --git a/src/textual/messages.py b/src/textual/messages.py index 636d6ed65..84bc8d14a 100644 --- a/src/textual/messages.py +++ b/src/textual/messages.py @@ -50,6 +50,7 @@ class InvokeLater(Message, verbosity=3): yield "callback", self.callback +# TODO: This should really be an Event @rich.repr.auto class CursorMove(Message): def __init__(self, sender: MessagePump, line: int) -> None: @@ -66,7 +67,7 @@ class StylesUpdated(Message): return isinstance(message, StylesUpdated) -class Prompt(Message, system=True): +class Prompt(Message, no_dispatch=True): """Used to 'wake up' an event loop.""" def can_replace(self, message: Message) -> bool: diff --git a/src/textual/screen.py b/src/textual/screen.py index c4773b76e..1ec1345fe 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -156,9 +156,15 @@ class Screen(Widget): for callback in callbacks: await invoke(callback) - def handle_invoke_later(self, message: messages.InvokeLater) -> None: - # Enqueue the callback function to be called later - self._callbacks.append(message.callback) + def _invoke_later(self, callback: CallbackType) -> None: + """Enqueue a callback to be invoked after the screen is repainted. + + Args: + callback (CallbackType): A callback. + """ + + self._callbacks.append(callback) + self.check_idle() def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None: """Refresh the layout (can change size and positions of widgets).""" @@ -201,7 +207,7 @@ class Screen(Widget): if display_update is not None: self.app._display(display_update) - async def handle_update(self, message: messages.Update) -> None: + async def on_update(self, message: messages.Update) -> None: message.stop() message.prevent_default() widget = message.widget @@ -209,7 +215,7 @@ class Screen(Widget): self._dirty_widgets.add(widget) self.check_idle() - async def handle_layout(self, message: messages.Layout) -> None: + async def on_layout(self, message: messages.Layout) -> None: message.stop() message.prevent_default() self._layout_required = True diff --git a/src/textual/views/__init__.py b/src/textual/views/__init__.py deleted file mode 100644 index d0f5515b6..000000000 --- a/src/textual/views/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._dock_view import DockView, Dock, DockEdge -from ._grid_view import GridView -from ._window_view import WindowView diff --git a/src/textual/views/_dock_view.py b/src/textual/views/_dock_view.py deleted file mode 100644 index b859d9081..000000000 --- a/src/textual/views/_dock_view.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations -from typing import cast, Optional - -from ..layouts.dock import DockLayout, Dock, DockEdge -from ..layouts.grid import GridLayout, GridAlign -from ..screen import Screen -from ..widget import Widget - - -class DoNotSet: - pass - - -do_not_set = DoNotSet() - - -class DockView(Screen): - def __init__(self, name: str | None = None) -> None: - super().__init__(layout=DockLayout(), name=name) - - async def dock( - self, - *widgets: Widget, - name: str | None = None, - id: str | None = None, - edge: DockEdge = "top", - z: int = 0, - size: int | None | DoNotSet = do_not_set, - ) -> None: - - dock = Dock(edge, widgets, z) - assert isinstance(self.layout, DockLayout) - self.layout.docks.append(dock) - for widget in widgets: - if id is not None: - widget._id = id - if name is not None: - widget.name = name - if size is not do_not_set: - widget.layout_size = cast(Optional[int], size) - if name is None: - await self.mount(widget) - else: - await self.mount(**{name: widget}) - await self._refresh_layout() - - async def dock_grid( - self, - *, - name: str | None = None, - id: str | None = None, - edge: DockEdge = "top", - z: int = 0, - size: int | None | DoNotSet = do_not_set, - gap: tuple[int, int] | int | None = None, - gutter: tuple[int, int] | int | None = None, - align: tuple[GridAlign, GridAlign] | None = None, - ) -> GridLayout: - - grid = GridLayout(gap=gap, gutter=gutter, align=align) - view = Screen(layout=grid, id=id, name=name) - dock = Dock(edge, (view,), z) - assert isinstance(self.layout, DockLayout) - self.layout.docks.append(dock) - if size is not do_not_set: - view.layout_size = cast(Optional[int], size) - if name is None: - await self.mount(view) - else: - await self.mount(**{name: view}) - await self._refresh_layout() - return grid diff --git a/src/textual/views/_document_view.py b/src/textual/views/_document_view.py deleted file mode 100644 index 2e68e5829..000000000 --- a/src/textual/views/_document_view.py +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import annotations - -from ..screen import Screen diff --git a/src/textual/views/_grid_view.py b/src/textual/views/_grid_view.py deleted file mode 100644 index b08614b5e..000000000 --- a/src/textual/views/_grid_view.py +++ /dev/null @@ -1,9 +0,0 @@ -from ..screen import Screen -from ..layouts.grid import GridLayout - - -class GridView(Screen, layout=GridLayout): - @property - def grid(self) -> GridLayout: - assert isinstance(self.layout, GridLayout) - return self.layout diff --git a/src/textual/views/_window_view.py b/src/textual/views/_window_view.py deleted file mode 100644 index 3caf0b797..000000000 --- a/src/textual/views/_window_view.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -from rich.console import RenderableType - -from .. import events -from ..geometry import Size, SpacingDimensions -from ..layouts.vertical import VerticalLayout -from ..screen import Screen -from ..message import Message -from .. import messages -from ..widget import Widget -from ..widgets import Static - - -class WindowChange(Message): - def can_replace(self, message: Message) -> bool: - return isinstance(message, WindowChange) - - -class WindowView(Screen, layout=VerticalLayout): - def __init__( - self, - widget: RenderableType | Widget, - *, - auto_width: bool = False, - gutter: SpacingDimensions = (0, 0), - name: str | None = None, - ) -> None: - layout = VerticalLayout(gutter=gutter, auto_width=auto_width) - self.widget = widget if isinstance(widget, Widget) else Static(widget) - layout.add(self.widget) - super().__init__(name=name, layout=layout) - - async def update(self, widget: Widget | RenderableType) -> None: - layout = self.layout - assert isinstance(layout, VerticalLayout) - layout.clear() - self.widget = widget if isinstance(widget, Widget) else Static(widget) - layout.add(self.widget) - layout.require_update() - self.refresh(layout=True) - await self.emit(WindowChange(self)) - - async def handle_update(self, message: messages.Update) -> None: - message.prevent_default() - await self.emit(WindowChange(self)) - - async def handle_layout(self, message: messages.Layout) -> None: - self.layout.require_update() - message.stop() - self.refresh() - - async def watch_virtual_size(self, size: Size) -> None: - await self.emit(WindowChange(self)) - - async def watch_scroll_x(self, value: int) -> None: - self.layout.require_update() - self.refresh() - - async def watch_scroll_y(self, value: int) -> None: - self.layout.require_update() - self.refresh() - - async def on_resize(self, event: events.Resize) -> None: - await self.emit(WindowChange(self)) diff --git a/src/textual/widget.py b/src/textual/widget.py index 76e4dd043..1aefcae1f 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1105,13 +1105,12 @@ class Widget(DOMNode): Args: event (events.Idle): Idle event. """ - if self._repaint_required: + self._repaint_required = False self.screen.post_message_no_wait(messages.Update(self, self)) if self._layout_required: + self._layout_required = False self.screen.post_message_no_wait(messages.Layout(self)) - self._layout_required = False - self._repaint_required = False def focus(self) -> None: """Give input focus to this widget.""" @@ -1205,27 +1204,27 @@ class Widget(DOMNode): if self.scroll_up(animate=False): event.stop() - def handle_scroll_to(self, message: ScrollTo) -> None: + def on_scroll_to(self, message: ScrollTo) -> None: if self.is_scrollable: self.scroll_to(message.x, message.y, animate=message.animate, duration=0.1) message.stop() - def handle_scroll_up(self, event: ScrollUp) -> None: + def on_scroll_up(self, event: ScrollUp) -> None: if self.is_scrollable: self.scroll_page_up() event.stop() - def handle_scroll_down(self, event: ScrollDown) -> None: + def on_scroll_down(self, event: ScrollDown) -> None: if self.is_scrollable: self.scroll_page_down() event.stop() - def handle_scroll_left(self, event: ScrollLeft) -> None: + def on_scroll_left(self, event: ScrollLeft) -> None: if self.is_scrollable: self.scroll_page_left() event.stop() - def handle_scroll_right(self, event: ScrollRight) -> None: + def on_scroll_right(self, event: ScrollRight) -> None: if self.is_scrollable: self.scroll_page_right() event.stop() diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index aa4ccf475..a937f546f 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -300,3 +300,6 @@ class Button(Widget, can_focus=True): id=id, classes=classes, ) + + +print(Button.Pressed.namespace) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 1ede166e5..ed5dde3f8 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -220,7 +220,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): return self.header_height return self.rows[row_index].height - async def handle_styles_updated(self, message: messages.StylesUpdated) -> None: + async def on_styles_updated(self, message: messages.StylesUpdated) -> None: self._clear_caches() self.refresh()