Skip to content

Commit

Permalink
Cocoa ScrollContainer at 100%, including container rework.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Jun 12, 2023
1 parent 04619a9 commit b89f14a
Show file tree
Hide file tree
Showing 22 changed files with 534 additions and 253 deletions.
11 changes: 5 additions & 6 deletions cocoa/src/toga_cocoa/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,9 @@ def container(self, value):
self.container.native.addConstraint(self.height_constraint)

def update(self, x, y, width, height):
if self.container:
# print(f"UPDATE CONSTRAINTS {self.widget} in {self.container} {width}x{height}@{x},{y}")
self.left_constraint.constant = x
self.top_constraint.constant = y
# print(f"UPDATE CONSTRAINTS {self.widget} in {self.container} {width}x{height}@{x},{y}")
self.left_constraint.constant = x
self.top_constraint.constant = y

self.width_constraint.constant = width
self.height_constraint.constant = height
self.width_constraint.constant = width
self.height_constraint.constant = height
104 changes: 104 additions & 0 deletions cocoa/src/toga_cocoa/containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from rubicon.objc import objc_method

from .libs import (
NSLayoutAttributeBottom,
NSLayoutAttributeLeft,
NSLayoutAttributeRight,
NSLayoutAttributeTop,
NSLayoutConstraint,
NSLayoutRelationGreaterThanOrEqual,
NSView,
)


class TogaView(NSView):
@objc_method
def isFlipped(self) -> bool:
# Default Cocoa coordinate frame is around the wrong way.
return True


class BaseContainer:
def __init__(self, on_refresh=None):
self.on_refresh = on_refresh
# macOS always renders at 96dpi. Scaling is handled
# transparently at the level of the screen compositor.
self.dpi = 96
self.baseline_dpi = self.dpi

def refreshed(self):
if self.on_refresh:
self.on_refresh()


class MinimumContainer(BaseContainer):
def __init__(self):
"""A container for evaluating the minumum possible size for a layout"""
super().__init__()
self.width = 0
self.height = 0


class Container(BaseContainer):
def __init__(
self,
min_width=100,
min_height=100,
layout_native=None,
on_refresh=None,
):
"""
:param min_width: The minimum width to enforce on the container
:param min_height: The minimum height to enforce on the container
:param layout_native: The native widget that should be used to provide
size hints to the layout. This will usually be the container widget
itself; however, for widgets like ScrollContainer where the layout
needs to be computed based on a different size to what will be
rendered, the source of the size can be different.
:param on_refresh: The callback to be notified when this container's
layout is refreshed.
"""
super().__init__(on_refresh=on_refresh)
self.native = TogaView.alloc().init()
self.layout_native = self.native if layout_native is None else layout_native

# Enforce a minimum size based on the content size.
# This is enforcing the *minimum* size; the container might actually be
# bigger. If the window is resizable, using >= allows the window to
# be dragged larger; if not resizable, it enforces the smallest
# size that can be programmatically set on the window.
self._min_width_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # noqa: E501
self.native,
NSLayoutAttributeRight,
NSLayoutRelationGreaterThanOrEqual,
self.native,
NSLayoutAttributeLeft,
1.0,
min_width,
)
self.native.addConstraint(self._min_width_constraint)

self._min_height_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # noqa: E501
self.native,
NSLayoutAttributeBottom,
NSLayoutRelationGreaterThanOrEqual,
self.native,
NSLayoutAttributeTop,
1.0,
min_height,
)
self.native.addConstraint(self._min_height_constraint)

@property
def width(self):
return self.layout_native.frame.size.width

@property
def height(self):
return self.layout_native.frame.size.height

def set_min_width(self, width):
self._min_width_constraint.constant = width

def set_min_height(self, height):
self._min_height_constraint.constant = height
5 changes: 5 additions & 0 deletions cocoa/src/toga_cocoa/libs/foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
NSNotification = ObjCClass("NSNotification")
NSNotification.declare_property("object")

######################################################################
# NSRunLoop.h
NSRunLoop = ObjCClass("NSRunLoop")
NSRunLoop.declare_class_property("currentRunLoop")

######################################################################
# NSURL.h
NSURL = ObjCClass("NSURL")
Expand Down
14 changes: 3 additions & 11 deletions cocoa/src/toga_cocoa/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def container(self):
@container.setter
def container(self, container):
if self.container:
assert container is None, f"Widget {self} already has a container"
assert container is None, f"{self} already has a container"

# Existing container should be removed
self.constraints.container = None
Expand All @@ -53,11 +53,7 @@ def container(self, container):

@property
def viewport(self):
return self._viewport

@viewport.setter
def viewport(self, viewport):
self._viewport = viewport
return self._container

def get_enabled(self):
return self.native.isEnabled
Expand Down Expand Up @@ -106,11 +102,7 @@ def set_tab_index(self, tab_index):
# INTERFACE

def add_child(self, child):
if self.viewport:
# we are the top level NSView
child.container = self
else:
child.container = self.container
child.container = self.container

def insert_child(self, index, child):
self.add_child(child)
Expand Down
9 changes: 1 addition & 8 deletions cocoa/src/toga_cocoa/widgets/box.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
from travertino.size import at_least

from toga_cocoa.libs import NSView, objc_method
from toga_cocoa.containers import TogaView

from .base import Widget


class TogaView(NSView):
@objc_method
def isFlipped(self) -> bool:
# Default Cocoa coordinate frame is around the wrong way.
return True


class Box(Widget):
def create(self):
self.native = TogaView.alloc().init()
Expand Down
15 changes: 7 additions & 8 deletions cocoa/src/toga_cocoa/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from rubicon.objc import objc_method
from travertino.size import at_least

from toga_cocoa.libs import NSObject, NSTabView, NSTabViewItem, objc_method
from toga_cocoa.window import CocoaViewport
from toga_cocoa.containers import Container
from toga_cocoa.libs import NSObject, NSTabView, NSTabViewItem

from ..libs import objc_property
from .base import Widget
Expand Down Expand Up @@ -52,20 +53,18 @@ def add_content(self, index, text, widget):
text (str): The text for the option container
widget: The widget or widget tree that belongs to the text.
"""
widget.viewport = CocoaViewport(widget.native)

for child in widget.interface.children:
child._impl.container = widget
container = Container()
widget.container = container

item = NSTabViewItem.alloc().init()
item.label = text

# Turn the autoresizing mask on the widget widget
# into constraints. This makes the widget fill the
# available space inside the OptionContainer.
widget.native.translatesAutoresizingMaskIntoConstraints = True
container.native.translatesAutoresizingMaskIntoConstraints = True

item.view = widget.native
item.view = container.native
self.native.insertTabViewItem(item, atIndex=index)

def remove_content(self, index):
Expand Down
57 changes: 33 additions & 24 deletions cocoa/src/toga_cocoa/widgets/scrollcontainer.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from rubicon.objc import SEL, objc_method, objc_property
from travertino.size import at_least

from toga_cocoa.containers import Container
from toga_cocoa.libs import (
NSColor,
NSMakePoint,
NSMakeRect,
NSNoBorder,
NSNotificationCenter,
NSRunLoop,
NSScrollView,
NSScrollViewDidEndLiveScrollNotification,
NSScrollViewDidLiveScrollNotification,
)
from toga_cocoa.window import CocoaViewport

from .base import Widget

Expand All @@ -22,14 +23,6 @@ class TogaScrollView(NSScrollView):

@objc_method
def didScroll_(self, note) -> None:
# print(
# f"SCROLL frame={self.impl.native.frame.size.width}x{self.impl.native.frame.size.height}"
# f" @ {self.impl.native.frame.origin.x}x{self.impl.native.frame.origin.y}, "
# f"doc={self.impl.native.documentView.frame.size.width}x{self.impl.native.documentView.frame.size.height}"
# f" @ {self.impl.native.documentView.frame.origin.x}x{self.impl.native.documentView.frame.origin.y}, "
# f"content={self.impl.native.contentView.frame.size.width}x{self.impl.native.contentView.frame.size.height}"
# f" @ {self.impl.native.contentView.frame.origin.x}x{self.impl.native.contentView.frame.origin.y}, "
# )
self.interface.on_scroll(None)


Expand All @@ -43,8 +36,14 @@ def create(self):
self.native.borderType = NSNoBorder
self.native.backgroundColor = NSColor.windowBackgroundColor

self.native.translatesAutoresizingMaskIntoConstraints = False
self.native.autoresizesSubviews = True
# The container for the document bases its layout on the
# size of the content view. It can only exceed the size
# of the contentView if scrolling is enabled in that axis.
self.document_container = Container(
layout_native=self.native.contentView,
on_refresh=self.content_refreshed,
)
self.native.documentView = self.document_container.native

NSNotificationCenter.defaultCenter.addObserver(
self.native,
Expand All @@ -63,26 +62,36 @@ def create(self):
self.add_constraints()

def set_content(self, widget):
if widget:
self.native.documentView = widget._impl.native
widget._impl.viewport = CocoaViewport(self.native.documentView)
# If there's existing content, clear its container
if self.interface.content:
self.interface.content._impl.container = None

for child in widget.children:
child._impl.container = widget._impl
else:
self.native.documentView = None
# If there's new content, set the container of the content
if widget:
widget.container = self.document_container

def set_bounds(self, x, y, width, height):
# print("SET BOUNDS", x, y, width, height)
super().set_bounds(x, y, width, height)
# Restrict dimensions of content to dimensions of ScrollContainer
# along any non-scrolling directions. Set dimensions of content
# to its layout dimensions along the scrolling directions.

# Setting the bounds changes the constraints, but that doesn't mean
# the constraints have been fully applied. Let the NSRunLoop tick once
# to ensure constraints are applied.
NSRunLoop.currentRunLoop.runUntilDate(None)

# Now that we have an updated size for the ScrollContainer, re-evaluate
# the size of the document content
if self.interface._content:
self.interface._content.refresh()

def content_refreshed(self):
width = self.native.frame.size.width
height = self.native.frame.size.height

if self.interface.horizontal:
width = self.interface.content.layout.width
width = max(self.interface.content.layout.width, width)

if self.interface.vertical:
height = self.interface.content.layout.height
height = max(self.interface.content.layout.height, height)

self.native.documentView.frame = NSMakeRect(0, 0, width, height)

Expand Down
16 changes: 7 additions & 9 deletions cocoa/src/toga_cocoa/widgets/splitcontainer.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from rubicon.objc import objc_method, objc_property
from travertino.size import at_least

from toga_cocoa.libs import NSObject, NSSize, NSSplitView, objc_method
from toga_cocoa.window import CocoaViewport
from toga_cocoa.containers import Container
from toga_cocoa.libs import NSObject, NSSize, NSSplitView

from ..libs import objc_property
from .base import Widget


Expand Down Expand Up @@ -64,17 +64,15 @@ def create(self):

def add_content(self, position, widget, flex):
# TODO: add flex option to the implementation
widget.viewport = CocoaViewport(widget.native)

for child in widget.interface.children:
child._impl.container = widget
container = Container()
widget.container = container

# Turn the autoresizing mask on the widget into constraints.
# This makes the widget fill the available space inside the
# SplitContainer.
# FIXME Use Constraints to enforce min width and height of the widgets otherwise width of 0 is possible.
widget.native.translatesAutoresizingMaskIntoConstraints = True
self.native.addSubview(widget.native)
container.native.translatesAutoresizingMaskIntoConstraints = True
self.native.addSubview(container.native)

def set_direction(self, value):
self.native.vertical = value
Expand Down
Loading

0 comments on commit b89f14a

Please sign in to comment.