Skip to content
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

[widget Audit] toga.ScrollContainer #1969

Merged
merged 46 commits into from
Jul 12, 2023

Conversation

freakboy3742
Copy link
Member

@freakboy3742 freakboy3742 commented Jun 8, 2023

Audit of ScrollContainer.

Includes a Cocoa and iOS version of #1794. This allows us to remove the special case handling of the root content object in any given layout - constraints are used for all Toga widgets, rather than "all widgets except the root widget", and we now know that all widgets that are in a layout have a container.

It also fixes some issues with ScrollContainer specifically, as we need to decouple the size of the document being scrolled from the layout:

  • The size of the document being scrolled should not alter the layout of the scroll container. For Cocoa/iOS, this means the document must be at least the size of the content window it is being displayed in
  • If the document is larger than the scroll container in an axis that doesn't allow scrolling, Cocoa and iOS will turn on scrolling in that axis.

Some changes that follow on from this:

  • The dummy backend has been modified to be more "Container"-like than "viewport"-like.
  • When adding/removing content from a layout, the widget's refresh needs to be updated, not the window. If the content is inside a scroll container, it should only impact the layout inside the scroll container, not the overall window.
  • refresh_sublayouts isn't needed for scroll container; and I suspect it won't be needed for OptionContainer or SplitContainer either.
  • iOS ViewController handling has been moved to be a container property. This lays the groundwork for introducing other controllers, such as the UITabController for OptionView, or a UINavigationController for navigating stacks of content.

Issues fixed:

Audit checklist

  • Core tests ported to pytest
  • Core tests at 100% coverage
  • Core docstrings complete, and in Sphinx format
  • Documentation complete and consistently formatted
  • cocoa backend at 100% coverage
  • winforms backend at 100% coverage
  • gtk backend at 100% coverage
  • iOS backend at 100% coverage
  • android backend at 100% coverage
  • Widget support table updated
  • Widget Issue backlog reviewed

@freakboy3742 freakboy3742 force-pushed the audit-scrollcontainer branch from a860811 to e256720 Compare June 9, 2023 01:26
@freakboy3742 freakboy3742 force-pushed the audit-scrollcontainer branch 3 times, most recently from 5b5b21d to 8f56f27 Compare June 12, 2023 04:48
@freakboy3742 freakboy3742 force-pushed the audit-scrollcontainer branch 2 times, most recently from afb0600 to ba3a44f Compare June 12, 2023 06:29
@freakboy3742 freakboy3742 force-pushed the audit-scrollcontainer branch from ba3a44f to 73edde4 Compare June 12, 2023 06:42
@freakboy3742 freakboy3742 marked this pull request as ready for review June 13, 2023 07:30
@freakboy3742
Copy link
Member Author

freakboy3742 commented Jul 6, 2023

I've just (re)audited the ScrollContainer issues; I've presumptively put them all on the ticket as "fixed"; the current status of each AFAICT is:

[Merged into the Fixes list above, except for the following. -- @mhsmith]

@freakboy3742 freakboy3742 mentioned this pull request Jul 7, 2023
42 tasks
@mhsmith
Copy link
Member

mhsmith commented Jul 10, 2023

Includes a Cocoa and iOS version of #1794. This allows us to remove the special case handling of the root content object in any given layout - constraints are used for all Toga widgets, rather than "all widgets except the root widget", and we now know that all widgets that are in a layout have a container.

The same has now been done for Winforms and Android.

Comment on lines 96 to 98
self._content = widget
self.refresh()
if widget:
widget.refresh()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning for this change is that setting the content should not affect the size of self (the container), but it will affect the size of widget (the content).

Is this correct? And if so, is there anything which might incorrectly be depending on the previous behavior?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that this seems correct. I can't think of any behavior that would depending on this - it's just an easy optimisation, AFAICT.

On Cocoa/iOS, calling refresh on the widget will cause set_bounds() to be invoked on the widget, which will trigger a refresh on the content anyway; this optimisation shortcuts the unnecessary changes to the container, and goes directly to the content. When the content layout completes (or any other content change is applied), the container scroll bounds are updated by the on_refresh callback on the container.

On GTK, all you're changing is which widget is marked as dirty; once the widget is marked as dirty, it will get a layout in the next render pass. Again, all this change does is optimise how much is marked as dirty.

@mhsmith
Copy link
Member

mhsmith commented Jul 10, 2023

Winforms is now passing the testbed, but I still need to verify that everything in the Fixes list above is actually fixed.

Copy link
Member Author

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of questions inline; the only big issue I see is the composition vs inheritance thing for Window and ScrollContainer. Inheritance clearly works for these two cases - although the approach won't work for OptionContainer and SplitContainer (because there's multiple sub-containers).

def viewport(self):
return self._container

def scale_in(self, value):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick note for future explorers on which direction is "in" and "out" would be helpful here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, done.



class Window:
class Window(Container):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason for using "is-A" rather than "has-A" as the composition for Window and Container? It's an interesting discrepancy between the iOS/Cocoa/GTK world and the Android one; I'm wondering what the source of that difference is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the benefit of inheritance turned out to be fairly small. It's mainly just providing a single implementation of the set_content method, reducing the number of distinct objects you need to keep track of, and unifying the meaning of the word "container".

As discussed, my preference is to keep it the way it is for this PR, and revisit it in the SplitContainer PR where the container/content relationship will no longer be 1-1.

return self._viewport
return self._container

def scale_in(self, value):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with android - some documentation for future explorers would be helpful


class ScrollContainer(Widget, Container):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar thing here with composition vs inheritance for the ScrollContainer.

# at the top of this file).
def apply_insets():
need_scrollbar = False
if self.vertical and layout.height > self.height:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this comparison be with self.height, or inset_height?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.height is the correct value in both cases:

  • In the first call to apply_insets, self.height == full_height, because you're checking whether the initial layout needs a vertical scroll bar.
  • In the second call, self.height may have been set to inset_height if a horizontal scroll bar was added by the first call, in which case we now need to check whether the second layout fits in that smaller space.

vertical_shift += self.native.MainMenuStrip.Height

self.native_content.Location = Point(0, vertical_shift)
super().resize_content(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the essentially the reason I'm questioning the composition vs inheritance approach - composition gives a clear distinction between the process of resizing the window, and the fact this requires resizing the container.

@mhsmith
Copy link
Member

mhsmith commented Jul 11, 2023

Confirmed that all the Windows issues in the Fixes list are actually fixed, except for #1606, which I've added some notes to.

@mhsmith
Copy link
Member

mhsmith commented Jul 11, 2023

Everything looks fine now, except for a couple of uncovered branches in Cocoa and iOS which I'm not sure how to fix:

Users/runner/work/toga/toga/testbed/build/testbed/macos/app/Toga Testbed.app/Contents/Resources/app_packages/toga_cocoa/widgets/scrollcontainer.py 77 0 18 1 98.9% 31->exit

Users/runner/Library/Developer/CoreSimulator/Devices/72A177E7-6329-4DF7-ACCA-085F1FFB5AD6/data/Containers/Bundle/Application/ED74A15B-E796-4174-8E6E-16508E070879/Toga Testbed.app/app_packages/toga_iOS/widgets/scrollcontainer.py 76 0 20 1 99.0% 19->exit

@freakboy3742
Copy link
Member Author

Testbed test case added for the 2 missing lines. It's the edge case of a ScrollContainer with no content having it's layout updated. Looks like GTK, Windows, and Android all pass that test case without any additional changes.

@freakboy3742 freakboy3742 requested a review from mhsmith July 12, 2023 03:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment