-
-
Notifications
You must be signed in to change notification settings - Fork 689
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
Conversation
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.
@freakboy3742: Can you explain why the two calls to make_dirty
below are needed? They both seem to be requesting a layout as the result of another layout, and I guess the only reason this isn't causing an infinite loop is because, as it says here:
Calls to gtk_widget_queue_resize() from inside GtkWidgetClass::size_allocate will be silently ignored.
I tried commenting both of them out and testing with the following app, and everything seemed to work fine, including the minimum window size.
Example app
import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW
INITIAL_SIZE = 2
MAX_SIZE = 5
class HelloWorld(toga.App):
def startup(self):
self.width_button = toga.Button("", on_press=self.change_width)
self.height_button = toga.Button("", on_press=self.change_height)
self.label = toga.Label("", style=Pack(background_color="cyan"))
main_box = toga.Box(style=Pack(direction=COLUMN), children=[
toga.Box(style=Pack(direction=ROW), children=[
self.width_button,
self.height_button,
]),
toga.Box(style=Pack(direction=ROW), children=[
self.label,
toga.Label("", style=Pack(background_color="pink", flex=1)),
toga.Label("", style=Pack(background_color="yellow", flex=1)),
])
])
self.width = self.height = INITIAL_SIZE
self.set_text()
self.main_window = toga.MainWindow(title=self.formal_name)
self.main_window.content = main_box
self.main_window.show()
def set_text(self):
self.width_button.text = f"Width {self.width}"
self.height_button.text = f"Height {self.height}"
self.label.text = "\n".join(
" ".join("X" for i in range(self.width))
for j in range(self.height)
)
def change_width(self, button):
self.width = (self.width + 1) % (MAX_SIZE + 1)
self.set_text()
def change_height(self, button):
self.height = (self.height + 1) % (MAX_SIZE + 1)
self.set_text()
def main():
return HelloWorld()
core/src/toga/widgets/base.py
Outdated
# Refreshing the layout means the viewport needs a redraw. | ||
self._impl.viewport.make_dirty() |
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 to refresh()
shouldn't be needed at all.
A lot of what refresh()
has historically been used for could actually be replaced with make_dirty()
on the container. If the places that are currently calling some_widget.refresh()
was replaced with some_widget.container.make_dirty()
, that would remove the redundant layout in the GTK case, and other platforms could implement make_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 specific mark_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 as style.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).
As a result of the GTK restructuring,
I've added a new "resize" example app for testing all of this. |
@@ -9,12 +9,11 @@ def create(self): | |||
self.native = Gtk.Button() | |||
self.native.interface = self.interface | |||
|
|||
self.native.connect("show", lambda event: self.rehint()) | |||
self.native.connect("show", lambda event: self.refresh()) |
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.
I suspect these event handlers may no longer be necessary. @freakboy3742: Do you know what they're for?
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.
The GTK "Show" event is the event that fires when the widget is first visible on screen; as I recall, it was possible (at least, previously, anyway), for the layout algorithm to fire when the widget hadn't yet been made visible, which resulted in initial sizes of (0,0). This call forced a rehint when the widget was made visible, to ensure the first visible size was sensible.
However, this may now be reundant, as displaying the container will cause a layout, and (I think?) implicitly rehint of all widgets.
I can reproduce the iOS testbed crash locally, but I don't understand what it means. @freakboy3742: Any idea? |
Yup - I've seen that one, and fixed it in the audit-button branch. It's effectively a segfault with a weird presentation (and sometimes, if the wind is blowing just right, it does occur as a segfault). The issue is that colors are being stored in a cache to remove the need to repeatedly re-allocate "RED"; but the object references aren't being retained correctly, so the instance is being garbage collected on the ObjC side. I've pushed the fix from the button branch. |
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.
There's some good conceptual cleanup here.
To make sure I've understood the broad strokes:
- Core widget.refresh() - Instructs the backend widget to refresh, then, passes the request to the root element in the layout tree to computes a layout, ensuring that the entire widget tree has been laid out.
- Backend widget.rehint() - recomputes geometry for the widget (or, queues such a request to occur as soon as possible)
- Backend widget.refresh() - On most platforms, does an immediate backend widget rehint. On GTK, marks the widget as dirty, which will cause a backend widget rehint to occur when the UI is next redrawn.
On the interface, when any widget property (like text) or style property (like font or flex=1) changes, that widget gets an interface refresh. This implies a rehint of that widget (soon, if not immediately), followed by a re-layout.
The problem I can see is that there's a redundant layout pass on GTK. Calling an interface level refresh() does a rehint, which marks the GTK widget and container dirty, queuing a redraw, which calls do_size_allocate, doing a layout. However, the interface level refresh also does an immediate layout, which calls set_bounds on each widget, marking the container dirty, queuing a redraw, etc.
My initial read on this is that the issue is the dual purpose being carried by refresh()
on the core widget. That API endpoint is inherited from Node, and is a key part of doing layout. However, we don't want to do a layout on GTK unless we're actually allocating the container; on other platforms, calling Node.refresh() needs to follow directly after the rehint. It seems to me that the call to refresh the Node should possibly be invoked from the backend - either directly, as part of the refresh() method, or indirectly in the case of GTK. By extension, that means the "reflect these changes visually" API is _impl.refresh()
, not core refresh(). Does that make sense?
# 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. | ||
resized = (allocation.width, allocation.height) != (self.width, self.height) |
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.
Flagging this because I'm not 100% sure - in the process of writing my original version of this PR, I came across a lot of commentary talking about the relationship between gtk_widget_size_allocate()
and do_size_allocate()
(e.g. in the docs for get_allocation) - effectively flagging that calling size allocation methods while you're doing a size allocation can be complex.
AFAICT, I think what you've got here is correct - you're getting the current allocation (indirectly, via the self.width and self.height properties), and then setting the new allocation on the next line; and in our case, we're not doing anything complex (just setting the provided allocation, as is, to consume all available space) - but I wanted to confirm that is what you think it's doing as well.
This comment is also possibly of interest.
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.
Yes, that's the idea. And it sounds as if the "adjustment" mentioned in those links is about GTK-level margins and alignment, which we're not using.
Details from an in-person conversation:
|
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.
As noted in the previous comment, there's still a lot of work needed in container layout, and the potential for a number of optimisations - but this is a significant improvement, and leaves everything in a stable state.
[Originally from #1778 by @freakboy3742]
In the process of performing the audit of Button, it became clear that GTK had some fundamental issues with the layout of widgets, and the root widget in particular.
This PR performs a radical change to how layout is achieved on GTK, which fixes a lot of edge case bugs.
rehint()
request doesn't do an immediate layout; instead, it adds the widget to a list of "dirty" widgets. When a layout occurs, all dirty widgets are actually re-hinted, ensuring that the hints are the right size going into a layout.We should also investigate whether this change (or platform-appropriate variations) should be made on other backends. I strongly suspect that the root widget handling is subtly wrong on most platforms (except GTK, at this point). As an interim measure, the implementation maintains an interface that allows for
_impl.container
and_impl.viewport
, even though they are now the same object on the GTK backend.PR Checklist: