diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index f63945122e..b9ee6a9b8a 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -41,6 +41,11 @@ def width(self): def height(self): return self.native.getHeight() + def make_dirty(self, widget=None): + # TODO: This won't be required once we complete the refactor + # making container a separate impl concept. + pass + class Window: def __init__(self, interface, title, position, size): diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 586196ff4a..7ea9d3961c 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -47,6 +47,11 @@ def width(self): def height(self): return 0 if self.view is None else self.view.frame.size.height + def make_dirty(self, widget=None): + # TODO: This won't be required once we complete the refactor + # making container a separate impl concept. + pass + class WindowDelegate(NSObject): diff --git a/core/src/toga/style/applicator.py b/core/src/toga/style/applicator.py index 89aae76c96..f45418d648 100644 --- a/core/src/toga/style/applicator.py +++ b/core/src/toga/style/applicator.py @@ -4,8 +4,12 @@ class TogaApplicator: def __init__(self, widget): self.widget = widget + def refresh(self): + # print("RE-EVALUATE LAYOUT", self.widget) + self.widget.refresh() + def set_bounds(self): - # print("LAYOUT", self.widget, self.widget.layout) + # print("APPLY LAYOUT", self.widget, self.widget.layout) self.widget._impl.set_bounds( self.widget.layout.absolute_content_left, self.widget.layout.absolute_content_top, diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 2f71785273..6c723ad678 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -93,7 +93,7 @@ def apply(self, prop, value): else: value = LEFT self._applicator.set_text_alignment(value) - if prop == "text_direction": + elif prop == "text_direction": if self.text_align is None: self._applicator.set_text_alignment(RIGHT if value == RTL else LEFT) elif prop == "color": @@ -101,10 +101,7 @@ def apply(self, prop, value): elif prop == "background_color": self._applicator.set_background_color(value) elif prop == "visibility": - hidden = False - if value == HIDDEN: - hidden = True - self._applicator.set_hidden(hidden) + self._applicator.set_hidden(value == HIDDEN) elif prop in ( "font_family", "font_size", @@ -121,6 +118,10 @@ def apply(self, prop, value): weight=self.font_weight, ) ) + else: + # Any other style change will cause a change in layout geometry, + # so perform a refresh. + self._applicator.refresh() def layout(self, node, viewport): # Precompute `scale_factor` by providing it as a default param. diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 1d105da405..a42f920021 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -281,8 +281,11 @@ def refresh(self): self._root.refresh() else: self.refresh_sublayouts() + # We can't compute a layout until we have a viewport if self._impl.viewport: super().refresh(self._impl.viewport) + # Refreshing the layout means the viewport needs a redraw. + self._impl.viewport.make_dirty() def refresh_sublayouts(self): for child in self.children: diff --git a/core/tests/style/test_pack.py b/core/tests/style/test_pack.py index 22ece28d62..12f3f24877 100644 --- a/core/tests/style/test_pack.py +++ b/core/tests/style/test_pack.py @@ -35,6 +35,12 @@ def __init__(self, name, style, size=None, children=None): def __repr__(self): return f"<{self.name} at {id(self)}>" + def refresh(self): + # We're directly modifying sytles and computing layouts for specific + # viewports, so we don't need to trigger layout changes when a style is + # changed. + pass + class TestViewport: def __init__(self, width, height, dpi=96, baseline_dpi=96): diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 843359d801..7198d3b25a 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -16,6 +16,11 @@ def width(self): def height(self): return self.window.get_size()[1] + def make_dirty(self, widget=None): + # TODO: This won't be required once we complete the refactor + # making container a separate impl concept. + pass + class Window(LoggedObject): def __init__(self, interface, title, position, size): diff --git a/gtk/src/toga_gtk/container.py b/gtk/src/toga_gtk/container.py index 14e29c04b2..b577db531d 100644 --- a/gtk/src/toga_gtk/container.py +++ b/gtk/src/toga_gtk/container.py @@ -19,24 +19,63 @@ def __init__(self): self.dpi = 96 self.baseline_dpi = self.dpi - self.dirty = set() + # The dirty widgets are the set of widgets that are known to need + # re-hinting before any redraw occurs. + self._dirty_widgets = set() + + # A flag that can be used to explicitly flag that a redraw is required. + self._needs_redraw = True + + @property + def needs_redraw(self): + """Does the container need a redraw?""" + return self._needs_redraw or bool(self._dirty_widgets) + + def make_dirty(self, widget=None): + """Mark the container (or a specific widget in the container) as dirty. + + :param widget: Optional; if provided, the widget that is now dirty. If + not provided, the entire container is considered dirty. + """ + if widget is None: + self._needs_redraw = True + self.queue_resize() + else: + self._dirty_widgets.add(widget) + widget.native.queue_resize() @property def width(self): - # Treat `native=None` as a 0x0 viewport. + """The display width of the container. + + If the container doesn't have any content yet, the width is 0. + """ if self._content is None: return 0 return self.get_allocated_width() @property def height(self): - # Treat `native=None` as a 0x0 viewport. + """The display height of the container. + + If the container doesn't have any content yet, the height is 0. + """ if self._content is None: return 0 return self.get_allocated_height() @property def content(self): + """The Toga implementation widget that is the root content of this + container. + + All children of the root content will also be added to the container as + a result of assigning content. + + If the container already has content, the old content will be replaced. + The old root content and all it's children will be removed from the + container. + """ return self._content @content.setter @@ -49,12 +88,17 @@ def content(self, widget): widget.container = self def recompute(self): - if self._content and self.dirty: + """Rehint and re-layout the container's content, if necessary. + + Any widgets known to be dirty will be rehinted. The minimum + possible layout size for the container will also be recomputed. + """ + if self._content and self.needs_redraw: # If any of the widgets have been marked as dirty, # recompute their bounds, and re-evaluate the minimum # allowed size fo the layout. - while self.dirty: - widget = self.dirty.pop() + while self._dirty_widgets: + widget = self._dirty_widgets.pop() widget.gtk_rehint() # Compute the layout using a 0-size container @@ -67,7 +111,16 @@ def recompute(self): self.min_height = self._content.interface.layout.height def do_get_preferred_width(self): - # Calculate the minimum and natural width of the container. + """Return (recomputing if necessary) the preferred width for the + container. + + The preferred size of the container is it's minimum size. This + preference will be overridden with the layout size when the layout is + applied. + + If the container does not yet have content, the minimum width is set to + 0. + """ # print("GET PREFERRED WIDTH", self._content) if self._content is None: return 0, 0 @@ -80,7 +133,16 @@ def do_get_preferred_width(self): return self.min_width, self.min_width def do_get_preferred_height(self): - # Calculate the minimum and natural height of the container. + """Return (recomputing if necessary) the preferred height for the + container. + + The preferred size of the container is it's minimum size. This + preference will be overridden with the layout size when the + layout is applied. + + If the container does not yet have content, the minimum height + is set to 0. + """ # print("GET PREFERRED HEIGHT", self._content) if self._content is None: return 0, 0 @@ -93,6 +155,14 @@ def do_get_preferred_height(self): return self.min_height, self.min_height def do_size_allocate(self, allocation): + """Perform the actual layout for the widget, and all it's children. + + The container will assume whatever size it has been given by GTK - + usually the full space of the window that holds the container. + The layout will then be re-computed based on this new available size, + and that new geometry will be applied to all child widgets of the + container. + """ # print(self._content, f"Container layout {allocation.width}x{allocation.height} @ {allocation.x}x{allocation.y}") # The container will occupy the full space it has been allocated. @@ -124,3 +194,6 @@ def do_size_allocate(self, allocation): widget_allocation.height = widget.interface.layout.content_height widget.size_allocate(widget_allocation) + + # The layout has been redrawn + self._needs_redraw = False diff --git a/gtk/src/toga_gtk/widgets/base.py b/gtk/src/toga_gtk/widgets/base.py index 1ada9bad21..a0d7e2a3de 100644 --- a/gtk/src/toga_gtk/widgets/base.py +++ b/gtk/src/toga_gtk/widgets/base.py @@ -72,9 +72,9 @@ def set_tab_index(self, tab_index): ###################################################################### def set_bounds(self, x, y, width, height): - # No implementation required here; the new sizing will be picked up - # by the box's allocation handler. - pass + # If the bounds have changed, we need to queue a resize on the container + if self.container: + self.container.make_dirty() def set_alignment(self, alignment): # By default, alignment can't be changed @@ -119,7 +119,7 @@ def rehint(self): # Instead, put the widget onto a dirty list to be rehinted before the # next layout. if self.container: - self.container.dirty.add(self) + self.container.make_dirty(self) def gtk_rehint(self): # Perform the actual GTK rehint. diff --git a/gtk/tests_backend/widgets/base.py b/gtk/tests_backend/widgets/base.py index 12d81e2bed..1d99982aea 100644 --- a/gtk/tests_backend/widgets/base.py +++ b/gtk/tests_backend/widgets/base.py @@ -20,15 +20,9 @@ def assert_container(self, container): async def redraw(self): """Request a redraw of the app, waiting until that redraw has completed.""" - # Refresh the layout - self.widget.window.content.refresh() - - self.impl.container.queue_resize() - # Force a repaint - while self.impl.container.dirty: - while Gtk.events_pending(): - Gtk.main_iteration_do(blocking=False) + while self.impl.container.needs_redraw or Gtk.events_pending(): + Gtk.main_iteration_do(blocking=False) @property def enabled(self): diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 74cb4a42cb..cfe7c5da2e 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -52,6 +52,11 @@ def height(self): self.widget.native.bounds.size.height - self.bottom_offset - self.top_offset ) + def make_dirty(self, widget=None): + # TODO: This won't be required once we complete the refactor + # making container a separate impl concept. + pass + class Window: def __init__(self, interface, title, position, size): diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 5a2bc996a9..2974542818 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -14,6 +14,11 @@ def width(self): def height(self): return 768 + def make_dirty(self, widget=None): + # TODO: This won't be required once we complete the refactor + # making container a separate impl concept. + pass + class Window: def __init__(self, interface, title, position, size): diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 3bc72dc88b..e3dc446020 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -30,6 +30,11 @@ def dpi(self): return self.baseline_dpi return self.native.CreateGraphics().DpiX + def make_dirty(self, widget=None): + # TODO: This won't be required once we complete the refactor + # making container a separate impl concept. + pass + class Window: def __init__(self, interface, title, position, size):