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

Set default background color for widgets on iOS #3009

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/767.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
On iOS, the default background color is now TRANSPARENT for Box, Canvas, ImageView, Label, ProgressBar, ScrollContainer and Slider widgets.
24 changes: 10 additions & 14 deletions iOS/src/toga_iOS/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,17 @@ def set_color(self, color):
# By default, color can't be changed
pass

def set_background_color(self, color):
# By default, background color can't be changed
pass

# TODO: check if it's safe to make this the default implementation.
def set_background_color_simple(self, value):
if value:
self.native.backgroundColor = native_color(value)
def set_background_color(self, color, is_native_color=False):
Copy link
Member

Choose a reason for hiding this comment

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

This isn't a good idea. The API either takes a native color, or it doesn't. And given that the only usage that I can see of is_native_color=True... uses native_color() on the invocation, it seems to be completely unnecessary.

Copy link
Contributor Author

@proneon267 proneon267 Dec 4, 2024

Choose a reason for hiding this comment

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

set_background_color() will receive a toga color from the core interface, but on the backend since we are working directly with the native UIColor, so any invocation of set_background_color() from the backend requires is_native_color=True. This is why in the previous review, I was specifying _default_background_color in the form of toga color instead of the native UIColor.

But since now we are directly specfying the _default_background_color in the form of native UIColor, we need the additional indicator is_native_color=True to prevent any incorrect interpretation while assigning the background color.

You were right.

if is_native_color:
self.native.backgroundColor = color
else:
try:
# systemBackgroundColor() was introduced in iOS 13
# We don't test on iOS 12, so mark the other branch as nocover
self.native.backgroundColor = UIColor.systemBackgroundColor()
except AttributeError: # pragma: no cover
self.native.backgroundColor = UIColor.whiteColor
default_background_color = getattr(self, "_default_background_color", None)
if default_background_color is None:
default_background_color = UIColor.systemBackgroundColor()
Copy link
Member

Choose a reason for hiding this comment

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

Why not just use _default_background_color as the default in the call to getattr?

Also - it feels like there's some ambiguity here about what "None" means in the context of default_background_color - is it "transparent", or "system background"? Does it make a difference if the property is explicitly set to None, or the property is not defined at all? (it probably should - because it gives you a way to differentiate between two interpretations of "reset" behaviour - and that interpretation should be documented here).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have changed it to use UIColor.systemBackgroundColor() as the default in the call to getattr.

On the backends, there are 3 different possible values for set_background_color():

None Default background color
TRANSPARENT Actual transparency
Color Actual color

But for Button, the historical behavior is "None = TRANSPARENT", which I have preserved in this PR.


self.native.backgroundColor = (
default_background_color if color is None else native_color(color)
)

# INTERFACE
def add_child(self, child):
Expand Down
6 changes: 2 additions & 4 deletions iOS/src/toga_iOS/widgets/box.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rubicon.objc import objc_property
from travertino.size import at_least

from toga_iOS.libs import UIView
from toga_iOS.libs import UIColor, UIView
from toga_iOS.widgets.base import Widget


Expand All @@ -19,12 +19,10 @@ def create(self):
self.native.interface = self.interface
self.native.impl = self

self._default_background_color = UIColor.clearColor
# Add the layout constraints
self.add_constraints()

def set_background_color(self, value):
self.set_background_color_simple(value)

def rehint(self):
self.interface.intrinsic.width = at_least(0)
self.interface.intrinsic.height = at_least(0)
9 changes: 5 additions & 4 deletions iOS/src/toga_iOS/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def create(self):

self._icon = None

self._default_background_color = None
# Add the layout constraints
self.add_constraints()

Expand Down Expand Up @@ -68,10 +69,10 @@ def set_color(self, color):
)

def set_background_color(self, color):
if color == TRANSPARENT or color is None:
self.native.backgroundColor = None
else:
self.native.backgroundColor = native_color(color)
super().set_background_color(
(None if color in {None, TRANSPARENT} else native_color(color)),
is_native_color=True,
)
Copy link
Member

Choose a reason for hiding this comment

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

AFAICT, this is a change in behavior. backgroundColor=None is a required call in this instance, and this new implementation doesn't provide that as an option.

