Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch update #1832

Merged
merged 17 commits into from
Feb 21, 2023
Merged
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [0.12.0] - Unreleased

### Added

- Added `App.batch_update`
- Added horizontal rule to Markdown
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved

### Changed

- Scrolling by page now adds to current position.
- Markdown lists have been polished: a selection of bullets, better alignment of numbers, style tweaks

### Removed

Expand Down
6 changes: 3 additions & 3 deletions examples/dictionary.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ Input {
}

#results {
width: auto;
min-height: 100%;
padding: 0 1;
width: 100%;
height: auto;

}

#results-container {
Expand Down
9 changes: 4 additions & 5 deletions examples/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
except ImportError:
raise ImportError("Please install httpx with 'pip install httpx' ")

from rich.markdown import Markdown

from textual.app import App, ComposeResult
from textual.containers import Content
from textual.widgets import Input, Static
from textual.widgets import Input, Markdown


class DictionaryApp(App):
Expand All @@ -21,7 +20,7 @@ class DictionaryApp(App):

def compose(self) -> ComposeResult:
yield Input(placeholder="Search for a word")
yield Content(Static(id="results"), id="results-container")
yield Content(Markdown(id="results"), id="results-container")

def on_mount(self) -> None:
"""Called when app starts."""
Expand All @@ -35,7 +34,7 @@ async def on_input_changed(self, message: Input.Changed) -> None:
asyncio.create_task(self.lookup_word(message.value))
else:
# Clear the results
self.query_one("#results", Static).update()
await self.query_one("#results", Markdown).update("")

async def lookup_word(self, word: str) -> None:
"""Looks up a word."""
Expand All @@ -50,7 +49,7 @@ async def lookup_word(self, word: str) -> None:

if word == self.query_one(Input).value:
markdown = self.make_word_markdown(results)
self.query_one("#results", Static).update(Markdown(markdown))
await self.query_one("#results", Markdown).update(markdown)

def make_word_markdown(self, results: object) -> str:
"""Convert the results in to markdown."""
Expand Down
26 changes: 26 additions & 0 deletions examples/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,32 @@ Two tildes indicates strikethrough, e.g. `~~cross out~~` render ~~cross out~~.

Inline code is indicated by backticks. e.g. `import this`.

## Lists

1. Lists can be ordered
2. Lists can be unordered
- I must not fear.
- Fear is the mind-killer.
- Fear is the little-death that brings total obliteration.
- I will face my fear.
- I will permit it to pass over me and through me.
- And when it has gone past, I will turn the inner eye to see its path.
- Where the fear has gone there will be nothing. Only I will remain.

### Longer list

1. **Duke Leto I Atreides**, head of House Atreides
2. **Lady Jessica**, Bene Gesserit and concubine of Leto, and mother of Paul and Alia
3. **Paul Atreides**, son of Leto and Jessica
4. **Alia Atreides**, daughter of Leto and Jessica
5. **Gurney Halleck**, troubadour warrior of House Atreides
6. **Thufir Hawat**, Mentat and Master of Assassins of House Atreides
7. **Duncan Idaho**, swordmaster of House Atreides
8. **Dr. Wellington Yueh**, Suk doctor of House Atreides
9. **Leto**, first son of Paul and Chani who dies as a toddler
10. **Esmar Tuek**, a smuggler on Arrakis
11. **Staban Tuek**, son of Esmar

## Fences

Fenced code blocks are introduced with three back-ticks and the optional parser. Here we are rendering the code in a sub-widget with syntax highlighting and indent guides.
Expand Down
70 changes: 52 additions & 18 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
import warnings
from asyncio import Task
from concurrent.futures import Future
from contextlib import asynccontextmanager, redirect_stderr, redirect_stdout
from contextlib import (
asynccontextmanager,
contextmanager,
redirect_stderr,
redirect_stdout,
)
from datetime import datetime
from functools import partial
from pathlib import Path, PurePath
Expand All @@ -22,6 +27,7 @@
Any,
Awaitable,
Callable,
Generator,
Generic,
Iterable,
List,
Expand Down Expand Up @@ -411,6 +417,7 @@ def __init__(
self._screenshot: str | None = None
self._dom_lock = asyncio.Lock()
self._dom_ready = False
self._batch_count = 0
self.set_class(self.dark, "-dark-mode")

@property
Expand All @@ -426,6 +433,30 @@ def children(self) -> Sequence["Widget"]:
except ScreenError:
return ()

@contextmanager
def batch_update(self) -> Generator[None, None, None]:
"""Suspend all repaints until the end of the batch."""
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved
self._begin_batch()
try:
yield
finally:
self._end_batch()

def _begin_batch(self) -> None:
"""Begin a batch update."""
self._batch_count += 1

def _end_batch(self) -> None:
"""End a batch update."""
self._batch_count -= 1
assert self._batch_count >= 0, "This won't happen if you use `batch_update`"
if not self._batch_count:
try:
self.screen.check_idle()
except ScreenStackError:
pass
self.check_idle()

def animate(
self,
attribute: str,
Expand Down Expand Up @@ -1504,28 +1535,29 @@ async def invoke_ready_callback() -> None:
if inspect.isawaitable(ready_result):
await ready_result

try:
with self.batch_update():
try:
await self._dispatch_message(events.Compose(sender=self))
await self._dispatch_message(events.Mount(sender=self))
finally:
self._mounted_event.set()
try:
await self._dispatch_message(events.Compose(sender=self))
await self._dispatch_message(events.Mount(sender=self))
finally:
self._mounted_event.set()

Reactive._initialize_object(self)
Reactive._initialize_object(self)

self.stylesheet.update(self)
self.refresh()
self.stylesheet.update(self)
self.refresh()

await self.animator.start()
await self.animator.start()

except Exception:
await self.animator.stop()
raise
except Exception:
await self.animator.stop()
raise

finally:
self._running = True
await self._ready()
await invoke_ready_callback()
finally:
self._running = True
await self._ready()
await invoke_ready_callback()

try:
await self._process_messages_loop()
Expand Down Expand Up @@ -1611,11 +1643,12 @@ async def _on_compose(self) -> None:
raise TypeError(
f"{self!r} compose() returned an invalid response; {error}"
) from error

await self.mount_all(widgets)

def _on_idle(self) -> None:
"""Perform actions when there are no messages in the queue."""
if self._require_stylesheet_update:
if self._require_stylesheet_update and not self._batch_count:
nodes: set[DOMNode] = {
child
for node in self._require_stylesheet_update
Expand Down Expand Up @@ -1796,6 +1829,7 @@ async def _shutdown(self) -> None:
self._writer_thread.stop()

async def _on_exit_app(self) -> None:
self._begin_batch() # Prevent repaint / layout while shutting down
await self._message_queue.put(None)

def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
Expand Down
1 change: 0 additions & 1 deletion src/textual/cli/previews/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ def update_view(self) -> None:
content.mount(ColorsView())

def on_button_pressed(self, event: Button.Pressed) -> None:
self.bell()
self.query(ColorGroup).remove_class("-active")
group = self.query_one(f"#group-{event.button.id}", ColorGroup)
group.add_class("-active")
Expand Down
2 changes: 1 addition & 1 deletion src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ async def _on_idle(self, event: events.Idle) -> None:
# Check for any widgets marked as 'dirty' (needs a repaint)
event.prevent_default()

if self.is_current:
if not self.app._batch_count and self.is_current:
async with self.app._dom_lock:
if self.is_current:
if self._layout_required:
Expand Down
Loading