diff --git a/android/tests_backend/widgets/base.py b/android/tests_backend/widgets/base.py index fae94f78b7..ac82ef521b 100644 --- a/android/tests_backend/widgets/base.py +++ b/android/tests_backend/widgets/base.py @@ -16,9 +16,15 @@ def assert_container(self, container): else: raise AssertionError(f"cannot find {self.native} in {container_native}") + def alignment_equivalent(self, actual, expected): + assert actual == expected + return True + async def redraw(self): """Request a redraw of the app, waiting until that redraw has completed.""" - # Refresh the layout + # TODO: Travertino/Pack doesn't force a layout refresh + # when properties such as flex or width are altered. + # For now, do a manual refresh. self.widget.window.content.refresh() @property diff --git a/cocoa/src/toga_cocoa/constraints.py b/cocoa/src/toga_cocoa/constraints.py index 8cfa10beb0..dcf5b2ac0f 100644 --- a/cocoa/src/toga_cocoa/constraints.py +++ b/cocoa/src/toga_cocoa/constraints.py @@ -31,6 +31,7 @@ def container(self): @container.setter def container(self, value): if value is None and self.container: + # print("Remove constraints for", self.widget, 'in', self.container) self.container.native.removeConstraint(self.width_constraint) self.container.native.removeConstraint(self.height_constraint) self.container.native.removeConstraint(self.left_constraint) @@ -84,9 +85,8 @@ def container(self, value): self.container.native.addConstraint(self.height_constraint) def update(self, x, y, width, height): - # print("UPDATE", self.widget, 'in', self.container, 'to', x, y, width, height) if self.container: - # print("IN CONTAINER") + # print("UPDATE", self.widget, 'in', self.container, 'to', x, y, width, height) self.left_constraint.constant = x self.top_constraint.constant = y diff --git a/cocoa/tests_backend/widgets/base.py b/cocoa/tests_backend/widgets/base.py index b4efbbfabb..572a2804b4 100644 --- a/cocoa/tests_backend/widgets/base.py +++ b/cocoa/tests_backend/widgets/base.py @@ -12,10 +12,17 @@ def assert_container(self, container): else: raise ValueError(f"cannot find {self.native} in {container_native}") + def alignment_equivalent(self, actual, expected): + assert actual == expected + return True + async def redraw(self): """Request a redraw of the app, waiting until that redraw has completed.""" - # Refresh the layout + # TODO: Travertino/Pack doesn't force a layout refresh + # when properties such as flex or width are altered. + # For now, do a manual refresh. self.widget.window.content.refresh() + # Force a repaint self.widget.window.content._impl.native.displayIfNeeded() diff --git a/cocoa/tests_backend/widgets/properties.py b/cocoa/tests_backend/widgets/properties.py index a6a4206cc1..99b701ec0c 100644 --- a/cocoa/tests_backend/widgets/properties.py +++ b/cocoa/tests_backend/widgets/properties.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from travertino.fonts import Font from toga.colors import rgba from toga.fonts import FANTASY, NORMAL, SYSTEM @@ -23,15 +23,6 @@ def toga_color(color): return None -@dataclass -class Font: - family: str - size: int - style: str = NORMAL - variant: str = NORMAL - weight: str = NORMAL - - def toga_font(font): return Font( family={ @@ -39,6 +30,9 @@ def toga_font(font): "Papyrus": FANTASY, }.get(str(font.familyName), str(font.familyName)), size=font.pointSize, + style=NORMAL, # TODO: ITALIC if..., SMALL_CAPS if ... + variant=NORMAL, + weight=NORMAL, # TODO: BOLD if ... ) diff --git a/core/src/toga/style/applicator.py b/core/src/toga/style/applicator.py index f45418d648..e2b820f2ad 100644 --- a/core/src/toga/style/applicator.py +++ b/core/src/toga/style/applicator.py @@ -9,7 +9,7 @@ def refresh(self): self.widget.refresh() def set_bounds(self): - # print("APPLY LAYOUT", self.widget, self.widget.layout) + # print(" APPLY LAYOUT", self.widget, self.widget.layout) self.widget._impl.set_bounds( self.widget.layout.absolute_content_left, self.widget.layout.absolute_content_top, @@ -27,8 +27,11 @@ def set_hidden(self, hidden): self.widget._impl.set_hidden(hidden) def set_font(self, font): + # Changing the font of a widget can make the widget change size, + # which in turn means we need to do a re-layout self.widget._impl.set_font(font) self.widget._impl.rehint() + self.widget.refresh() def set_color(self, color): self.widget._impl.set_color(color) diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index 026aebd04a..97a0b7b3d1 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -47,7 +47,10 @@ def text(self, value): else: value = str(value) self._impl.set_text(value) + # Changing the text will probably cause the size of the button to change + # so we need to rehint, then recompute layout. self._impl.rehint() + self.refresh() @property def on_press(self): diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 1c9603380e..22ad648fd9 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -50,4 +50,7 @@ def text(self, value): else: self._text = str(value) self._impl.set_text(value) + # Changing the text will probably cause the size of the label to change + # so we need to rehint, then recompute layout. self._impl.rehint() + self.refresh() diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index 85fd8aea75..3875575bb4 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -140,7 +140,10 @@ def text(self, value): else: self._text = str(value) self._impl.set_text(value) + # Changing the text will probably cause the size of the switch to change + # so we need to rehint, then recompute layout. self._impl.rehint() + self.refresh() @property def on_change(self): diff --git a/docs/reference/api/widgets/label.rst b/docs/reference/api/widgets/label.rst index cf684b94c1..5ee8be3440 100644 --- a/docs/reference/api/widgets/label.rst +++ b/docs/reference/api/widgets/label.rst @@ -22,6 +22,12 @@ Usage label = toga.Label('Hello world') +Notes +----- + +* Winforms does not support an alignment value of ``JUSTIFIED``. If this + alignment value is used, the label will default left alignment. + Reference --------- diff --git a/gtk/tests_backend/widgets/base.py b/gtk/tests_backend/widgets/base.py index 8554bd76e3..f975e6a86a 100644 --- a/gtk/tests_backend/widgets/base.py +++ b/gtk/tests_backend/widgets/base.py @@ -18,6 +18,10 @@ def assert_container(self, container): else: raise ValueError(f"cannot find {self.native} in {container_native}") + def alignment_equivalent(self, actual, expected): + assert actual == expected + return True + async def redraw(self): """Request a redraw of the app, waiting until that redraw has completed.""" # Force a repaint diff --git a/iOS/tests_backend/widgets/base.py b/iOS/tests_backend/widgets/base.py index 4de90e3dd3..a903079157 100644 --- a/iOS/tests_backend/widgets/base.py +++ b/iOS/tests_backend/widgets/base.py @@ -40,12 +40,18 @@ def assert_container(self, container): else: raise ValueError(f"cannot find {self.native} in {container_native}") + def alignment_equivalent(self, actual, expected): + assert actual == expected + return True + async def redraw(self): """Request a redraw of the app, waiting until that redraw has completed.""" - # Refresh the layout + # TODO: Travertino/Pack doesn't force a layout refresh + # when properties such as flex or width are altered. + # For now, do a manual refresh. self.widget.window.content.refresh() + # Force a repaint - # self.widget.window.content._impl.native.layer.setNeedsDisplay_(True) self.widget.window.content._impl.native.layer.displayIfNeeded() @property diff --git a/testbed/tests/widgets/properties.py b/testbed/tests/widgets/properties.py index 640b060fe2..e2ca5e8556 100644 --- a/testbed/tests/widgets/properties.py +++ b/testbed/tests/widgets/properties.py @@ -73,7 +73,7 @@ async def test_color(widget, probe): @mark.skipif( - current_platform in {"android", "windows"}, + current_platform in {"android"}, reason="color resets don't work", ) async def test_color_reset(widget, probe): @@ -111,10 +111,7 @@ async def test_background_color_reset(widget, probe): assert_color(probe.background_color, original) -@mark.skipif( - current_platform == "windows", - reason="TRANSPARENT not implemented", -) async def test_background_color_transparent(widget, probe): + "Background transparency is treated as a color reset" widget.style.background_color = TRANSPARENT assert_color(probe.background_color, TRANSPARENT) diff --git a/testbed/tests/widgets/test_button.py b/testbed/tests/widgets/test_button.py index 3cb46fac46..3bf9555c5a 100644 --- a/testbed/tests/widgets/test_button.py +++ b/testbed/tests/widgets/test_button.py @@ -18,6 +18,12 @@ test_text_width_change, ) + +@fixture +async def widget(): + return toga.Button("Hello") + + test_font = mark.skipif( current_platform in {"iOS"}, reason="font changes don't alter size", @@ -28,12 +34,6 @@ reason="round trip empty strings don't work", )(test_text) - -@fixture -async def widget(): - return toga.Button("Hello") - - test_text_width_change = mark.skipif( current_platform in {"linux"}, reason="resizes not applying correctly", @@ -54,10 +54,6 @@ async def test_press(widget, probe): handler.assert_called_once_with(widget) -@mark.skipif( - current_platform in {"windows"}, - reason="color reset on transparent not implemented", -) async def test_background_color_transparent(widget, probe): "Buttons treat background transparency as a color reset." widget.style.background_color = TRANSPARENT @@ -75,10 +71,10 @@ async def test_background_color_transparent(widget, probe): 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, based on content size. + # Initial button size is small (but non-zero), based on content size. await probe.redraw() - assert 50 <= probe.width <= 100 - assert probe.height <= 50 + assert 10 <= probe.width <= 100 + assert 10 <= probe.height <= 50 # Make the button flexible; it will expand to fill horizontal space widget.style.flex = 1 diff --git a/testbed/tests/widgets/test_label.py b/testbed/tests/widgets/test_label.py index c034af1320..2893132b5a 100644 --- a/testbed/tests/widgets/test_label.py +++ b/testbed/tests/widgets/test_label.py @@ -51,14 +51,14 @@ def make_lines(n): async def test_alignment(widget, probe): - # Initial alignment is LEFT + # Initial alignment is LEFT, initial direction is LTR widget.parent.style.direction = COLUMN assert probe.alignment == LEFT for alignment in [RIGHT, CENTER, JUSTIFY]: widget.style.text_align = alignment await probe.redraw() - assert probe.alignment == alignment + assert probe.alignment_equivalent(probe.alignment, alignment) # Clearing the alignment reverts to default alignment of LEFT widget.style.text_align = None diff --git a/winforms/src/toga_winforms/libs/__init__.py b/winforms/src/toga_winforms/libs/__init__.py index b0d3166c37..029ef9b4f4 100644 --- a/winforms/src/toga_winforms/libs/__init__.py +++ b/winforms/src/toga_winforms/libs/__init__.py @@ -8,6 +8,7 @@ Action, Bitmap, Color, + ContentAlignment, Convert, CultureInfo, Drawing2D, @@ -32,6 +33,7 @@ String, StringFormat, SystemColors, + SystemFonts, Task, TaskScheduler, Threading, diff --git a/winforms/src/toga_winforms/widgets/base.py b/winforms/src/toga_winforms/widgets/base.py index 3f9eb3454d..92466b9b0b 100644 --- a/winforms/src/toga_winforms/widgets/base.py +++ b/winforms/src/toga_winforms/widgets/base.py @@ -1,4 +1,5 @@ -from toga_winforms.libs import Point, Size +from toga_winforms.colors import native_color +from toga_winforms.libs import Point, Size, SystemColors class Widget: @@ -100,12 +101,16 @@ def set_font(self, font): pass def set_color(self, color): - # By default, color can't be changed - pass + if color is None: + self.native.ForeColor = SystemColors.WindowText + else: + self.native.ForeColor = native_color(color) def set_background_color(self, color): - # By default, background color can't be changed. - pass + if color is None: + self.native.BackColor = SystemColors.Control + else: + self.native.BackColor = native_color(color) # INTERFACE diff --git a/winforms/src/toga_winforms/widgets/box.py b/winforms/src/toga_winforms/widgets/box.py index 71a2a4604c..3c84b98516 100644 --- a/winforms/src/toga_winforms/widgets/box.py +++ b/winforms/src/toga_winforms/widgets/box.py @@ -1,6 +1,3 @@ -from travertino.constants import TRANSPARENT - -from toga_winforms.colors import native_color from toga_winforms.libs import Point, Size, WinForms from .base import Widget @@ -19,6 +16,9 @@ def set_bounds(self, x, y, width, height): vertical_shift = ( self.frame.vertical_shift - self.interface.style.padding_top ) + # The outermost widget assumes the size of the viewport + width = self.viewport.width + height = self.viewport.height except AttributeError: vertical_shift = self.interface.style.padding_top horizontal_shift = self.interface.style.padding_left @@ -30,9 +30,3 @@ def set_bounds(self, x, y, width, height): width + horizontal_size_adjustment, height + vertical_size_adjustment ) self.native.Location = Point(x - horizontal_shift, y + vertical_shift) - - def set_background_color(self, value): - if value: - self.native.BackColor = native_color(value) - else: - self.native.BackColor = native_color(TRANSPARENT) diff --git a/winforms/src/toga_winforms/widgets/button.py b/winforms/src/toga_winforms/widgets/button.py index 92578d4502..1c57a4ea0f 100644 --- a/winforms/src/toga_winforms/widgets/button.py +++ b/winforms/src/toga_winforms/widgets/button.py @@ -1,8 +1,8 @@ -from travertino.constants import TRANSPARENT from travertino.size import at_least +from toga.colors import TRANSPARENT from toga_winforms.colors import native_color -from toga_winforms.libs import WinForms +from toga_winforms.libs import SystemColors, WinForms from .base import Widget @@ -10,6 +10,7 @@ class Button(Widget): def create(self): self.native = WinForms.Button() + self.native.AutoSizeMode = WinForms.AutoSizeMode.GrowAndShrink self.native.Click += self.winforms_click self.set_enabled(self.interface._enabled) @@ -22,11 +23,16 @@ def get_text(self): def set_text(self, text): self.native.Text = text - self.rehint() def set_font(self, font): self.native.Font = font._impl.native + def set_background_color(self, color): + if color is None or color == TRANSPARENT: + self.native.BackColor = SystemColors.Control + else: + self.native.BackColor = native_color(color) + def set_enabled(self, value): self.native.Enabled = self.interface._enabled @@ -34,18 +40,6 @@ def set_on_press(self, handler): # No special handling required pass - def set_color(self, value): - if value: - self.native.ForeColor = native_color(value) - else: - self.native.ForeColor = native_color(TRANSPARENT) - - def set_background_color(self, value): - if value: - self.native.BackColor = native_color(value) - else: - self.native.BackColor = native_color(TRANSPARENT) - def rehint(self): # self.native.Size = Size(0, 0) # print("REHINT Button", self, self.native.PreferredSize) diff --git a/winforms/src/toga_winforms/widgets/label.py b/winforms/src/toga_winforms/widgets/label.py index 0272c502cc..85aafee14b 100644 --- a/winforms/src/toga_winforms/widgets/label.py +++ b/winforms/src/toga_winforms/widgets/label.py @@ -1,7 +1,5 @@ -from travertino.constants import TRANSPARENT from travertino.size import at_least -from toga_winforms.colors import native_color from toga_winforms.libs import TextAlignment, WinForms from .base import Widget @@ -10,6 +8,7 @@ class Label(Widget): def create(self): self.native = WinForms.Label() + self.native.AutoSizeMode = WinForms.AutoSizeMode.GrowAndShrink def set_alignment(self, value): self.native.TextAlign = TextAlignment(value) @@ -18,20 +17,7 @@ def set_text(self, value): self.native.Text = self.interface._text def set_font(self, font): - if font: - self.native.Font = font._impl.native - - def set_color(self, value): - if value: - self.native.ForeColor = native_color(value) - else: - self.native.ForeColor = native_color(TRANSPARENT) - - def set_background_color(self, value): - if value: - self.native.BackColor = native_color(value) - else: - self.native.BackColor = native_color(TRANSPARENT) + self.native.Font = font._impl.native def rehint(self): # Width & height of a label is known and fixed. diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index e3dc446020..126e11fd35 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -18,6 +18,7 @@ def width(self): @property def height(self): + # Treat `native=None` as a 0x0 viewport if self.native is None: return 0 # Subtract any vertical shift of the frame. This is to allow diff --git a/winforms/tests_backend/widgets/base.py b/winforms/tests_backend/widgets/base.py index 909715a5c6..bbd7a10dab 100644 --- a/winforms/tests_backend/widgets/base.py +++ b/winforms/tests_backend/widgets/base.py @@ -1,6 +1,10 @@ from System import EventArgs, Object +from System.Drawing import SystemColors -from .properties import toga_color +from toga.colors import TRANSPARENT +from toga.style.pack import JUSTIFY, LEFT + +from .properties import toga_color, toga_font class SimpleProbe: @@ -17,22 +21,42 @@ def assert_container(self, container): else: raise ValueError(f"cannot find {self.native} in {container_native}") + def alignment_equivalent(self, actual, expected): + # Winforms doesn't have a "Justified" alignment; it falls back to LEFT + if expected == JUSTIFY: + assert actual == LEFT + else: + assert actual == expected + return True + async def redraw(self): """Request a redraw of the app, waiting until that redraw has completed.""" - # Refresh the layout + # TODO: Travertino/Pack doesn't force a layout refresh + # when properties such as flex or width are altered. + # For now, do a manual refresh. self.widget.window.content.refresh() @property def enabled(self): return self.native.Enabled + @property + def color(self): + if self.native.ForeColor == SystemColors.WindowText: + return None + else: + return toga_color(self.native.ForeColor) + @property def background_color(self): - return toga_color(self.native.BackColor) + if self.native.BackColor == SystemColors.Control: + return TRANSPARENT + else: + return toga_color(self.native.BackColor) @property - def color(self): - return toga_color(self.native.ForeColor) + def font(self): + return toga_font(self.native.Font) @property def hidden(self): diff --git a/winforms/tests_backend/widgets/button.py b/winforms/tests_backend/widgets/button.py index a1c6fe343a..507cb30f82 100644 --- a/winforms/tests_backend/widgets/button.py +++ b/winforms/tests_backend/widgets/button.py @@ -1,7 +1,8 @@ import System.Windows.Forms -from pytest import skip +from System.Drawing import SystemColors from .base import SimpleProbe +from .properties import toga_color class ButtonProbe(SimpleProbe): @@ -12,5 +13,8 @@ def text(self): return self.native.Text @property - def font(self): - skip("Font probe not implemented") + def background_color(self): + if self.native.BackColor == SystemColors.Control: + return None + else: + return toga_color(self.native.BackColor) diff --git a/winforms/tests_backend/widgets/label.py b/winforms/tests_backend/widgets/label.py index 9ce8c70da6..e814c92311 100644 --- a/winforms/tests_backend/widgets/label.py +++ b/winforms/tests_backend/widgets/label.py @@ -1,7 +1,7 @@ import System.Windows.Forms -from pytest import skip from .base import SimpleProbe +from .properties import toga_alignment class LabelProbe(SimpleProbe): @@ -11,10 +11,6 @@ class LabelProbe(SimpleProbe): def text(self): return self.native.Text - @property - def font(self): - skip("Font probe not implemented") - @property def alignment(self): - skip("Alignment probe not implemented") + return toga_alignment(self.native.TextAlign) diff --git a/winforms/tests_backend/widgets/properties.py b/winforms/tests_backend/widgets/properties.py index f0eba27506..5e85ec343f 100644 --- a/winforms/tests_backend/widgets/properties.py +++ b/winforms/tests_backend/widgets/properties.py @@ -1,5 +1,56 @@ -from toga.colors import rgba +from System.Drawing import ( + Color, + ContentAlignment, + FontFamily, + SystemColors, + SystemFonts, +) +from travertino.fonts import Font + +from toga.colors import TRANSPARENT, rgba +from toga.fonts import ( + BOLD, + CURSIVE, + FANTASY, + ITALIC, + MESSAGE, + MONOSPACE, + NORMAL, + SANS_SERIF, + SERIF, + SYSTEM, +) +from toga.style.pack import CENTER, LEFT, RIGHT def toga_color(color): - return rgba(color.R, color.G, color.B, color.A / 255) + if color in {Color.Empty, SystemColors.Control}: + return TRANSPARENT + else: + return rgba(color.R, color.G, color.B, color.A / 255) + + +def toga_font(font): + return Font( + family={ + SystemFonts.DefaultFont.FontFamily.Name: SYSTEM, + SystemFonts.MenuFont.FontFamily.Name: MESSAGE, + FontFamily.GenericSerif.Name: SERIF, + FontFamily.GenericSansSerif.Name: SANS_SERIF, + "Comic Sans MS": CURSIVE, + "Impact": FANTASY, + FontFamily.GenericMonospace.Name: MONOSPACE, + }.get(str(font.Name), str(font.Name)), + size=int(font.SizeInPoints), + style=ITALIC if font.Italic else NORMAL, + variant=NORMAL, + weight=BOLD if font.Bold else NORMAL, + ) + + +def toga_alignment(alignment): + return { + ContentAlignment.MiddleLeft: LEFT, + ContentAlignment.MiddleRight: RIGHT, + ContentAlignment.MiddleCenter: CENTER, + }[alignment]