Copy link
Contributor Author

@proneon267 proneon267 Dec 3, 2024

Choose a reason for hiding this comment

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

I've restored it. But it's worth noting that backgroundColor=None produces the same result as backgroundColor=UIColor.clearColor:

backgroundColor=None image
backgroundColor=UIColor.clearColor image

According to: https://developer.apple.com/documentation/uikit/uiview/1622591-backgroundcolor?language=objc:

Changes to this property can be animated. The default value is nil, which results in a transparent background color.

But according to: https://developer.apple.com/documentation/uikit/uibackgroundconfiguration/3600317-backgroundcolor

If the value is nil, the background color is the view’s tint color. Use clear for a transparent background with no color.

So, I have set the background color of widgets which should have transparent background by default to UIColor.clearColor. But for button, I've set the default background color to None.

Copy link
Member

Choose a reason for hiding this comment

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

Ok - I think that interpretation of "None = Transparent" is what I was remembering. Based on the examples you've presented here, it looks like you've preserved the historical behavior.


def set_font(self, font):
self.native.titleLabel.font = font._impl.native
Expand Down
6 changes: 4 additions & 2 deletions iOS/src/toga_iOS/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
from travertino.size import at_least

from toga.colors import BLACK, TRANSPARENT, color
from toga.colors import BLACK, TRANSPARENT, color as toga_color
from toga.constants import Baseline, FillRule
from toga_iOS.colors import native_color
from toga_iOS.images import nsdata_to_bytes
Expand Down Expand Up @@ -70,6 +70,8 @@ def create(self):
self.native.interface = self.interface
self.native.impl = self

self._default_background_color = UIColor.clearColor

# Add the layout constraints
self.add_constraints()

