Skip to content

Commit f076844

Browse files
committed
fix remove freeze
1 parent ccabbf0 commit f076844

File tree

5 files changed

+63
-56
lines changed

5 files changed

+63
-56
lines changed

src/textual/app.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
from . import Logger, LogGroup, LogVerbosity, actions, events, log, messages
4141
from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction
42+
from .await_remove import AwaitRemove
4243
from ._ansi_sequences import SYNC_END, SYNC_START
4344
from ._callback import invoke
4445
from ._context import active_app
@@ -353,6 +354,7 @@ def __init__(
353354
else None
354355
)
355356
self._screenshot: str | None = None
357+
self._dom_lock = asyncio.Lock()
356358

357359
@property
358360
def return_value(self) -> ReturnType | None:
@@ -1936,6 +1938,42 @@ def _walk_children(self, root: Widget) -> Iterable[list[Widget]]:
19361938
for child in widget.children:
19371939
push(child)
19381940

1941+
def _remove_nodes(self, widgets: list[Widget]) -> AwaitRemove:
1942+
"""Remove nodes from DOM, and return an awaitable that awaits cleanup.
1943+
1944+
Args:
1945+
widgets (list[Widget]): List of nodes to remvoe.
1946+
1947+
Returns:
1948+
AwaitRemove: Awaitable that returns when the nodes have been fully removed.
1949+
"""
1950+
1951+
async def remove_task(
1952+
widgets: list[Widget], finished_event: asyncio.Event
1953+
) -> None:
1954+
try:
1955+
await self._prune_nodes(widgets)
1956+
finally:
1957+
finished_event.set()
1958+
1959+
removed_widgets = self._detach_from_dom(widgets)
1960+
self.refresh(layout=True)
1961+
1962+
finished_event = asyncio.Event()
1963+
asyncio.create_task(remove_task(removed_widgets, finished_event))
1964+
1965+
return AwaitRemove(finished_event)
1966+
1967+
async def _prune_nodes(self, widgets: list[Widget]) -> None:
1968+
"""Remove nodes and children.
1969+
1970+
Args:
1971+
widgets (Widget): _description_
1972+
"""
1973+
async with self._dom_lock:
1974+
for widget in widgets:
1975+
await self._prune_node(widget)
1976+
19391977
async def _prune_node(self, root: Widget) -> None:
19401978
"""Remove a node and its children. Children are removed before parents.
19411979

src/textual/css/query.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -356,16 +356,9 @@ def remove(self) -> AwaitRemove:
356356
Returns:
357357
AwaitRemove: An awaitable object that waits for the widgets to be removed.
358358
"""
359-
prune_finished_event = asyncio.Event()
360359
app = active_app.get()
361-
app.post_message_no_wait(
362-
events.Prune(
363-
app,
364-
widgets=app._detach_from_dom(list(self)),
365-
finished_flag=prune_finished_event,
366-
)
367-
)
368-
return AwaitRemove(prune_finished_event)
360+
await_remove = app._remove_nodes(list(self))
361+
return await_remove
369362

370363
def set_styles(
371364
self, css: str | None = None, **update_styles

src/textual/events.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -127,28 +127,6 @@ class Unmount(Mount, bubble=False, verbose=False):
127127
"""Sent when a widget is unmounted and may not longer receive messages."""
128128

129129

130-
class Prune(Event, bubble=False):
131-
"""Sent to the app to ask it to prune one or more widgets from the DOM.
132-
133-
Attributes:
134-
widgets (list[Widgets]): The list of widgets to prune.
135-
finished_flag (asyncio.Event): An asyncio Event to that will be flagged when the prune is done.
136-
"""
137-
138-
def __init__(
139-
self, sender: MessageTarget, widgets: list[Widget], finished_flag: asyncio.Event
140-
) -> None:
141-
"""Initialise the event.
142-
143-
Args:
144-
widgets (list[Widgets]): The list of widgets to prune.
145-
finished_flag (asyncio.Event): An asyncio Event to that will be flagged when the prune is done.
146-
"""
147-
super().__init__(sender)
148-
self.finished_flag = finished_flag
149-
self.widgets = widgets
150-
151-
152130
class Show(Event, bubble=False):
153131
"""Sent when a widget has become visible."""
154132

src/textual/screen.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -310,18 +310,19 @@ async def _on_idle(self, event: events.Idle) -> None:
310310
# Check for any widgets marked as 'dirty' (needs a repaint)
311311
event.prevent_default()
312312

313-
if self.is_current:
314-
if self._layout_required:
315-
self._refresh_layout()
316-
self._layout_required = False
317-
self._dirty_widgets.clear()
318-
if self._repaint_required:
319-
self._dirty_widgets.clear()
320-
self._dirty_widgets.add(self)
321-
self._repaint_required = False
322-
323-
if self._dirty_widgets:
324-
self.update_timer.resume()
313+
async with self.app._dom_lock:
314+
if self.is_current:
315+
if self._layout_required:
316+
self._refresh_layout()
317+
self._layout_required = False
318+
self._dirty_widgets.clear()
319+
if self._repaint_required:
320+
self._dirty_widgets.clear()
321+
self._dirty_widgets.add(self)
322+
self._repaint_required = False
323+
324+
if self._dirty_widgets:
325+
self.update_timer.resume()
325326

326327
# The Screen is idle - a good opportunity to invoke the scheduled callbacks
327328
await self._invoke_and_clear_callbacks()

src/textual/widget.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -523,15 +523,19 @@ def mount(
523523

524524
# Decide the final resting place depending on what we've been asked
525525
# to do.
526+
insert_before: int | None = None
527+
insert_after: int | None = None
526528
if before is not None:
527-
parent, before = self._find_mount_point(before)
529+
parent, insert_before = self._find_mount_point(before)
528530
elif after is not None:
529-
parent, after = self._find_mount_point(after)
531+
parent, insert_after = self._find_mount_point(after)
530532
else:
531533
parent = self
532534

533535
return AwaitMount(
534-
self.app._register(parent, *widgets, before=before, after=after)
536+
self.app._register(
537+
parent, *widgets, before=insert_before, after=insert_after
538+
)
535539
)
536540

537541
def move_child(
@@ -697,7 +701,6 @@ def get_content_height(self, container: Size, viewport: Size, width: int) -> int
697701
Returns:
698702
int: The height of the content.
699703
"""
700-
701704
if self.is_container:
702705
assert self._layout is not None
703706
height = (
@@ -2114,15 +2117,9 @@ def remove(self) -> AwaitRemove:
21142117
Returns:
21152118
AwaitRemove: An awaitable object that waits for the widget to be removed.
21162119
"""
2117-
prune_finished_event = AsyncEvent()
2118-
self.app.post_message_no_wait(
2119-
events.Prune(
2120-
self,
2121-
widgets=self.app._detach_from_dom([self]),
2122-
finished_flag=prune_finished_event,
2123-
)
2124-
)
2125-
return AwaitRemove(prune_finished_event)
2120+
2121+
await_remove = self.app._remove_nodes([self])
2122+
return await_remove
21262123

21272124
def render(self) -> RenderableType:
21282125
"""Get renderable for widget.

0 commit comments

Comments
 (0)