Skip to content

Commit befe39c

Browse files
committed
Allow textual widgets to defer mounting children
- Textual does not permit mounting widgets on to other widgets that are not themselves mounted. - This implements a callback mechanism for widgets to mount their children they are mounted.
1 parent fe1137b commit befe39c

File tree

3 files changed

+48
-4
lines changed

3 files changed

+48
-4
lines changed

changes/2822.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The Textual backend is now compatible with versions of Textual after v0.63.3.

textual/src/toga_textual/widgets/base.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from collections import OrderedDict
2+
from collections.abc import Callable
3+
14
from travertino.size import at_least
25

6+
from textual.widget import Widget as TextualWidget
37
from toga.style.pack import ROW
48
from toga.types import Size
59

@@ -74,7 +78,7 @@ def set_bounds(self, x, y, width, height):
7478

7579
# Positions are more complicated. The (x,y) provided as an argument is
7680
# in absolute coordinates. The `content_left` ad `content_right` values
77-
# of the layout are relative coordianate. Textual doesn't allow specifying
81+
# of the layout are relative coordinate. Textual doesn't allow specifying
7882
# either absolute *or* relative - we can only specify margin values within
7983
# a row/column box. This means we need to reverse engineer the margins from
8084
# the computed layout.
@@ -158,7 +162,7 @@ def insert_child(self, index, child):
158162
pass
159163

160164
def remove_child(self, child):
161-
self.native.remove(child.native)
165+
self.native.remove_children([child.native])
162166

163167
def refresh(self):
164168
intrinsic = self.interface.intrinsic
@@ -169,3 +173,38 @@ def refresh(self):
169173

170174
intrinsic.width = self.scale_out_horizontal(intrinsic.width)
171175
intrinsic.height = self.scale_out_vertical(intrinsic.height)
176+
177+
178+
class DeferredMount:
179+
"""Mount children to a widget once the widget itself is mounted.
180+
181+
Inject this mixin in to the MRO for native Textual widgets that support children.
182+
183+
Textual does not permit mounting widgets on to a widget that is not itself already
184+
mounted. As such, when a child is added to an unmounted widget, a callback is added
185+
that can mount it. This mixin runs those callbacks once the widget is mounted.
186+
"""
187+
188+
def __init__(self, *children: TextualWidget):
189+
super().__init__(*children)
190+
self._on_mount_callback: OrderedDict[Widget, Callable] = OrderedDict()
191+
192+
def mount(self, widget, **kwargs):
193+
if self.is_attached:
194+
super().mount(widget, **kwargs)
195+
else:
196+
self._on_mount_callback[widget] = lambda: self.mount(widget)
197+
198+
def remove_children(self, widgets):
199+
for widget in widgets:
200+
try:
201+
self._on_mount_callback.pop(widget)
202+
except KeyError:
203+
super().remove_children([widget])
204+
205+
def on_mount(self):
206+
"""Textual calls this hook when the widget is mounted."""
207+
if self._on_mount_callback:
208+
for child, callback in self._on_mount_callback.copy().items():
209+
callback()
210+
self._on_mount_callback.pop(child)

textual/src/toga_textual/widgets/box.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
from textual.containers import Container as TextualContainer
44
from toga.style.pack import ROW
55

6-
from .base import Widget
6+
from .base import DeferredMount, Widget
7+
8+
9+
class TogaContainer(DeferredMount, TextualContainer):
10+
"""Textual container that supports deferred children mounting."""
711

812

913
class Box(Widget):
1014
def create(self):
11-
self.native = TextualContainer()
15+
self.native = TogaContainer()
1216

1317
def set_bounds(self, x, y, width, height):
1418
super().set_bounds(x, y, width, height)

0 commit comments

Comments
 (0)