Skip to content

Commit

Permalink
Automatically recompute layout on style changes.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Feb 16, 2023
1 parent e86a136 commit 996cf23
Show file tree
Hide file tree
Showing 13 changed files with 80 additions and 22 deletions.
5 changes: 5 additions & 0 deletions android/src/toga_android/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions cocoa/src/toga_cocoa/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
6 changes: 5 additions & 1 deletion core/src/toga/style/applicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 6 additions & 5 deletions core/src/toga/style/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,15 @@ 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":
self._applicator.set_color(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",
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions core/src/toga/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions core/tests/style/test_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions dummy/src/toga_dummy/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
28 changes: 24 additions & 4 deletions gtk/src/toga_gtk/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,24 @@ 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):
return self._needs_redraw or bool(self._dirty_widgets)

def make_dirty(self, widget=None):
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):
Expand Down Expand Up @@ -49,12 +66,12 @@ def content(self, widget):
widget.container = self

def recompute(self):
if self._content and self.dirty:
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
Expand Down Expand Up @@ -124,3 +141,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
8 changes: 4 additions & 4 deletions gtk/src/toga_gtk/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.mark_dirty()

def set_alignment(self, alignment):
# By default, alignment can't be changed
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 2 additions & 8 deletions gtk/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions iOS/src/toga_iOS/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions web/src/toga_web/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions winforms/src/toga_winforms/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 996cf23

Please sign in to comment.