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 1 commit
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.
15 changes: 14 additions & 1 deletion iOS/src/toga_iOS/colors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from toga.colors import TRANSPARENT
from ctypes import byref

from rubicon.objc import CGFloat

from toga.colors import TRANSPARENT, rgba
from toga_iOS.libs import UIColor

CACHE = {TRANSPARENT: UIColor.clearColor}
Expand All @@ -18,3 +22,12 @@ def native_color(c):
CACHE[c] = color

return color


def toga_color(c):
red = CGFloat()
green = CGFloat()
blue = CGFloat()
alpha = CGFloat()
c.getRed(byref(red), green=byref(green), blue=byref(blue), alpha=byref(alpha))
return rgba(red.value * 255, green.value * 255, blue.value * 255, alpha.value)
30 changes: 15 additions & 15 deletions iOS/src/toga_iOS/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import abstractmethod

from toga_iOS.colors import native_color
from toga_iOS.colors import native_color, toga_color
from toga_iOS.constraints import Constraints
from toga_iOS.libs import UIColor

Expand All @@ -13,6 +13,15 @@ def __init__(self, interface):
self._container = None
self.constraints = None
self.native = None

# Set default background color
try:
# systemBackgroundColor() was introduced in iOS 13
Copy link
Member

Choose a reason for hiding this comment

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

We no longer need this branch; as a result of the PEP 730 changes, iOS 13 is the minimum supported iOS version. The iOS platform docs should also be updated to reflect this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I've removed that branch.

# We don't test on iOS 12, so mark the other branch as nocover
self._default_background_color = toga_color(UIColor.systemBackgroundColor())
except AttributeError: # pragma: no cover
self._default_background_color = toga_color(UIColor.whiteColor)

self.create()
self.interface.style.reapply()

Expand Down Expand Up @@ -90,20 +99,11 @@ def set_color(self, color):
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)
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
self.native.backgroundColor = (
native_color(self._default_background_color)
Copy link
Member

Choose a reason for hiding this comment

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

This seems unnecessarily complex - doing a round trip from UIColor to toga.Color to UIColor just to get a (cached, and possibly stale) UIColor.systemBackgroundColor().

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 agree, I've modified it to work with the native colors directly.

if color is None
else native_color(color)
)

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

from toga.colors import TRANSPARENT
from toga_iOS.libs import UIView
from toga_iOS.widgets.base import Widget

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

self._default_background_color = TRANSPARENT

# 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)
7 changes: 3 additions & 4 deletions iOS/src/toga_iOS/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,9 @@ 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(
self._default_background_color if color in {None, TRANSPARENT} else color
)
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 named_color
Copy link
Member

Choose a reason for hiding this comment

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

Worth noting that this is a little misleading - color here isn't just named colors - it's a toga color that will accept any valid string.

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've renamed it to 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 = TRANSPARENT

# 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=named_color(BLACK)).size()
for line in text.splitlines()
]
return (
Expand Down
9 changes: 2 additions & 7 deletions iOS/src/toga_iOS/widgets/imageview.py
Original file line number Diff line number Diff line change
@@ -1,6 +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.widgets.base import Widget

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

self.add_constraints()
self._default_background_color = TRANSPARENT

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
8 changes: 2 additions & 6 deletions iOS/src/toga_iOS/widgets/label.py
Original file line number Diff line number Diff line change
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 = TRANSPARENT

# 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
3 changes: 3 additions & 0 deletions iOS/src/toga_iOS/widgets/progressbar.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.colors import TRANSPARENT
from toga_iOS.libs import CGSize, UIProgressView, UIProgressViewStyle
from toga_iOS.widgets.base import Widget

Expand Down Expand Up @@ -41,6 +42,8 @@ def create(self):
self.native = UIProgressView.alloc().initWithProgressViewStyle_(
UIProgressViewStyle.Default
)
self._default_background_color = TRANSPARENT

self.add_constraints()

self._running = False
Expand Down
6 changes: 3 additions & 3 deletions iOS/src/toga_iOS/widgets/scrollcontainer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rubicon.objc import SEL, NSMakePoint, NSMakeSize, objc_method, objc_property
from travertino.size import at_least

from toga.colors import TRANSPARENT
from toga_iOS.container import Container
from toga_iOS.libs import UIScrollView
from toga_iOS.widgets.base import Widget
Expand Down Expand Up @@ -31,6 +32,8 @@ def create(self):
self.native.impl = self
self.native.delegate = self.native

self._default_background_color = TRANSPARENT

# 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 +71,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
@@ -1,6 +1,7 @@
from rubicon.objc import SEL, CGSize, objc_method, objc_property
from travertino.size import at_least

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

self._default_background_color = TRANSPARENT

# 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