diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ce08816..7b6164202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - `textual run` execs apps in a new context. +- Textual console no longer parses console markup. ### Added diff --git a/src/textual/devtools/client.py b/src/textual/devtools/client.py index 2982d07fe..1887d41ed 100644 --- a/src/textual/devtools/client.py +++ b/src/textual/devtools/client.py @@ -211,9 +211,9 @@ class DevtoolsClient: log: The log to write to devtools """ if isinstance(log.objects_or_string, str): - self.console.print(log.objects_or_string) + self.console.print(log.objects_or_string, markup=False) else: - self.console.print(*log.objects_or_string) + self.console.print(*log.objects_or_string, markup=False) segments = self.console.export_segments() diff --git a/src/textual/events.py b/src/textual/events.py index f2484b23d..fce28a6ea 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -50,15 +50,6 @@ class Callback(Event, bubble=False, verbose=True): yield "callback", self.callback -class InvokeCallbacks(Event, bubble=False, verbose=True): - """An internal event, sent to the screen to run callbacks. - - - [ ] Bubbles - - [X] Verbose - - """ - - class ShutdownRequest(Event): pass diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 9d96f3d6a..34d691199 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -374,6 +374,7 @@ class MessagePump(metaclass=_MessagePumpMeta): **kwargs: Keyword arguments to pass to the callable. """ self._next_callbacks.append(partial(callback, *args, **kwargs)) + self.check_idle() def _on_invoke_later(self, message: messages.InvokeLater) -> None: # Forward InvokeLater message to the Screen @@ -506,6 +507,7 @@ class MessagePump(metaclass=_MessagePumpMeta): except Exception as error: self.app._handle_exception(error) break + await self._flush_next_callbacks() async def _flush_next_callbacks(self) -> None: """Invoke pending callbacks in next callbacks queue.""" diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 68dcf8f25..cb18223f1 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -100,11 +100,11 @@ class Pilot(Generic[ReturnType]): target_widget, offset, button=1, shift=shift, meta=meta, control=control ) app.post_message(MouseDown(**message_arguments)) - await self.pause() + await self.pause(0.1) app.post_message(MouseUp(**message_arguments)) - await self.pause() + await self.pause(0.1) app.post_message(Click(**message_arguments)) - await self.pause() + await self.pause(0.1) async def hover( self, diff --git a/src/textual/screen.py b/src/textual/screen.py index b56213fbe..ddd832bfb 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -444,15 +444,12 @@ class Screen(Generic[ScreenResultType], Widget): or self._dirty_widgets ): self._update_timer.resume() + return - # The Screen is idle - a good opportunity to invoke the scheduled callbacks - - if self._callbacks: - self._on_timer_update() + await self._invoke_and_clear_callbacks() def _on_timer_update(self) -> None: """Called by the _update_timer.""" - self._update_timer.pause() if self.is_current: if self._layout_required: @@ -465,12 +462,11 @@ class Screen(Generic[ScreenResultType], Widget): self._scroll_required = False if self._repaint_required: - self._update_timer.resume() self._dirty_widgets.clear() self._dirty_widgets.add(self) self._repaint_required = False - if self._dirty_widgets and self.is_current: + if self._dirty_widgets: self._compositor.update_widgets(self._dirty_widgets) update = self._compositor.render_update( screen_stack=self.app._background_screens @@ -479,20 +475,12 @@ class Screen(Generic[ScreenResultType], Widget): self._dirty_widgets.clear() if self._callbacks: - self.post_message(events.InvokeCallbacks()) - - async def _on_invoke_callbacks(self, event: events.InvokeCallbacks) -> None: - """Handle PostScreenUpdate events, which are sent after the screen is updated""" - await self._invoke_and_clear_callbacks() + self.call_next(self._invoke_and_clear_callbacks) async def _invoke_and_clear_callbacks(self) -> None: """If there are scheduled callbacks to run, call them and clear the callback queue.""" if self._callbacks: - display_update = self._compositor.render_update( - screen_stack=self.app._background_screens - ) - self.app._display(self, display_update) callbacks = self._callbacks[:] self._callbacks.clear() for callback in callbacks: diff --git a/src/textual/scroll_view.py b/src/textual/scroll_view.py index 46dd934cb..63fd86a4e 100644 --- a/src/textual/scroll_view.py +++ b/src/textual/scroll_view.py @@ -7,6 +7,7 @@ from __future__ import annotations from rich.console import RenderableType +from ._animator import EasingFunction from .containers import ScrollableContainer from .geometry import Size @@ -111,3 +112,37 @@ class ScrollView(ScrollableContainer): from rich.panel import Panel return Panel(f"{self.scroll_offset} {self.show_vertical_scrollbar}") + + # Custom scroll to which doesn't require call_after_refresh + def scroll_to( + self, + x: float | None = None, + y: float | None = None, + *, + animate: bool = True, + speed: float | None = None, + duration: float | None = None, + easing: EasingFunction | str | None = None, + force: bool = False, + ) -> None: + """Scroll to a given (absolute) coordinate, optionally animating. + + Args: + x: X coordinate (column) to scroll to, or `None` for no change. + y: Y coordinate (row) to scroll to, or `None` for no change. + animate: Animate to new scroll position. + speed: Speed of scroll if `animate` is `True`; or `None` to use `duration`. + duration: Duration of animation, if `animate` is `True` and `speed` is `None`. + easing: An easing method for the scrolling animation. + force: Force scrolling even when prohibited by overflow styling. + """ + + self._scroll_to( + x, + y, + animate=animate, + speed=speed, + duration=duration, + easing=easing, + force=force, + ) diff --git a/src/textual/timer.py b/src/textual/timer.py index eae882bc9..c5c300266 100644 --- a/src/textual/timer.py +++ b/src/textual/timer.py @@ -131,20 +131,18 @@ class Timer: continue now = _time.get_time() wait_time = max(0, next_timer - now) - if wait_time > 1 / 1000: - await sleep(wait_time) - + await sleep(wait_time) count += 1 + try: + await self._tick(next_timer=next_timer, count=count) + except EventTargetGone: + break await self._active.wait() if self._reset: start = _time.get_time() count = 0 self._reset = False continue - try: - await self._tick(next_timer=next_timer, count=count) - except EventTargetGone: - break async def _tick(self, *, next_timer: float, count: int) -> None: """Triggers the Timer's action: either call its callback, or sends an event to its target""" diff --git a/tests/test_call_later.py b/tests/test_call_later.py index 506db93a5..98cdf4769 100644 --- a/tests/test_call_later.py +++ b/tests/test_call_later.py @@ -39,4 +39,4 @@ async def test_call_after_refresh() -> None: app.call_after_refresh(callback) await asyncio.wait_for(called_event.wait(), 1) app_display_count = app.display_count - assert app_display_count > display_count + assert app_display_count == display_count