From bb522f4f68b601942502bc70cb68eb24325eeb24 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 1 Mar 2023 09:13:34 +0800 Subject: [PATCH 01/21] Update tests for label. --- testbed/tests/widgets/properties.py | 40 +++++++++++++++++++++++++++ testbed/tests/widgets/test_button.py | 41 +--------------------------- testbed/tests/widgets/test_label.py | 26 ++++++++---------- 3 files changed, 53 insertions(+), 54 deletions(-) diff --git a/testbed/tests/widgets/properties.py b/testbed/tests/widgets/properties.py index b9603ce65b..05b693e2bf 100644 --- a/testbed/tests/widgets/properties.py +++ b/testbed/tests/widgets/properties.py @@ -1,5 +1,6 @@ from toga.colors import RED, TRANSPARENT, color as named_color from toga.fonts import FANTASY +from toga.style.pack import COLUMN from ..assertions import assert_color from ..data import COLORS, TEXTS @@ -143,3 +144,42 @@ async def test_background_color_transparent(widget, probe): widget.style.background_color = TRANSPARENT await probe.redraw() assert_color(probe.background_color, TRANSPARENT) + + +async def test_flex_horizontal_widget_size(widget, probe): + "Check that a widget that is flexible in the horizontal axis resizes as expected" + # Container is initially a non-flex row box. + # Initial widget size is small (but non-zero), based on content size. + await probe.redraw() + assert 10 <= probe.width <= 150, f"Width ({probe.width}) not in range (10, 150)" + assert 10 <= probe.height <= 50, f"Height ({probe.height}) not in range (10, 50)" + + # Make the widget flexible; it will expand to fill horizontal space + widget.style.flex = 1 + + # widget has expanded width, but has the same height. + await probe.redraw() + assert probe.width > 350 + assert probe.height <= 50 + + # Make the container a flexible column box + # This will make the height the flexible axis + widget.parent.style.direction = COLUMN + + # Widget is still the width of the screen + # and the height hasn't changed + await probe.redraw() + assert probe.width > 350 + assert probe.height <= 50 + + # Set an explicit height and width + widget.style.width = 300 + widget.style.height = 200 + + # Widget is approximately the requested size + # (Definitely less than the window size) + await probe.redraw() + assert 290 <= probe.width <= 330, f"Width ({probe.width}) not in range (290, 330)" + assert ( + 190 <= probe.height <= 230 + ), f"Height ({probe.height}) not in range (190, 230)" diff --git a/testbed/tests/widgets/test_button.py b/testbed/tests/widgets/test_button.py index 16e81fd61b..8d69db4bbb 100644 --- a/testbed/tests/widgets/test_button.py +++ b/testbed/tests/widgets/test_button.py @@ -4,7 +4,6 @@ import toga from toga.colors import TRANSPARENT -from toga.style.pack import COLUMN from ..assertions import assert_color from ..data import TEXTS @@ -13,6 +12,7 @@ test_background_color_reset, test_color, test_color_reset, + test_flex_horizontal_widget_size, test_font, test_text_width_change, ) @@ -55,42 +55,3 @@ async def test_background_color_transparent(widget, probe): widget.style.background_color = TRANSPARENT await probe.redraw() assert_color(probe.background_color, None) - - -async def test_button_size(widget, probe): - "Check that the button resizes" - # Container is initially a non-flex row box. - # Initial button size is small (but non-zero), based on content size. - await probe.redraw() - assert 10 <= probe.width <= 150, f"Width ({probe.width}) not in range (10, 150)" - assert 10 <= probe.height <= 50, f"Height ({probe.height}) not in range (10, 50)" - - # Make the button flexible; it will expand to fill horizontal space - widget.style.flex = 1 - - # Button has expanded width, but has the same height. - await probe.redraw() - assert probe.width > 350 - assert probe.height <= 50 - - # Make the container a flexible column box - # This will make the height the flexible axis - widget.parent.style.direction = COLUMN - - # Button is still the width of the screen - # and the height hasn't changed - await probe.redraw() - assert probe.width > 350 - assert probe.height <= 50 - - # Set an explicit height and width - widget.style.width = 300 - widget.style.height = 200 - - # Button is approximately the requested size - # (Definitely less than the window size) - await probe.redraw() - assert 290 <= probe.width <= 330, f"Width ({probe.width}) not in range (290, 330)" - assert ( - 190 <= probe.height <= 230 - ), f"Height ({probe.height}) not in range (190, 230)" diff --git a/testbed/tests/widgets/test_label.py b/testbed/tests/widgets/test_label.py index 2120ae4c5b..2432452d7a 100644 --- a/testbed/tests/widgets/test_label.py +++ b/testbed/tests/widgets/test_label.py @@ -1,7 +1,6 @@ -from pytest import approx, fixture, mark +from pytest import approx, fixture import toga -from toga.platform import current_platform from toga.style.pack import CENTER, COLUMN, JUSTIFY, LEFT, LTR, RIGHT, RTL from .properties import ( # noqa: F401 @@ -10,6 +9,7 @@ test_background_color_transparent, test_color, test_color_reset, + test_flex_horizontal_widget_size, test_font, test_text, test_text_empty, @@ -19,32 +19,29 @@ @fixture async def widget(): - return toga.Label("hello") + return toga.Label("hello, this is a label") -test_text_width_change = mark.skipif( - current_platform in {"linux"}, - reason="resizes not applying correctly", -)(test_text_width_change) - - -# TODO: a `width` test, for any widget whose width depends on its text. -@mark.skip("changing text does not trigger a refresh (#1289)") async def test_multiline(widget, probe): + """If the label contains multiline text, it resizes""" + def make_lines(n): return "\n".join(f"line{i}" for i in range(n)) widget.text = make_lines(1) - # TODO: Android at least will need an `await` after each text change, to give the - # native layout a chance to update. + + await probe.redraw() line_height = probe.height widget.text = make_lines(2) + + await probe.redraw() assert probe.height == approx(line_height * 2, rel=0.1) line_spacing = probe.height - (line_height * 2) for n in range(3, 10): widget.text = make_lines(n) + await probe.redraw() assert probe.height == approx( (line_height * n) + (line_spacing * (n - 1)), rel=0.1, @@ -52,6 +49,7 @@ def make_lines(n): async def test_alignment(widget, probe): + """Labels honor alignment settings""" # Initial alignment is LEFT, initial direction is LTR widget.parent.style.direction = COLUMN await probe.redraw() @@ -63,7 +61,7 @@ async def test_alignment(widget, probe): probe.assert_alignment_equivalent(probe.alignment, alignment) # Clearing the alignment reverts to default alignment of LEFT - widget.style.text_align = None + del widget.style.text_align await probe.redraw() assert probe.alignment == LEFT From 7ff85486c3cb08311f72bb083d2f16dc387b4ad4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 1 Mar 2023 09:14:45 +0800 Subject: [PATCH 02/21] Add Android text alignment probe, and support for text justification. --- android/src/toga_android/libs/android/graphics.py | 2 ++ android/src/toga_android/libs/android/os.py | 3 +++ android/src/toga_android/widgets/label.py | 15 +++++++++++---- android/tests_backend/widgets/label.py | 7 ++++--- android/tests_backend/widgets/properties.py | 13 +++++++++++++ 5 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 android/src/toga_android/libs/android/os.py diff --git a/android/src/toga_android/libs/android/graphics.py b/android/src/toga_android/libs/android/graphics.py index 4b99c4b034..b7be31cc7e 100644 --- a/android/src/toga_android/libs/android/graphics.py +++ b/android/src/toga_android/libs/android/graphics.py @@ -13,3 +13,5 @@ PorterDuffColorFilter = JavaClass("android/graphics/PorterDuffColorFilter") Rect = JavaClass("android/graphics/Rect") Typeface = JavaClass("android/graphics/Typeface") + +LineBreaker = JavaClass("android/graphics/text/LineBreaker") diff --git a/android/src/toga_android/libs/android/os.py b/android/src/toga_android/libs/android/os.py new file mode 100644 index 0000000000..25f4a762b5 --- /dev/null +++ b/android/src/toga_android/libs/android/os.py @@ -0,0 +1,3 @@ +from rubicon.java import JavaClass + +Build = JavaClass("android/os/Build") diff --git a/android/src/toga_android/widgets/label.py b/android/src/toga_android/widgets/label.py index bc687c61c6..d3f5e0d81f 100644 --- a/android/src/toga_android/widgets/label.py +++ b/android/src/toga_android/widgets/label.py @@ -1,7 +1,10 @@ from travertino.size import at_least +from toga.constants import JUSTIFY from toga_android.colors import native_color +from ..libs.android.graphics import LineBreaker +from ..libs.android.os import Build from ..libs.android.view import Gravity, View__MeasureSpec from ..libs.android.widget import TextView from .base import Widget, align @@ -50,12 +53,16 @@ def rehint(self): self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) def set_alignment(self, value): - # Refuse to set alignment if create() has not been called. - if self.native is None: - return # Refuse to set alignment if widget has no container. # On Android, calling setGravity() when the widget has no LayoutParams # results in a NullPointerException. if not self.native.getLayoutParams(): return - self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) + + # Justified text wasn't added until Android O (SDK 26) + if value == JUSTIFY and Build.VERSION.SDK_INT >= Build.VERSION_CODES.O: + self.native.setJustificationMode(LineBreaker.JUSTIFICATION_MODE_INTER_WORD) + self.native.setGravity(Gravity.CENTER_VERTICAL) + else: + self.native.setJustificationMode(LineBreaker.JUSTIFICATION_MODE_NONE) + self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) diff --git a/android/tests_backend/widgets/label.py b/android/tests_backend/widgets/label.py index ef31e56954..1598da5c77 100644 --- a/android/tests_backend/widgets/label.py +++ b/android/tests_backend/widgets/label.py @@ -1,8 +1,7 @@ from java import jclass -from pytest import skip from .base import SimpleProbe -from .properties import toga_color, toga_font +from .properties import toga_alignment, toga_color, toga_font class LabelProbe(SimpleProbe): @@ -30,4 +29,6 @@ def font(self): @property def alignment(self): - skip("Alignment probe not implemented") + return toga_alignment( + self.native.getGravity(), self.native.getJustificationMode() + ) diff --git a/android/tests_backend/widgets/properties.py b/android/tests_backend/widgets/properties.py index 33042ada27..38990f4aa9 100644 --- a/android/tests_backend/widgets/properties.py +++ b/android/tests_backend/widgets/properties.py @@ -2,8 +2,11 @@ from travertino.fonts import Font from android.graphics import Color, Typeface +from android.graphics.text import LineBreaker from android.util import TypedValue +from android.view import Gravity from toga.colors import TRANSPARENT, rgba +from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT from toga.fonts import ( BOLD, ITALIC, @@ -57,3 +60,13 @@ def toga_font(typeface, size, resources): variant=NORMAL, weight=BOLD if typeface.isBold() else NORMAL, ) + + +def toga_alignment(alignment, justification_mode): + if justification_mode == LineBreaker.JUSTIFICATION_MODE_INTER_WORD: + return JUSTIFY + return { + Gravity.LEFT: LEFT, + Gravity.RIGHT: RIGHT, + Gravity.CENTER_HORIZONTAL: CENTER, + }[alignment & Gravity.HORIZONTAL_GRAVITY_MASK] From 4363a9899bab405035409443d382ec7096d857f8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 1 Mar 2023 09:58:26 +0800 Subject: [PATCH 03/21] Correct label implementation on iOS. --- iOS/src/toga_iOS/colors.py | 3 ++- iOS/src/toga_iOS/widgets/button.py | 1 - iOS/src/toga_iOS/widgets/label.py | 13 +++++++------ iOS/tests_backend/widgets/label.py | 6 +++--- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/iOS/src/toga_iOS/colors.py b/iOS/src/toga_iOS/colors.py index 7bf636d4d1..25d80f3360 100644 --- a/iOS/src/toga_iOS/colors.py +++ b/iOS/src/toga_iOS/colors.py @@ -1,6 +1,7 @@ +from toga.colors import TRANSPARENT from toga_iOS.libs import UIColor -CACHE = {} +CACHE = {TRANSPARENT: UIColor.clearColor} def native_color(c): diff --git a/iOS/src/toga_iOS/widgets/button.py b/iOS/src/toga_iOS/widgets/button.py index 7d88840819..9f4cfb896e 100644 --- a/iOS/src/toga_iOS/widgets/button.py +++ b/iOS/src/toga_iOS/widgets/button.py @@ -61,7 +61,6 @@ def set_color(self, color): ) def set_background_color(self, color): - # By default, background color can't be changed if color == TRANSPARENT or color is None: self.native.backgroundColor = None else: diff --git a/iOS/src/toga_iOS/widgets/label.py b/iOS/src/toga_iOS/widgets/label.py index 31703255d5..eb20a964c4 100644 --- a/iOS/src/toga_iOS/widgets/label.py +++ b/iOS/src/toga_iOS/widgets/label.py @@ -10,21 +10,21 @@ class Label(Widget): def create(self): self.native = UILabel.new() + # Word wrap the text inside the allocated space self.native.lineBreakMode = NSLineBreakByWordWrapping # Add the layout constraints self.add_constraints() def set_alignment(self, value): - if value: - self.native.textAlignment = NSTextAlignment(value) + self.native.textAlignment = NSTextAlignment(value) def set_color(self, value): self.native.textColor = native_color(value) def set_background_color(self, color): - if color is TRANSPARENT: - self.native.backgroundColor = None + if color == TRANSPARENT or color is None: + self.native.backgroundColor = native_color(TRANSPARENT) else: self.native.backgroundColor = native_color(color) @@ -33,10 +33,11 @@ def set_font(self, font): def set_text(self, value): self.native.text = self.interface.text + # Tell the text layout algorithm how many lines are allowed + self.native.numberOfLines = len(self.interface.text.split("\n")) def rehint(self): - # Width & height of a label is known and fixed. - # print("REHINT label", self, self.native.fittingSize().width, self.native.fittingSize().height) fitting_size = self.native.systemLayoutSizeFittingSize(CGSize(0, 0)) + # print("REHINT label", self, fitting_size.width, fitting_size.height) self.interface.intrinsic.width = at_least(fitting_size.width) self.interface.intrinsic.height = fitting_size.height diff --git a/iOS/tests_backend/widgets/label.py b/iOS/tests_backend/widgets/label.py index 51e1702576..2614e80f84 100644 --- a/iOS/tests_backend/widgets/label.py +++ b/iOS/tests_backend/widgets/label.py @@ -1,5 +1,5 @@ -from toga.color import TRANSPARENT -from toga_iOS.libs import UILabel +from toga.colors import TRANSPARENT +from toga_iOS.libs import UIColor, UILabel from .base import SimpleProbe from .properties import toga_alignment, toga_color, toga_font @@ -18,7 +18,7 @@ def color(self): @property def background_color(self): - if self.native.backgroundColor is None: + if self.native.backgroundColor == UIColor.clearColor: return TRANSPARENT else: return toga_color(self.native.backgroundColor) From af06298fe9cf2cc7f7e397a0cd018052c0b33d6f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 1 Mar 2023 10:25:21 +0800 Subject: [PATCH 04/21] Migrate label tests to pytest. --- core/src/toga/widgets/label.py | 15 +------- core/tests/test_deprecated_factory.py | 6 --- core/tests/widgets/test_label.py | 54 ++++++++++++++++++--------- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 1b038472dc..c44141f53f 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -1,5 +1,3 @@ -import warnings - from .base import Widget @@ -9,7 +7,6 @@ def __init__( text, id=None, style=None, - factory=None, # DEPRECATED! ): """A text label. @@ -19,20 +16,9 @@ def __init__( :param id: The ID for the widget. :param style: A style object. If no style is provided, a default style will be applied to the widget. - :param factory: *Deprecated* """ super().__init__(id=id, style=style) - ###################################################################### - # 2022-09: Backwards compatibility - ###################################################################### - # factory no longer used - if factory: - warnings.warn("The factory argument is no longer used.", DeprecationWarning) - ###################################################################### - # End backwards compatibility. - ###################################################################### - # Create a platform specific implementation of a Label self._impl = self.factory.Label(interface=self) @@ -49,5 +35,6 @@ def text(self, value): self._text = "" else: self._text = str(value) + self._impl.set_text(value) self.refresh() diff --git a/core/tests/test_deprecated_factory.py b/core/tests/test_deprecated_factory.py index 514f018a6c..16422502fa 100644 --- a/core/tests/test_deprecated_factory.py +++ b/core/tests/test_deprecated_factory.py @@ -118,12 +118,6 @@ def test_image_view_created(self): self.assertEqual(widget._impl.interface, widget) self.assertNotEqual(widget.factory, self.factory) - def test_label_created(self): - with self.assertWarns(DeprecationWarning): - widget = toga.Label("Test", factory=self.factory) - self.assertEqual(widget._impl.interface, widget) - self.assertNotEqual(widget.factory, self.factory) - def test_multiline_text_input_created(self): with self.assertWarns(DeprecationWarning): widget = toga.MultilineTextInput(factory=self.factory) diff --git a/core/tests/widgets/test_label.py b/core/tests/widgets/test_label.py index a6fd7404cc..3f04a4e16c 100644 --- a/core/tests/widgets/test_label.py +++ b/core/tests/widgets/test_label.py @@ -1,27 +1,45 @@ +import pytest + import toga -from toga_dummy.utils import TestCase +from toga_dummy.utils import ( + EventLog, + assert_action_performed, + attribute_value, +) + + +@pytest.fixture +def label(): + return toga.Label("Test Label") -class LabelTests(TestCase): - def setUp(self): - super().setUp() +def test_label_created(label): + "A label can be created." + # Round trip the impl/interface + assert label._impl.interface == label + assert_action_performed(label, "create Label") - self.text = "test text" - self.label = toga.Label(self.text) +@pytest.mark.parametrize( + "value, expected", + [ + ("New Text", "New Text"), + (12345, "12345"), + (None, ""), + ("Contains\nsome\nnewlines", "Contains\nsome\nnewlines"), + ], +) +def test_update_label_text(label, value, expected): + assert label.text == "Test Label" - def test_widget_created(self): - self.assertEqual(self.label._impl.interface, self.label) - self.assertActionPerformed(self.label, "create Label") + # Clear the event log + EventLog.reset() - def test_update_label_text(self): - new_text = "updated text" - self.label.text = new_text - self.assertEqual(self.label.text, new_text) - self.assertValueSet(self.label, "text", new_text) - self.assertActionPerformed(self.label, "refresh") + label.text = value + assert label.text == expected - self.label.text = None - self.assertEqual(self.label.text, "") + # test backend has the right value + assert attribute_value(label, "text") == expected - self.assertValueSet(self.label, "text", "") + # A rehint was performed + assert_action_performed(label, "refresh") From 71b523cfaf03dfc9fc4a7a83728b60d5b8e32fa8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 1 Mar 2023 10:32:24 +0800 Subject: [PATCH 05/21] Update documentation for Label. --- docs/reference/api/widgets/button.rst | 2 +- docs/reference/api/widgets/label.rst | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/reference/api/widgets/button.rst b/docs/reference/api/widgets/button.rst index c6ad5e28c3..b443d2ffd3 100644 --- a/docs/reference/api/widgets/button.rst +++ b/docs/reference/api/widgets/button.rst @@ -27,7 +27,7 @@ A button has a text label. A handler can be associated with button press events. # handle event pass - button = toga.Button('Click me', on_press=my_callback) + button = toga.Button("Click me", on_press=my_callback) Notes ----- diff --git a/docs/reference/api/widgets/label.rst b/docs/reference/api/widgets/label.rst index 5ee8be3440..779c48aec6 100644 --- a/docs/reference/api/widgets/label.rst +++ b/docs/reference/api/widgets/label.rst @@ -20,13 +20,17 @@ Usage import toga - label = toga.Label('Hello world') + label = toga.Label("Hello world") Notes ----- +* Any object can be provided as the text for the label. If the object is + ``None``, this will be converted into the empty string; any other non-string + object will be automatically converted to a string using ``str()``. + * Winforms does not support an alignment value of ``JUSTIFIED``. If this - alignment value is used, the label will default left alignment. + alignment value is used, the label will default to left alignment. Reference --------- From 35f3f6c055eaf5951f50dead03a12097963c3c22 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 1 Mar 2023 10:32:40 +0800 Subject: [PATCH 06/21] Add changenote for label bugfix. --- changes/1289.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/1289.bugfix.rst diff --git a/changes/1289.bugfix.rst b/changes/1289.bugfix.rst new file mode 100644 index 0000000000..ae5e074a6a --- /dev/null +++ b/changes/1289.bugfix.rst @@ -0,0 +1 @@ +Label widgets now correctly resize after a change in label text. From 0a833509d5560100ec48abc5d6d7a1cf262528e9 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 1 Mar 2023 10:35:23 +0800 Subject: [PATCH 07/21] Add changenote for Label audit. --- changes/1799.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/1799.feature.rst diff --git a/changes/1799.feature.rst b/changes/1799.feature.rst new file mode 100644 index 0000000000..9747d91a74 --- /dev/null +++ b/changes/1799.feature.rst @@ -0,0 +1 @@ +The Label widget now has 100% test coverage, and complete API documentation. From 4238cb61cee0ff68b4eb32fc3165b3f9c06dc8d1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 1 Mar 2023 10:55:39 +0800 Subject: [PATCH 08/21] Add GTK alignment probe. --- gtk/src/toga_gtk/widgets/button.py | 1 - gtk/src/toga_gtk/widgets/label.py | 7 +------ gtk/tests_backend/widgets/label.py | 9 ++++++--- gtk/tests_backend/widgets/properties.py | 21 ++++++++++++++------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/gtk/src/toga_gtk/widgets/button.py b/gtk/src/toga_gtk/widgets/button.py index 7571ad3879..557d84536a 100644 --- a/gtk/src/toga_gtk/widgets/button.py +++ b/gtk/src/toga_gtk/widgets/button.py @@ -13,7 +13,6 @@ def create(self): self.native.get_style_context().add_class("toga") self.native.interface = self.interface - self.native.connect("show", lambda event: self.refresh()) self.native.connect("clicked", self.gtk_on_press) def get_text(self): diff --git a/gtk/src/toga_gtk/widgets/label.py b/gtk/src/toga_gtk/widgets/label.py index f4fb69a614..aa1c04357c 100644 --- a/gtk/src/toga_gtk/widgets/label.py +++ b/gtk/src/toga_gtk/widgets/label.py @@ -9,12 +9,9 @@ def create(self): self.native = Gtk.Label() self.native.set_name(f"toga-{self.interface.id}") self.native.get_style_context().add_class("toga") - - self.native.set_line_wrap(False) - self.native.interface = self.interface - self.native.connect("show", lambda event: self.refresh()) + self.native.set_line_wrap(False) def set_alignment(self, value): xalign, justify = gtk_alignment(value) @@ -25,8 +22,6 @@ def set_alignment(self, value): ) # Aligns multiple lines relative to each other. def set_text(self, value): - # FIXME after setting the label the label jumps to the top left - # corner and only jumps back at its place after resizing the window. self.native.set_text(self.interface._text) def rehint(self): diff --git a/gtk/tests_backend/widgets/label.py b/gtk/tests_backend/widgets/label.py index 0f4df67dcd..5965ef3705 100644 --- a/gtk/tests_backend/widgets/label.py +++ b/gtk/tests_backend/widgets/label.py @@ -1,8 +1,7 @@ -from pytest import skip - from toga_gtk.libs import Gtk from .base import SimpleProbe +from .properties import toga_alignment class LabelProbe(SimpleProbe): @@ -14,4 +13,8 @@ def text(self): @property def alignment(self): - skip("alignment probe not implemented") + return toga_alignment( + self.native.get_xalign(), + self.native.get_yalign(), + self.native.get_justify(), + ) diff --git a/gtk/tests_backend/widgets/properties.py b/gtk/tests_backend/widgets/properties.py index bc0ed1ef40..b51ac31756 100644 --- a/gtk/tests_backend/widgets/properties.py +++ b/gtk/tests_backend/widgets/properties.py @@ -1,3 +1,4 @@ +import pytest from travertino.fonts import Font from toga.colors import TRANSPARENT, rgba @@ -30,10 +31,16 @@ def toga_font(font): ) -def toga_alignment(alignment): - return { - (0.0, Gtk.Justification.LEFT): LEFT, - (1.0, Gtk.Justification.RIGHT): RIGHT, - (0.5, Gtk.Justification.CENTER): CENTER, - (0.0, Gtk.Justification.FILL): JUSTIFY, - }[alignment] +def toga_alignment(xalign, yalign, justify): + if yalign != 0.5: + pytest.fail("Y-alignment should be 0.5") + + try: + return { + (0.0, Gtk.Justification.LEFT): LEFT, + (1.0, Gtk.Justification.RIGHT): RIGHT, + (0.5, Gtk.Justification.CENTER): CENTER, + (0.0, Gtk.Justification.FILL): JUSTIFY, + }[(xalign, justify)] + except KeyError: + pytest.fail(f"Can't interpret GTK x alignment {xalign} with justify {justify}") From d0b3da089552091f17979290160443ed28c5cd18 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 1 Mar 2023 10:57:25 +0800 Subject: [PATCH 09/21] Update widget support chart. --- docs/reference/data/widgets_by_platform.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index d0987ffc59..677cb6e794 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -9,7 +9,7 @@ DatePicker,General Widget,:class:`~toga.widgets.datepicker.DatePicker`,An input DetailedList,General Widget,:class:`~toga.widgets.detailedlist.DetailedList`,A list of complex content,|b|,|b|,,|b|,|b|, Divider,General Widget,:class:`~toga.widgets.divider.Divider`,A horizontal or vertical line,|b|,|b|,|b|,,, ImageView,General Widget,:class:`~toga.widgets.imageview.ImageView`,Image Viewer,|b|,|b|,|b|,|b|,|b|, -Label,General Widget,:class:`~toga.widgets.label.Label`,Text label,|b|,|b|,|b|,|b|,|b|,|b| +Label,General Widget,:class:`~toga.widgets.label.Label`,Text label,|y|,|y|,|y|,|y|,|y|,|b| MultilineTextInput,General Widget,:class:`~toga.widgets.multilinetextinput.MultilineTextInput`,Multi-line Text Input field,|b|,|b|,|b|,|b|,|b|, NumberInput,General Widget,:class:`~toga.widgets.numberinput.NumberInput`,Number Input field,|b|,|b|,|b|,|b|,|b|, PasswordInput,General Widget,:class:`~toga.widgets.passwordinput.PasswordInput`,A text input that hides it’s input,|b|,|b|,|b|,|b|,|b|, From 4691de80c2408b0068254b0dd3fc9cfc99815634 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 21 Mar 2023 17:47:23 +0000 Subject: [PATCH 10/21] Tighten up Android alignment tests --- android/src/toga_android/widgets/label.py | 3 +-- android/tests_backend/widgets/base.py | 4 ++-- android/tests_backend/widgets/properties.py | 21 ++++++++++++++------- cocoa/tests_backend/widgets/base.py | 4 ++-- gtk/tests_backend/widgets/base.py | 4 ++-- iOS/tests_backend/widgets/base.py | 4 ++-- testbed/tests/widgets/test_label.py | 10 +++++----- winforms/tests_backend/widgets/base.py | 3 ++- 8 files changed, 30 insertions(+), 23 deletions(-) diff --git a/android/src/toga_android/widgets/label.py b/android/src/toga_android/widgets/label.py index 67e6293917..ff70b35de6 100644 --- a/android/src/toga_android/widgets/label.py +++ b/android/src/toga_android/widgets/label.py @@ -66,7 +66,6 @@ def set_alignment(self, value): # Justified text wasn't added until Android O (SDK 26) if value == JUSTIFY and Build.VERSION.SDK_INT >= Build.VERSION_CODES.O: self.native.setJustificationMode(LineBreaker.JUSTIFICATION_MODE_INTER_WORD) - self.native.setGravity(Gravity.CENTER_VERTICAL) else: self.native.setJustificationMode(LineBreaker.JUSTIFICATION_MODE_NONE) - self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) + self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) diff --git a/android/tests_backend/widgets/base.py b/android/tests_backend/widgets/base.py index 64a7cd17b2..8a6c5dd0f1 100644 --- a/android/tests_backend/widgets/base.py +++ b/android/tests_backend/widgets/base.py @@ -48,8 +48,8 @@ def assert_container(self, container): else: raise AssertionError(f"cannot find {self.native} in {container_native}") - def assert_alignment_equivalent(self, actual, expected): - assert actual == expected + def assert_alignment(self, expected): + assert self.alignment == expected def assert_font_family(self, expected): actual = self.font.family diff --git a/android/tests_backend/widgets/properties.py b/android/tests_backend/widgets/properties.py index bb0af66e8b..ae497c3fab 100644 --- a/android/tests_backend/widgets/properties.py +++ b/android/tests_backend/widgets/properties.py @@ -67,11 +67,18 @@ def toga_font(typeface, size, resources): ) -def toga_alignment(alignment, justification_mode): - if justification_mode == LineBreaker.JUSTIFICATION_MODE_INTER_WORD: +def toga_alignment(gravity, justification_mode): + horizontal_gravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK + if ( + justification_mode == LineBreaker.JUSTIFICATION_MODE_INTER_WORD + and horizontal_gravity == Gravity.LEFT + ): return JUSTIFY - return { - Gravity.LEFT: LEFT, - Gravity.RIGHT: RIGHT, - Gravity.CENTER_HORIZONTAL: CENTER, - }[alignment & Gravity.HORIZONTAL_GRAVITY_MASK] + elif justification_mode == LineBreaker.JUSTIFICATION_MODE_NONE: + return { + Gravity.LEFT: LEFT, + Gravity.RIGHT: RIGHT, + Gravity.CENTER_HORIZONTAL: CENTER, + }[horizontal_gravity] + else: + raise ValueError(f"unknown combination: {gravity=}, {justification_mode=}") diff --git a/cocoa/tests_backend/widgets/base.py b/cocoa/tests_backend/widgets/base.py index b1a83c4462..bad58265cf 100644 --- a/cocoa/tests_backend/widgets/base.py +++ b/cocoa/tests_backend/widgets/base.py @@ -17,8 +17,8 @@ def assert_container(self, container): else: raise ValueError(f"cannot find {self.native} in {container_native}") - def assert_alignment_equivalent(self, actual, expected): - assert actual == expected + def assert_alignment(self, expected): + assert self.alignment == expected def assert_font_family(self, expected): assert self.font.family == { diff --git a/gtk/tests_backend/widgets/base.py b/gtk/tests_backend/widgets/base.py index 9fdaad89c6..d0fee7ffc4 100644 --- a/gtk/tests_backend/widgets/base.py +++ b/gtk/tests_backend/widgets/base.py @@ -26,8 +26,8 @@ def assert_container(self, container): else: raise ValueError(f"cannot find {self.native} in {container_native}") - def assert_alignment_equivalent(self, actual, expected): - assert actual == expected + def assert_alignment(self, expected): + assert self.alignment == expected def assert_font_family(self, expected): assert self.font.family == expected diff --git a/iOS/tests_backend/widgets/base.py b/iOS/tests_backend/widgets/base.py index 3f1afabca3..db2ce267a1 100644 --- a/iOS/tests_backend/widgets/base.py +++ b/iOS/tests_backend/widgets/base.py @@ -45,8 +45,8 @@ def assert_container(self, container): else: raise ValueError(f"cannot find {self.native} in {container_native}") - def assert_alignment_equivalent(self, actual, expected): - assert actual == expected + def assert_alignment(self, expected): + assert self.alignment == expected def assert_font_family(self, expected): assert self.font.family == { diff --git a/testbed/tests/widgets/test_label.py b/testbed/tests/widgets/test_label.py index 621ebc6789..d08790aca6 100644 --- a/testbed/tests/widgets/test_label.py +++ b/testbed/tests/widgets/test_label.py @@ -53,24 +53,24 @@ async def test_alignment(widget, probe): # Initial alignment is LEFT, initial direction is LTR widget.parent.style.direction = COLUMN await probe.redraw() - assert probe.alignment == LEFT + probe.assert_alignment(LEFT) for alignment in [RIGHT, CENTER, JUSTIFY]: widget.style.text_align = alignment await probe.redraw() - probe.assert_alignment_equivalent(probe.alignment, alignment) + probe.assert_alignment(alignment) # Clearing the alignment reverts to default alignment of LEFT del widget.style.text_align await probe.redraw() - assert probe.alignment == LEFT + probe.assert_alignment(LEFT) # If text direction is RTL, default alignment is RIGHT widget.style.text_direction = RTL await probe.redraw() - assert probe.alignment == RIGHT + probe.assert_alignment(RIGHT) # If text direction is expliclty LTR, default alignment is LEFT widget.style.text_direction = LTR await probe.redraw() - assert probe.alignment == LEFT + probe.assert_alignment(LEFT) diff --git a/winforms/tests_backend/widgets/base.py b/winforms/tests_backend/widgets/base.py index 84e254c57c..176f7195ce 100644 --- a/winforms/tests_backend/widgets/base.py +++ b/winforms/tests_backend/widgets/base.py @@ -24,8 +24,9 @@ def assert_container(self, container): else: raise ValueError(f"cannot find {self.native} in {container_native}") - def assert_alignment_equivalent(self, actual, expected): + def assert_alignment(self, expected): # Winforms doesn't have a "Justified" alignment; it falls back to LEFT + actual = self.alignment if expected == JUSTIFY: assert actual == LEFT else: From 1b70eeec0d1d5f4d4762b8566ab01641737b03fa Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 21 Mar 2023 17:55:10 +0000 Subject: [PATCH 11/21] Remove change note for issue which may not be completely fixed yet --- changes/1289.bugfix.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changes/1289.bugfix.rst diff --git a/changes/1289.bugfix.rst b/changes/1289.bugfix.rst deleted file mode 100644 index ae5e074a6a..0000000000 --- a/changes/1289.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Label widgets now correctly resize after a change in label text. From cdedf68a5a02becdacd6091d033ccbfc4c59ada7 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 21 Mar 2023 18:58:11 +0000 Subject: [PATCH 12/21] Remove unnecessary skips in base probes --- android/tests_backend/widgets/base.py | 13 ------------- gtk/tests_backend/widgets/base.py | 10 ---------- 2 files changed, 23 deletions(-) diff --git a/android/tests_backend/widgets/base.py b/android/tests_backend/widgets/base.py index 8a6c5dd0f1..f0bfab91fa 100644 --- a/android/tests_backend/widgets/base.py +++ b/android/tests_backend/widgets/base.py @@ -1,7 +1,6 @@ import asyncio from java import dynamic_proxy -from pytest import skip from android.view import ViewTreeObserver from toga.fonts import SYSTEM @@ -71,18 +70,6 @@ async def redraw(self): def enabled(self): return self.native.isEnabled() - @property - def background_color(self): - skip("not implemented: background_color") - - @property - def color(self): - skip("not implemented: color") - - @property - def hidden(self): - skip("not implemented: hidden") - @property def width(self): # Return the value in DP diff --git a/gtk/tests_backend/widgets/base.py b/gtk/tests_backend/widgets/base.py index d0fee7ffc4..f9004af7e6 100644 --- a/gtk/tests_backend/widgets/base.py +++ b/gtk/tests_backend/widgets/base.py @@ -1,7 +1,5 @@ import asyncio -from pytest import skip - from toga_gtk.libs import Gtk from .properties import toga_color, toga_font @@ -42,14 +40,6 @@ async def redraw(self): if self.widget.app.run_slow: await asyncio.sleep(1) - @property - def enabled(self): - skip("enabled probe not implemented") - - @property - def hidden(self): - skip("hidden probe not implemented") - @property def width(self): return self.native.get_allocation().width From 0f798ccbc0ab72047fe9819466b2ea0866940b96 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 21 Mar 2023 20:39:46 +0000 Subject: [PATCH 13/21] iOS: round up size in Label.rehint to prevent lines being omitted --- changes/1501.bugfix.rst | 1 + iOS/src/toga_iOS/widgets/label.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changes/1501.bugfix.rst diff --git a/changes/1501.bugfix.rst b/changes/1501.bugfix.rst new file mode 100644 index 0000000000..b9c429e10d --- /dev/null +++ b/changes/1501.bugfix.rst @@ -0,0 +1 @@ +iOS now supports newlines in Labels. diff --git a/iOS/src/toga_iOS/widgets/label.py b/iOS/src/toga_iOS/widgets/label.py index eb20a964c4..7f64c38778 100644 --- a/iOS/src/toga_iOS/widgets/label.py +++ b/iOS/src/toga_iOS/widgets/label.py @@ -1,3 +1,5 @@ +from math import ceil + from rubicon.objc import CGSize from travertino.size import at_least @@ -39,5 +41,5 @@ def set_text(self, value): def rehint(self): fitting_size = self.native.systemLayoutSizeFittingSize(CGSize(0, 0)) # print("REHINT label", self, fitting_size.width, fitting_size.height) - self.interface.intrinsic.width = at_least(fitting_size.width) - self.interface.intrinsic.height = fitting_size.height + self.interface.intrinsic.width = at_least(ceil(fitting_size.width)) + self.interface.intrinsic.height = ceil(fitting_size.height) From 025259db3c6b90efa387c94ed5dfb3a74fbdbec1 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 21 Mar 2023 21:01:03 +0000 Subject: [PATCH 14/21] Add empty label height test --- testbed/tests/widgets/test_label.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/testbed/tests/widgets/test_label.py b/testbed/tests/widgets/test_label.py index d08790aca6..9eaf46c7a7 100644 --- a/testbed/tests/widgets/test_label.py +++ b/testbed/tests/widgets/test_label.py @@ -23,23 +23,26 @@ async def widget(): async def test_multiline(widget, probe): - """If the label contains multiline text, it resizes""" + """If the label contains multiline text, it resizes vertically""" def make_lines(n): return "\n".join(f"line{i}" for i in range(n)) widget.text = make_lines(1) - await probe.redraw() line_height = probe.height - widget.text = make_lines(2) + # Empty text should not cause the widget to collapse. + widget.text = "" + await probe.redraw() + assert probe.height == line_height + widget.text = make_lines(2) await probe.redraw() assert probe.height == approx(line_height * 2, rel=0.1) line_spacing = probe.height - (line_height * 2) - for n in range(3, 10): + for n in range(3, 6): widget.text = make_lines(n) await probe.redraw() assert probe.height == approx( From f2fb171cff437b8706a8dcb0665e224295309912 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 22 Mar 2023 09:51:25 +0800 Subject: [PATCH 15/21] Remove the use of shadow handling in label. --- android/src/toga_android/widgets/label.py | 3 +++ cocoa/src/toga_cocoa/widgets/label.py | 5 ++++- core/src/toga/widgets/label.py | 18 ++++++++++++------ core/tests/widgets/test_label.py | 1 + dummy/src/toga_dummy/widgets/label.py | 5 ++++- gtk/src/toga_gtk/widgets/label.py | 5 ++++- iOS/src/toga_iOS/widgets/label.py | 5 ++++- web/src/toga_web/widgets/label.py | 3 +++ winforms/src/toga_winforms/widgets/label.py | 5 ++++- 9 files changed, 39 insertions(+), 11 deletions(-) diff --git a/android/src/toga_android/widgets/label.py b/android/src/toga_android/widgets/label.py index ff70b35de6..60d523bb23 100644 --- a/android/src/toga_android/widgets/label.py +++ b/android/src/toga_android/widgets/label.py @@ -31,6 +31,9 @@ def create(self): self.native = TextView(self._native_activity) self.cache_textview_defaults() + def get_text(self): + return self.native.getText() + def set_text(self, value): self.native.setText(value) diff --git a/cocoa/src/toga_cocoa/widgets/label.py b/cocoa/src/toga_cocoa/widgets/label.py index 746a13afe2..a686bc259b 100644 --- a/cocoa/src/toga_cocoa/widgets/label.py +++ b/cocoa/src/toga_cocoa/widgets/label.py @@ -26,8 +26,11 @@ def set_color(self, value): def set_font(self, font): self.native.font = font._impl.native + def get_text(self): + return self.native.stringValue + def set_text(self, value): - self.native.stringValue = self.interface._text + self.native.stringValue = value def rehint(self): # Width & height of a label is known and fixed. diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index c44141f53f..77f86dc95a 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -26,15 +26,21 @@ def __init__( @property def text(self): - """The text displayed by the label.""" - return self._text + """The text displayed by the label. + + ``None``, and the Unicode codepoint U+200B (ZERO WIDTH SPACE), will be + interpreted and returned as an empty string. Any other object will be + converted to a string using ``str()``. + + """ + return self._impl.get_text() @text.setter def text(self, value): - if value is None: - self._text = "" + if value is None or value == "\u200B": + text = "" else: - self._text = str(value) + text = str(value) - self._impl.set_text(value) + self._impl.set_text(text) self.refresh() diff --git a/core/tests/widgets/test_label.py b/core/tests/widgets/test_label.py index 3f04a4e16c..81e3eedf02 100644 --- a/core/tests/widgets/test_label.py +++ b/core/tests/widgets/test_label.py @@ -26,6 +26,7 @@ def test_label_created(label): ("New Text", "New Text"), (12345, "12345"), (None, ""), + ("\u200B", ""), ("Contains\nsome\nnewlines", "Contains\nsome\nnewlines"), ], ) diff --git a/dummy/src/toga_dummy/widgets/label.py b/dummy/src/toga_dummy/widgets/label.py index 9fb28f1972..1c654b249e 100644 --- a/dummy/src/toga_dummy/widgets/label.py +++ b/dummy/src/toga_dummy/widgets/label.py @@ -8,5 +8,8 @@ def create(self): def set_alignment(self, value): self._set_value("alignment", value) + def get_text(self): + return self._get_value("text") + def set_text(self, value): - self._set_value("text", self.interface._text) + self._set_value("text", value) diff --git a/gtk/src/toga_gtk/widgets/label.py b/gtk/src/toga_gtk/widgets/label.py index aa1c04357c..d70ae4f80a 100644 --- a/gtk/src/toga_gtk/widgets/label.py +++ b/gtk/src/toga_gtk/widgets/label.py @@ -21,8 +21,11 @@ def set_alignment(self, value): justify ) # Aligns multiple lines relative to each other. + def get_text(self): + return self.native.get_text() + def set_text(self, value): - self.native.set_text(self.interface._text) + self.native.set_text(value) def rehint(self): # print("REHINT", self, diff --git a/iOS/src/toga_iOS/widgets/label.py b/iOS/src/toga_iOS/widgets/label.py index 7f64c38778..8226a128f7 100644 --- a/iOS/src/toga_iOS/widgets/label.py +++ b/iOS/src/toga_iOS/widgets/label.py @@ -33,8 +33,11 @@ def set_background_color(self, color): def set_font(self, font): self.native.font = font._impl.native + def get_text(self): + return self.native.text + def set_text(self, value): - self.native.text = self.interface.text + self.native.text = value # Tell the text layout algorithm how many lines are allowed self.native.numberOfLines = len(self.interface.text.split("\n")) diff --git a/web/src/toga_web/widgets/label.py b/web/src/toga_web/widgets/label.py index 75d2ad9b74..1f81d0b333 100644 --- a/web/src/toga_web/widgets/label.py +++ b/web/src/toga_web/widgets/label.py @@ -5,6 +5,9 @@ class Label(Widget): def create(self): self.native = self._create_native_widget("span") + def get_text(self): + return self.native.innerHTML + def set_text(self, value): self.native.innerHTML = value diff --git a/winforms/src/toga_winforms/widgets/label.py b/winforms/src/toga_winforms/widgets/label.py index 85aafee14b..0dd98d7f1e 100644 --- a/winforms/src/toga_winforms/widgets/label.py +++ b/winforms/src/toga_winforms/widgets/label.py @@ -13,8 +13,11 @@ def create(self): def set_alignment(self, value): self.native.TextAlign = TextAlignment(value) + def get_text(self): + return self.native.Text + def set_text(self, value): - self.native.Text = self.interface._text + self.native.Text = value def set_font(self, font): self.native.Font = font._impl.native From 28d370408fb58029f0726e80aff180c99173197b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 22 Mar 2023 10:20:13 +0800 Subject: [PATCH 16/21] Add test resilience against changes in text length. --- testbed/tests/data.py | 15 ++++++++++++++- testbed/tests/widgets/test_label.py | 10 +++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/testbed/tests/data.py b/testbed/tests/data.py index de1dcbbb12..0af222dfd1 100644 --- a/testbed/tests/data.py +++ b/testbed/tests/data.py @@ -1,6 +1,19 @@ from toga.colors import rgba -TEXTS = ["", " ", "a", "ab", "abc", "hello world", "hello\nworld", "你好, wørłd!"] +# The text examples must both increase and decrease in size between examples to +# ensure that reducing the size of a label doesn't prevent future labels from +# increasing in size. +TEXTS = [ + "example", + "", + "a", + " ", + "ab", + "abc", + "hello world", + "hello\nworld", + "你好, wørłd!", +] COLORS = [ diff --git a/testbed/tests/widgets/test_label.py b/testbed/tests/widgets/test_label.py index 9eaf46c7a7..7d43b21ae5 100644 --- a/testbed/tests/widgets/test_label.py +++ b/testbed/tests/widgets/test_label.py @@ -26,16 +26,21 @@ async def test_multiline(widget, probe): """If the label contains multiline text, it resizes vertically""" def make_lines(n): - return "\n".join(f"line{i}" for i in range(n)) + return "\n".join(f"This is line {i}" for i in range(n)) widget.text = make_lines(1) await probe.redraw() line_height = probe.height + # Label should have a signficant width. + assert probe.width > 50 + # Empty text should not cause the widget to collapse. widget.text = "" await probe.redraw() assert probe.height == line_height + # Label should have almost 0 width + assert probe.width < 10 widget.text = make_lines(2) await probe.redraw() @@ -45,10 +50,13 @@ def make_lines(n): for n in range(3, 6): widget.text = make_lines(n) await probe.redraw() + # Label height should reflect the number of lines assert probe.height == approx( (line_height * n) + (line_spacing * (n - 1)), rel=0.1, ) + # Label should have a signficant width. + assert probe.width > 50 async def test_alignment(widget, probe): From 572b07b6eaef4fecb93a84684272379a1824f992 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 22 Mar 2023 11:20:34 +0800 Subject: [PATCH 17/21] Correct handling of empty labels on iOS. --- cocoa/src/toga_cocoa/widgets/label.py | 2 +- iOS/src/toga_iOS/widgets/label.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cocoa/src/toga_cocoa/widgets/label.py b/cocoa/src/toga_cocoa/widgets/label.py index a686bc259b..a0e0461e67 100644 --- a/cocoa/src/toga_cocoa/widgets/label.py +++ b/cocoa/src/toga_cocoa/widgets/label.py @@ -27,7 +27,7 @@ def set_font(self, font): self.native.font = font._impl.native def get_text(self): - return self.native.stringValue + return str(self.native.stringValue) def set_text(self, value): self.native.stringValue = value diff --git a/iOS/src/toga_iOS/widgets/label.py b/iOS/src/toga_iOS/widgets/label.py index 8226a128f7..1ca1168764 100644 --- a/iOS/src/toga_iOS/widgets/label.py +++ b/iOS/src/toga_iOS/widgets/label.py @@ -34,15 +34,17 @@ def set_font(self, font): self.native.font = font._impl.native def get_text(self): - return self.native.text + return str(self.native.text) def set_text(self, value): + if value == "": + value = "\u200B" self.native.text = value # Tell the text layout algorithm how many lines are allowed self.native.numberOfLines = len(self.interface.text.split("\n")) def rehint(self): fitting_size = self.native.systemLayoutSizeFittingSize(CGSize(0, 0)) - # print("REHINT label", self, fitting_size.width, fitting_size.height) + # print(f"REHINT label {self} {self.get_text()!r} {fitting_size.width} {fitting_size.height}") self.interface.intrinsic.width = at_least(ceil(fitting_size.width)) self.interface.intrinsic.height = ceil(fitting_size.height) From 7864c9732f985d49bd7e7727431e7f2729f29a71 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 22 Mar 2023 11:27:13 +0800 Subject: [PATCH 18/21] Correct the Pango negative size warning. --- gtk/src/toga_gtk/fonts.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/gtk/src/toga_gtk/fonts.py b/gtk/src/toga_gtk/fonts.py index 15efc890d3..62f006f7db 100644 --- a/gtk/src/toga_gtk/fonts.py +++ b/gtk/src/toga_gtk/fonts.py @@ -1,4 +1,11 @@ -from toga.constants import BOLD, ITALIC, OBLIQUE, SMALL_CAPS, SYSTEM +from toga.constants import ( + BOLD, + ITALIC, + OBLIQUE, + SMALL_CAPS, + SYSTEM, + SYSTEM_DEFAULT_FONT_SIZE, +) from .libs import Pango @@ -27,8 +34,9 @@ def __init__(self, interface): font.set_family(family) - # Set font size - font.set_size(self.interface.size * Pango.SCALE) + # If this is a non-default font size, set the font size + if self.interface.size != SYSTEM_DEFAULT_FONT_SIZE: + font.set_size(self.interface.size * Pango.SCALE) # Set font style if self.interface.style == ITALIC: From 2ffd585b130a39f152fd8f4204105fcf1b9d0c2a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 22 Mar 2023 11:46:05 +0800 Subject: [PATCH 19/21] Correct probe/API handling of ZWS. --- iOS/src/toga_iOS/widgets/label.py | 5 ++++- iOS/tests_backend/widgets/label.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/iOS/src/toga_iOS/widgets/label.py b/iOS/src/toga_iOS/widgets/label.py index 1ca1168764..d9f7e56738 100644 --- a/iOS/src/toga_iOS/widgets/label.py +++ b/iOS/src/toga_iOS/widgets/label.py @@ -34,7 +34,10 @@ def set_font(self, font): self.native.font = font._impl.native def get_text(self): - return str(self.native.text) + value = str(self.native.text) + if value == "\u200B": + return "" + return value def set_text(self, value): if value == "": diff --git a/iOS/tests_backend/widgets/label.py b/iOS/tests_backend/widgets/label.py index 2614e80f84..e2d870bbdc 100644 --- a/iOS/tests_backend/widgets/label.py +++ b/iOS/tests_backend/widgets/label.py @@ -10,7 +10,10 @@ class LabelProbe(SimpleProbe): @property def text(self): - return str(self.native.text) + value = str(self.native.text) + if value == "\u200B": + return "" + return value @property def color(self): From 879f63e891e761dc27d8503db93c471aad0aade2 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 22 Mar 2023 12:22:30 +0800 Subject: [PATCH 20/21] Remove a redundant documentation note. --- docs/reference/api/widgets/label.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/reference/api/widgets/label.rst b/docs/reference/api/widgets/label.rst index 779c48aec6..e2dfcbb66d 100644 --- a/docs/reference/api/widgets/label.rst +++ b/docs/reference/api/widgets/label.rst @@ -25,10 +25,6 @@ Usage Notes ----- -* Any object can be provided as the text for the label. If the object is - ``None``, this will be converted into the empty string; any other non-string - object will be automatically converted to a string using ``str()``. - * Winforms does not support an alignment value of ``JUSTIFIED``. If this alignment value is used, the label will default to left alignment. From 15c3515760c489689da8fe47f192b5b061f3e8b4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 22 Mar 2023 15:50:57 +0800 Subject: [PATCH 21/21] Relax width and height constraint during iOS rehinting to avoid word wrapping. --- iOS/src/toga_iOS/widgets/label.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/iOS/src/toga_iOS/widgets/label.py b/iOS/src/toga_iOS/widgets/label.py index d9f7e56738..ad4674bfba 100644 --- a/iOS/src/toga_iOS/widgets/label.py +++ b/iOS/src/toga_iOS/widgets/label.py @@ -5,15 +5,15 @@ from toga.colors import TRANSPARENT from toga_iOS.colors import native_color -from toga_iOS.libs import NSLineBreakByWordWrapping, NSTextAlignment, UILabel +from toga_iOS.libs import NSLineBreakByClipping, NSTextAlignment, UILabel from toga_iOS.widgets.base import Widget class Label(Widget): def create(self): self.native = UILabel.new() - # Word wrap the text inside the allocated space - self.native.lineBreakMode = NSLineBreakByWordWrapping + # We shouldn't ever word wrap; if faced with that option, clip. + self.native.lineBreakMode = NSLineBreakByClipping # Add the layout constraints self.add_constraints() @@ -47,6 +47,17 @@ def set_text(self, value): self.native.numberOfLines = len(self.interface.text.split("\n")) def rehint(self): + # iOS text layout is an interplay between the layout constraints and the + # text layout algorithm. If the layout constraints fix the width, this + # can cause the text layout algorithm to try word wrapping to make text + # fit. To avoid this, temporarily relax the width and height constraint + # on the widget to "effectively infinite" values; they will be + # re-applied as part of the application of the newly hinted layout. + if self.constraints: + if self.constraints.width_constraint: + self.constraints.width_constraint.constant = 100000 + if self.constraints.height_constraint: + self.constraints.height_constraint.constant = 100000 fitting_size = self.native.systemLayoutSizeFittingSize(CGSize(0, 0)) # print(f"REHINT label {self} {self.get_text()!r} {fitting_size.width} {fitting_size.height}") self.interface.intrinsic.width = at_least(ceil(fitting_size.width))