-
-
Notifications
You must be signed in to change notification settings - Fork 671
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
Modify container handling for GTK #1794
Merged
Merged
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
1a477a3
Continuation of #1778
mhsmith 6b2f92b
Update PR number
mhsmith abebc9a
Removed a redundant make_dirty() call.
freakboy3742 dd8114d
Reduce excessive refreshes
mhsmith e642791
Add examples/resize
mhsmith 8d41a33
Make GTK container.needs_redraw public, since it's used by the probe
mhsmith 82df332
Add flex control to examples/resize
mhsmith 8e391ee
Remove redundant calls to `rehint`
mhsmith 9f65938
Ensure refresh always happens after rehint (working on Cocoa)
mhsmith 30150ee
Working on Winforms
mhsmith 0b265f6
Working on Android and iOS
mhsmith a19d509
Update dummy backend
mhsmith 4baff90
Working on GTK
mhsmith eba7cbe
Update web backend
mhsmith 08525ce
Add missing retain that causes iOS crashes.
freakboy3742 aa625ca
Tweak the resize app example so it can be run directly as a module.
freakboy3742 07a1fa2
Fix iOS background color tests
mhsmith b58bb1b
Update dummy event name from `rehint` to `refresh`
mhsmith File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Issues with reducing the size of windows on GTK have been resolved. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
The handling of GTK layouts has been modified to reduce the frequency and increase the accuracy of layout results. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
from .libs import Gdk, Gtk | ||
|
||
|
||
class TogaContainer(Gtk.Fixed): | ||
"""A GTK container widget implementing Toga's layout. | ||
|
||
This is a GTK widget, with no Toga interface manifestation. | ||
""" | ||
|
||
def __init__(self): | ||
super().__init__() | ||
self._content = None | ||
self.min_width = 100 | ||
self.min_height = 100 | ||
|
||
# GDK/GTK always renders at 96dpi. When HiDPI mode is enabled, it is | ||
# managed at the compositor level. See | ||
# https://wiki.archlinux.org/index.php/HiDPI#GDK_3_(GTK_3) for details | ||
self.dpi = 96 | ||
self.baseline_dpi = self.dpi | ||
|
||
# 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): | ||
"""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): | ||
"""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 | ||
def content(self, widget): | ||
if self._content: | ||
self._content.container = None | ||
|
||
self._content = widget | ||
if widget: | ||
widget.container = self | ||
|
||
def recompute(self): | ||
"""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_widgets: | ||
widget = self._dirty_widgets.pop() | ||
widget.gtk_rehint() | ||
|
||
# Compute the layout using a 0-size container | ||
self._content.interface.style.layout( | ||
self._content.interface, TogaContainer() | ||
) | ||
|
||
# print(" computed min layout", self._content.interface.layout) | ||
self.min_width = self._content.interface.layout.width | ||
self.min_height = self._content.interface.layout.height | ||
|
||
def do_get_preferred_width(self): | ||
"""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 | ||
|
||
# Ensure we have an accurate min layout size | ||
self.recompute() | ||
|
||
# The container will conform to the size of the allocation it is given, | ||
# so the min and preferred size are the same. | ||
return self.min_width, self.min_width | ||
|
||
def do_get_preferred_height(self): | ||
"""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 | ||
|
||
# Ensure we have an accurate min layout size | ||
self.recompute() | ||
|
||
# The container will conform to the size of the allocation it is given, | ||
# so the min and preferred size are the same. | ||
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. | ||
self.set_allocation(allocation) | ||
|
||
if self._content: | ||
# Re-evaluate the layout using the allocation size as the basis for geometry | ||
# print("REFRESH LAYOUT", allocation.width, allocation.height) | ||
self._content.interface.refresh() | ||
|
||
# WARNING! This is the list of children of the *container*, not | ||
# the Toga widget. Toga maintains a tree of children; all nodes | ||
# in that tree are direct children of the container. | ||
for widget in self.get_children(): | ||
if not widget.get_visible(): | ||
# print(" not visible {widget.interface}") | ||
pass | ||
else: | ||
# Set the size of the child widget to the computed layout size. | ||
# print(f" allocate child {widget.interface}: {widget.interface.layout}") | ||
widget_allocation = Gdk.Rectangle() | ||
widget_allocation.x = ( | ||
widget.interface.layout.absolute_content_left + allocation.x | ||
) | ||
widget_allocation.y = ( | ||
widget.interface.layout.absolute_content_top + allocation.y | ||
) | ||
widget_allocation.width = widget.interface.layout.content_width | ||
widget_allocation.height = widget.interface.layout.content_height | ||
|
||
widget.size_allocate(widget_allocation) | ||
|
||
# The layout has been redrawn | ||
self._needs_redraw = False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See top-level review comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a
make_dirty()
with no arguments, so it's marking the container as dirty. On GTK, this triggers a "queue_resize" event, which (eventually) causes the actual layout to occur.In the GTK context, this results in a redundant layout calculation (the one 2 lines above the highlighted line); however, this call is the one that is required on other platforms.
To me, this is part of the problem with "container/viewport" abstraction. The
refresh()
API entry point is defined on Node, where it requires a viewport definition; but a good portion of the implementation in toga-core replicates the definition from Node so that the viewport can be injected.The existence of
refresh()
on toga.core.Widget is also a source of confusion, with people using it as a public facing API in the hope of applying any style change. Admittedly, this has been needed due to various bugs; however, once we've fixed those bugs (and this PR and #1761 fixes a lot of those cases), manual calls torefresh()
shouldn't be needed at all.A lot of what
refresh()
has historically been used for could actually be replaced withmake_dirty()
on the container. If the places that are currently callingsome_widget.refresh()
was replaced withsome_widget.container.make_dirty()
, that would remove the redundant layout in the GTK case, and other platforms could implementmake_dirty()
as required (e.g., immediately invoking layout on the root widget of the layout with the appropriate viewport on Cocoa).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the example you've provided - this
make_dirty()
won't be needed, because any change on the text widget that marks the text widget as needing a rehint (e.g., changing the text) will also flag the container as dirty, causing a layout. This specificmark_dirty()
is needed for the case where a layout is happening but there's no widget-specific rehint requested. One example would be marking the text widget asstyle.flex=1
. That will affect the layout, but not the hinting of a specific widget.Again, this would be a situation where it would be preferable for the applicator to invoke
mark_dirty()
directly on the container, and have backends respond with a layout at the appropriate time (immediately on Cocoa; on a redraw for GTK).