Expand Down Expand Up @@ -260,7 +262,7 @@ def _line_height(self, font):
def measure_text(self, text, font):
# We need at least a fill color to render, but that won't change the size.
sizes = [
self._render_string(line, font, fill_color=color(BLACK)).size()
self._render_string(line, font, fill_color=toga_color(BLACK)).size()
for line in text.splitlines()
]
return (
Expand Down
12 changes: 3 additions & 9 deletions iOS/src/toga_iOS/widgets/imageview.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from toga.colors import TRANSPARENT
from toga.widgets.imageview import rehint_imageview
from toga_iOS.colors import native_color
from toga_iOS.libs import UIImageView, UIViewContentMode
from toga_iOS.libs import UIColor, UIImageView, UIViewContentMode
from toga_iOS.widgets.base import Widget


Expand All @@ -15,13 +13,9 @@ def create(self):
self.native.setTranslatesAutoresizingMaskIntoConstraints_(False)
self.native.setAutoresizesSubviews_(False)

self.add_constraints()
self._default_background_color = UIColor.clearColor

def set_background_color(self, color):
if color == TRANSPARENT or color is None:
self.native.backgroundColor = native_color(TRANSPARENT)
else:
self.native.backgroundColor = native_color(color)
self.add_constraints()

def set_image(self, image):
if image:
Expand Down
10 changes: 3 additions & 7 deletions iOS/src/toga_iOS/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from rubicon.objc import CGRect, NSInteger, NSMakeRect, objc_method, send_super
from travertino.size import at_least

from toga.colors import TRANSPARENT
from toga_iOS.colors import native_color
from toga_iOS.libs import (
NSLineBreakByClipping,
NSTextAlignment,
UIColor,
UILabel,
)
from toga_iOS.widgets.base import Widget
Expand Down Expand Up @@ -38,6 +38,8 @@ def create(self):
# We shouldn't ever word wrap; if faced with that option, clip.
self.native.lineBreakMode = NSLineBreakByClipping

self._default_background_color = UIColor.clearColor

# Add the layout constraints
self.add_constraints()

Expand All @@ -47,12 +49,6 @@ def set_alignment(self, value):
def set_color(self, value):
self.native.textColor = native_color(value)

def set_background_color(self, color):
if color == TRANSPARENT or color is None:
self.native.backgroundColor = native_color(TRANSPARENT)
else:
self.native.backgroundColor = native_color(color)

def set_font(self, font):
self.native.font = font._impl.native

Expand Down
3 changes: 0 additions & 3 deletions iOS/src/toga_iOS/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,6 @@ def set_color(self, value):
self.native.textColor = color
self.placeholder_label.textColor = color

def set_background_color(self, color):
self.set_background_color_simple(color)

def set_alignment(self, value):
self.native.textAlignment = NSTextAlignment(value)

Expand Down
3 changes: 0 additions & 3 deletions iOS/src/toga_iOS/widgets/numberinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,6 @@ def set_font(self, font):
def set_color(self, color):
self.native.textColor = native_color(color)

def set_background_color(self, color):
self.set_background_color_simple(color)

def rehint(self):
# Height of a text input is known.
fitting_size = self.native.systemLayoutSizeFittingSize(CGSize(0, 0))
Expand Down
4 changes: 3 additions & 1 deletion iOS/src/toga_iOS/widgets/progressbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from travertino.size import at_least

from toga_iOS.libs import CGSize, UIProgressView, UIProgressViewStyle
from toga_iOS.libs import CGSize, UIColor, UIProgressView, UIProgressViewStyle
from toga_iOS.widgets.base import Widget

# Implementation notes
Expand Down Expand Up @@ -41,6 +41,8 @@ def create(self):
self.native = UIProgressView.alloc().initWithProgressViewStyle_(
UIProgressViewStyle.Default
)
self._default_background_color = UIColor.clearColor

self.add_constraints()

self._running = False
Expand Down
7 changes: 3 additions & 4 deletions iOS/src/toga_iOS/widgets/scrollcontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from travertino.size import at_least

from toga_iOS.container import Container
from toga_iOS.libs import UIScrollView
from toga_iOS.libs import UIColor, UIScrollView
from toga_iOS.widgets.base import Widget


Expand Down Expand Up @@ -31,6 +31,8 @@ def create(self):
self.native.impl = self
self.native.delegate = self.native

self._default_background_color = UIColor.clearColor

# UIScrollView doesn't have a native ability to disable a scrolling direction;
# it's handled by controlling the scrollable area.
self._allow_horizontal = True
Expand Down Expand Up @@ -68,9 +70,6 @@ def content_refreshed(self, container):

self.native.contentSize = NSMakeSize(width, height)

def set_background_color(self, value):
self.set_background_color_simple(value)

def rehint(self):
self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
self.interface.intrinsic.height = at_least(self.interface._MIN_HEIGHT)
Expand Down
3 changes: 0 additions & 3 deletions iOS/src/toga_iOS/widgets/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,6 @@ def set_alignment(self, value):
def set_color(self, color):
self.native.textColor = native_color(color)

def set_background_color(self, color):
self.set_background_color_simple(color)

def set_font(self, font):
self.native.font = font._impl.native

Expand Down
3 changes: 3 additions & 0 deletions iOS/src/toga_iOS/widgets/slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from travertino.size import at_least

from toga_iOS.libs import (
UIColor,
UIControlEventTouchCancel,
UIControlEventTouchDown,
UIControlEventTouchUpInside,
Expand Down Expand Up @@ -49,6 +50,8 @@ def create(self):
self.native.interface = self.interface
self.native.impl = self

self._default_background_color = UIColor.clearColor

# Dummy values used during initialization.
self.value = 0
self.min_value = 0
Expand Down
3 changes: 0 additions & 3 deletions iOS/src/toga_iOS/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,6 @@ def set_alignment(self, value):
def set_color(self, color):
self.native.textColor = native_color(color)

def set_background_color(self, color):
self.set_background_color_simple(color)

def set_font(self, font):
self.native.font = font._impl.native

Expand Down
7 changes: 5 additions & 2 deletions testbed/tests/widgets/test_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ async def test_press(widget, probe):

async def test_background_color_transparent(widget, probe):
"Buttons treat background transparency as a color reset."
del widget.style.background_color
original_background_color = probe.background_color

widget.style.background_color = TRANSPARENT
await probe.redraw("Button background color should be transparent")
assert_color(probe.background_color, None)
await probe.redraw("Button background color should be reset to the default color")
assert_color(probe.background_color, original_background_color)
Loading