diff --git a/android/src/toga_android/screens.py b/android/src/toga_android/screens.py index a4d3dead0c..57707f7ee1 100644 --- a/android/src/toga_android/screens.py +++ b/android/src/toga_android/screens.py @@ -4,6 +4,7 @@ ) from toga.screens import Screen as ScreenInterface +from toga.types import Position, Size from .widgets.base import Scalable @@ -26,11 +27,11 @@ def __new__(cls, app, native): def get_name(self): return self.native.getName() - def get_origin(self): - return (0, 0) + def get_origin(self) -> Position: + return Position(0, 0) - def get_size(self): - return ( + def get_size(self) -> Size: + return Size( self.scale_out(self.native.getWidth()), self.scale_out(self.native.getHeight()), ) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 6fbf0a9ea9..f4621a3ddb 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -10,6 +10,8 @@ from java import dynamic_proxy from java.io import ByteArrayOutputStream +from toga.types import Position, Size + from .container import Container from .screens import Screen as ScreenImpl @@ -96,8 +98,8 @@ def refreshed(self): # Window size ###################################################################### - def get_size(self): - return (self.width, self.height) + def get_size(self) -> Size: + return Size(self.width, self.height) def set_size(self, size): # Does nothing on mobile @@ -112,8 +114,8 @@ def get_current_screen(self): window_manager = context.getSystemService(Context.WINDOW_SERVICE) return ScreenImpl(self.app, window_manager.getDefaultDisplay()) - def get_position(self): - return 0, 0 + def get_position(self) -> Position: + return Position(0, 0) def set_position(self, position): # Does nothing on mobile diff --git a/changes/2388.feature.rst b/changes/2388.feature.rst new file mode 100644 index 0000000000..5701fba504 --- /dev/null +++ b/changes/2388.feature.rst @@ -0,0 +1 @@ +Size and position properties now return values as a `Size` and `Position` namedtuple, respectively. These namedtuple objects support addition and subtraction operations. Basic tuples can still be used to *set* these properties. diff --git a/cocoa/src/toga_cocoa/screens.py b/cocoa/src/toga_cocoa/screens.py index f58c1954e4..fab406b70d 100644 --- a/cocoa/src/toga_cocoa/screens.py +++ b/cocoa/src/toga_cocoa/screens.py @@ -1,6 +1,7 @@ from rubicon.objc import CGSize from toga.screens import Screen as ScreenInterface +from toga.types import Position, Size from toga_cocoa.libs import ( NSImage, core_graphics, @@ -23,13 +24,13 @@ def __new__(cls, native): def get_name(self): return str(self.native.localizedName) - def get_origin(self): + def get_origin(self) -> Position: frame_native = self.native.frame - return (int(frame_native.origin.x), int(frame_native.origin.y)) + return Position(int(frame_native.origin.x), int(frame_native.origin.y)) - def get_size(self): + def get_size(self) -> Size: frame_native = self.native.frame - return (int(frame_native.size.width), int(frame_native.size.height)) + return Size(int(frame_native.size.width), int(frame_native.size.height)) def get_image_data(self): # Retrieve the device description dictionary for the NSScreen diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 0bc63d99b6..d41da811df 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -1,6 +1,7 @@ from rubicon.objc import CGSize from toga.command import Command, Separator +from toga.types import Position, Size from toga_cocoa.container import Container from toga_cocoa.libs import ( SEL, @@ -285,9 +286,9 @@ def set_content(self, widget): # Window size ###################################################################### - def get_size(self): + def get_size(self) -> Size: frame = self.native.frame - return frame.size.width, frame.size.height + return Size(frame.size.width, frame.size.height) def set_size(self, size): frame = self.native.frame @@ -301,14 +302,14 @@ def set_size(self, size): def get_current_screen(self): return ScreenImpl(self.native.screen) - def get_position(self): + def get_position(self) -> Position: # The "primary" screen has index 0 and origin (0, 0). primary_screen = NSScreen.screens[0].frame window_frame = self.native.frame # macOS origin is bottom left of screen, and the screen might be # offset relative to other screens. Adjust for this. - return ( + return Position( window_frame.origin.x, primary_screen.size.height - (window_frame.origin.y + window_frame.size.height), diff --git a/core/src/toga/__init__.py b/core/src/toga/__init__.py index a6fc276daf..09bfa38098 100644 --- a/core/src/toga/__init__.py +++ b/core/src/toga/__init__.py @@ -12,7 +12,7 @@ from .keys import Key # Types -from .types import LatLng +from .types import LatLng, Position, Size # Widgets from .widgets.activityindicator import ActivityIndicator @@ -78,6 +78,8 @@ def warn(self, platform, feature): "Image", # Types "LatLng", + "Position", + "Size", # Widgets "ActivityIndicator", "Box", diff --git a/core/src/toga/app.py b/core/src/toga/app.py index c33ac9bfd0..5bef2bae82 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -30,11 +30,13 @@ from toga.paths import Paths from toga.platform import get_platform_factory from toga.screens import Screen +from toga.types import Position, Size from toga.widgets.base import Widget from toga.window import Window if TYPE_CHECKING: from toga.icons import IconContent + from toga.types import PositionT, SizeT # Make sure deprecation warnings are shown by default warnings.filterwarnings("default", category=DeprecationWarning) @@ -206,8 +208,8 @@ def __init__( self, id: str | None = None, title: str | None = None, - position: tuple[int, int] = (100, 100), - size: tuple[int, int] = (640, 480), + position: PositionT = Position(100, 100), + size: SizeT = Size(640, 480), resizable: bool = True, minimizable: bool = True, content: Widget | None = None, @@ -219,10 +221,10 @@ def __init__( :param id: A unique identifier for the window. If not provided, one will be automatically generated. :param title: Title for the window. Defaults to the formal name of the app. - :param position: Position of the window, as a tuple of ``(x, y)`` coordinates, - in :ref:`CSS pixels `. - :param size: Size of the window, as a tuple of ``(width, height)``, in :ref:`CSS - pixels `. + :param position: Position of the window, as a :any:`toga.Position` or tuple of + ``(x, y)`` coordinates, in :ref:`CSS pixels `. + :param size: Size of the window, as a :any:`toga.Size` or tuple of ``(width, + height)``, in :ref:`CSS pixels `. :param resizable: Can the window be resized by the user? :param minimizable: Can the window be minimized by the user? :param content: The initial content for the window. @@ -274,8 +276,8 @@ def __init__( doc: Document, id: str | None = None, title: str | None = None, - position: tuple[int, int] = (100, 100), - size: tuple[int, int] = (640, 480), + position: PositionT = Position(100, 100), + size: SizeT = Size(640, 480), resizable: bool = True, minimizable: bool = True, ): @@ -289,8 +291,10 @@ def __init__( :param document: The document being managed by this window :param id: The ID of the window. :param title: Title for the window. Defaults to the formal name of the app. - :param position: Position of the window, as a tuple of ``(x, y)`` coordinates. - :param size: Size of the window, as a tuple of ``(width, height)``, in pixels. + :param position: Position of the window, as a :any:`toga.Position` or tuple of + ``(x, y)`` coordinates. + :param size: Size of the window, as a :any:`toga.Size` or tuple of + ``(width, height)``, in pixels. :param resizable: Can the window be manually resized by the user? :param minimizable: Can the window be minimized by the user? """ diff --git a/core/src/toga/screens.py b/core/src/toga/screens.py index 65f341cc59..3dc24b8f9c 100644 --- a/core/src/toga/screens.py +++ b/core/src/toga/screens.py @@ -4,6 +4,7 @@ from toga.images import Image from toga.platform import get_platform_factory +from toga.types import Position, Size if TYPE_CHECKING: from toga.images import ImageT @@ -20,13 +21,14 @@ def name(self) -> str: return self._impl.get_name() @property - def origin(self) -> tuple[int, int]: - """The absolute coordinates of the screen's origin, as a ``(x, y)`` tuple.""" + def origin(self) -> Position: + """The absolute coordinates of the screen's origin, in :ref:`CSS pixels + `.""" return self._impl.get_origin() @property - def size(self) -> tuple[int, int]: - """The size of the screen, as a ``(width, height)`` tuple.""" + def size(self) -> Size: + """The size of the screen, in :ref:`CSS pixels `.""" return self._impl.get_size() def as_image(self, format: type[ImageT] = Image) -> ImageT: diff --git a/core/src/toga/types.py b/core/src/toga/types.py index 594a1ba4e0..eeacd34de8 100644 --- a/core/src/toga/types.py +++ b/core/src/toga/types.py @@ -1,6 +1,18 @@ from __future__ import annotations -from typing import NamedTuple +import sys +from typing import TYPE_CHECKING, NamedTuple, Tuple + +import toga + +if TYPE_CHECKING: + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias + else: + from typing import TypeAlias + + PositionT: TypeAlias = toga.Position | Tuple[int, int] + SizeT: TypeAlias = toga.Size | Tuple[int, int] class LatLng(NamedTuple): @@ -14,3 +26,35 @@ class LatLng(NamedTuple): def __str__(self): return f"({self.lat:6f}, {self.lng:6f})" + + +class Position(NamedTuple): + """A 2D window position.""" + + #: X coordinate, in CSS pixels. + x: int + + #: Y coordinate, in CSS pixels. + y: int + + def __str__(self) -> str: + return f"({self.x}, {self.y})" + + def __add__(self, other): + return Position(self.x + other.x, self.y + other.y) + + def __sub__(self, other): + return Position(self.x - other.x, self.y - other.y) + + +class Size(NamedTuple): + """A 2D window size.""" + + #: Width + width: int + + #: Height + height: int + + def __str__(self) -> str: + return f"({self.width} x {self.height})" diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index f1e5dc2676..4d1a64d36e 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -1,9 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from toga.handlers import wrapped_handler +from toga.types import Position from .base import Widget +if TYPE_CHECKING: + from toga.types import PositionT + class ScrollContainer(Widget): def __init__( @@ -191,8 +197,8 @@ def vertical_position(self, vertical_position): # the horizontal and vertical position separately would cause the horizontal and # vertical movement to appear as two separate animations. @property - def position(self) -> tuple[int, int]: - """The current scroll position, in the form (horizontal, vertical). + def position(self) -> Position: + """The current scroll position. If the value provided for either axis is negative, or greater than the maximum position in that axis, the value will be clipped to the valid range. @@ -200,10 +206,10 @@ def position(self) -> tuple[int, int]: If scrolling is disabled in either axis, the value provided for that axis will be ignored. """ - return (self.horizontal_position, self.vertical_position) + return Position(self.horizontal_position, self.vertical_position) @position.setter - def position(self, position): + def position(self, position: PositionT): horizontal_position, vertical_position = map(int, position) if self.horizontal: if horizontal_position < 0: diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 2fdb0b8593..651068d6b5 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -21,11 +21,13 @@ from toga.handlers import AsyncResult, wrapped_handler from toga.images import Image from toga.platform import get_platform_factory +from toga.types import Position, Size if TYPE_CHECKING: from toga.app import App from toga.images import ImageT from toga.screens import Screen + from toga.types import PositionT, SizeT from toga.widgets.base import Widget @@ -121,8 +123,8 @@ def __init__( self, id: str | None = None, title: str | None = None, - position: tuple[int, int] = (100, 100), - size: tuple[int, int] = (640, 480), + position: PositionT = Position(100, 100), + size: SizeT = Size(640, 480), resizable: bool = True, closable: bool = True, minimizable: bool = True, @@ -136,10 +138,10 @@ def __init__( :param id: A unique identifier for the window. If not provided, one will be automatically generated. :param title: Title for the window. Defaults to "Toga". - :param position: Position of the window, as a tuple of ``(x, y)`` coordinates, - in :ref:`CSS pixels `. - :param size: Size of the window, as a tuple of ``(width, height)``, in :ref:`CSS - pixels `. + :param position: Position of the window, as a :any:`toga.Position` or tuple of + ``(x, y)`` coordinates, in :ref:`CSS pixels `. + :param size: Size of the window, as a :any:`toga.Size` or tuple of ``(width, + height)``, in :ref:`CSS pixels `. :param resizable: Can the window be resized by the user? :param closable: Can the window be closed by the user? :param minimizable: Can the window be minimized by the user? @@ -182,6 +184,8 @@ def __init__( self._minimizable = minimizable self.factory = get_platform_factory() + position = Position(*position) + size = Size(*size) self._impl = getattr(self.factory, self._WINDOW_CLASS)( interface=self, title=title if title else self._default_title, @@ -343,13 +347,12 @@ def widgets(self) -> Mapping[str, Widget]: ###################################################################### @property - def size(self) -> tuple[int, int]: - """Size of the window, as a tuple of ``(width, height)``, in - :ref:`CSS pixels `.""" + def size(self) -> Size: + """Size of the window, in :ref:`CSS pixels `.""" return self._impl.get_size() @size.setter - def size(self, size: tuple[int, int]) -> None: + def size(self, size: SizeT) -> None: self._impl.set_size(size) if self.content: self.content.refresh() @@ -359,28 +362,21 @@ def size(self, size: tuple[int, int]) -> None: ###################################################################### @property - def position(self) -> tuple[int, int]: - """Absolute position of the window, as a ``(x, y)`` tuple coordinates, in - :ref:`CSS pixels `. + def position(self) -> Position: + """Absolute position of the window, in :ref:`CSS pixels `. The origin is the top left corner of the primary screen. """ absolute_origin = self._app.screens[0].origin absolute_window_position = self._impl.get_position() + window_position = absolute_window_position - absolute_origin - window_position = ( - absolute_window_position[0] - absolute_origin[0], - absolute_window_position[1] - absolute_origin[1], - ) return window_position @position.setter - def position(self, position: tuple[int, int]) -> None: + def position(self, position: PositionT) -> None: absolute_origin = self._app.screens[0].origin - absolute_new_position = ( - position[0] + absolute_origin[0], - position[1] + absolute_origin[1], - ) + absolute_new_position = Position(*position) + absolute_origin self._impl.set_position(absolute_new_position) @property @@ -393,26 +389,17 @@ def screen(self, app_screen: Screen) -> None: original_window_location = self.position original_origin = self.screen.origin new_origin = app_screen.origin - x = original_window_location[0] - original_origin[0] + new_origin[0] - y = original_window_location[1] - original_origin[1] + new_origin[1] - - self._impl.set_position((x, y)) + self._impl.set_position(original_window_location - original_origin + new_origin) @property - def screen_position(self) -> tuple[int, int]: - """Position of the window with respect to current screen, as a ``(x, y)`` tuple.""" - current_relative_position = ( - self.position[0] - self.screen.origin[0], - self.position[1] - self.screen.origin[1], - ) - return current_relative_position + def screen_position(self) -> Position: + """Position of the window with respect to current screen, in + :ref:`CSS pixels `.""" + return self.position - self.screen.origin @screen_position.setter - def screen_position(self, position: tuple[int, int]) -> None: - new_relative_position = ( - position[0] + self.screen.origin[0], - position[1] + self.screen.origin[1], - ) + def screen_position(self, position: PositionT) -> None: + new_relative_position = Position(*position) + self.screen.origin self._impl.set_position(new_relative_position) ###################################################################### diff --git a/core/tests/test_types.py b/core/tests/test_types.py new file mode 100644 index 0000000000..3b94cbf99a --- /dev/null +++ b/core/tests/test_types.py @@ -0,0 +1,29 @@ +from toga.types import Position, Size + + +def test_position_properties(): + """A Position NamedTuple has X and Y values.""" + p = Position(1, 2) + assert p.x == 1 + assert p.y == 2 + assert str(p) == "(1, 2)" + p == (1, 2) # Tuple equivalence for backwards-compatibility + + +def test_add_positions(): + """The sum of two Positions combines their X and Y values""" + assert Position(1, 2) + Position(3, 4) == Position(4, 6) + + +def test_sub_positions(): + """The difference of two Positions subtracts their X and Y values""" + assert Position(1, 2) - Position(3, 4) == Position(-2, -2) + + +def test_size_properties(): + """A Size NamedTuple has a width and height.""" + s = Size(1, 2) + assert s.width == 1 + assert s.height == 2 + assert str(s) == "(1 x 2)" + s == (1, 2) # Tuple equivalence for backwards-compatibility diff --git a/core/tests/widgets/test_scrollcontainer.py b/core/tests/widgets/test_scrollcontainer.py index cacb76099d..a17013eaaa 100644 --- a/core/tests/widgets/test_scrollcontainer.py +++ b/core/tests/widgets/test_scrollcontainer.py @@ -3,6 +3,7 @@ import pytest import toga +from toga.types import Position from toga_dummy.utils import ( EventLog, assert_action_not_performed, @@ -395,7 +396,9 @@ def test_set_vertical_position_when_not_vertical(scroll_container): @pytest.mark.parametrize( "position, expected", [ - ((37, 42), (37, 42)), + ((37, 42), Position(37, 42)), # Cast to a Position type + ((37, 42), (37, 42)), # Backwards-compatible with tuple + (Position(82, 82), Position(82, 82)), # Accepts Position input ((-100, 42), (0, 42)), # Clipped to minimum horizontal value ((37, -100), (37, 0)), # Clipped to minimum vertical value ((-100, -100), (0, 0)), # Clipped to minimum diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 482ed878a8..073846b0b8 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -29,8 +29,8 @@ def test_window_created(app): # We can't know what the ID is, but it must be a string. assert isinstance(window.id, str) assert window.title == "Toga" - assert window.position == (100, 100) - assert window.size == (640, 480) + assert window.position == toga.Position(100, 100) + assert window.size == toga.Size(640, 480) assert window.resizable assert window.closable assert window.minimizable @@ -45,8 +45,8 @@ def test_window_created_explicit(app): window = toga.Window( id="my-window", title="My Window", - position=(10, 20), - size=(200, 300), + position=toga.Position(10, 20), + size=toga.Position(200, 300), resizable=False, closable=False, minimizable=False, @@ -61,8 +61,8 @@ def test_window_created_explicit(app): assert window.id == "my-window" assert window.title == "My Window" - assert window.position == (10, 20) - assert window.size == (200, 300) + assert window.position == toga.Position(10, 20) + assert window.size == toga.Size(200, 300) assert not window.resizable assert not window.closable assert not window.minimizable @@ -70,6 +70,14 @@ def test_window_created_explicit(app): assert window.on_close._raw == on_close_handler +def test_window_creation_accepts_tuples(app): + """Tuple args are accepted and converted to NamedTuples""" + on_close_handler = Mock() + window = toga.Window(position=(10, 20), size=(200, 300), on_close=on_close_handler) + assert window.position == toga.Position(10, 20) + assert window.size == toga.Size(200, 300) + + def test_window_created_without_app(): """A window cannot be created without an active app.""" toga.App.app = None @@ -192,14 +200,14 @@ def test_set_position(window): """The position of the window can be set.""" window.position = (123, 456) - assert window.position == (123, 456) + assert window.position == toga.Position(123, 456) def test_set_size(window): """The size of the window can be set.""" window.size = (123, 456) - assert window.size == (123, 456) + assert window.size == toga.Size(123, 456) def test_set_size_with_content(window): @@ -209,7 +217,7 @@ def test_set_size_with_content(window): window.size = (123, 456) - assert window.size == (123, 456) + assert window.size == toga.Size(123, 456) assert_action_performed(content, "refresh") @@ -380,9 +388,9 @@ def test_screen(window, app): # window between the screens. # `window.screen` will return `Secondary Screen` assert window.screen == app.screens[1] - assert window.position == (100, 100) + assert window.position == toga.Position(100, 100) window.screen = app.screens[0] - assert window.position == (1466, 868) + assert window.position == toga.Position(1466, 868) def test_screen_position(window, app): @@ -391,13 +399,13 @@ def test_screen_position(window, app): initial_position = window.position window.position = (-100, -100) assert window.position != initial_position - assert window.position == (-100, -100) - assert window.screen_position == (1266, 668) + assert window.position == toga.Position(-100, -100) + assert window.screen_position == toga.Position(1266, 668) # Move the window to a new position. window.screen_position = (100, 100) - assert window.position == (-1266, -668) - assert window.screen_position == (100, 100) + assert window.position == toga.Position(-1266, -668) + assert window.screen_position == toga.Position(100, 100) def test_widget_id_reusablity(window, app): diff --git a/docs/reference/api/types.rst b/docs/reference/api/types.rst index 110008173d..b326e9122e 100644 --- a/docs/reference/api/types.rst +++ b/docs/reference/api/types.rst @@ -2,3 +2,5 @@ Types ===== .. autonamedtuple:: toga.LatLng +.. autonamedtuple:: toga.Position +.. autonamedtuple:: toga.Size diff --git a/dummy/src/toga_dummy/screens.py b/dummy/src/toga_dummy/screens.py index efde573938..07ff82afb6 100644 --- a/dummy/src/toga_dummy/screens.py +++ b/dummy/src/toga_dummy/screens.py @@ -1,6 +1,7 @@ from PIL import Image, ImageDraw from toga.screens import Screen as ScreenInterface +from toga.types import Position, Size from .utils import LoggedObject # noqa @@ -26,11 +27,11 @@ def __new__(cls, native): def get_name(self): return self.native[0] - def get_origin(self): - return self.native[1] + def get_origin(self) -> Position: + return Position(*self.native[1]) - def get_size(self): - return self.native[2] + def get_size(self) -> Size: + return Size(*self.native[2]) def get_image_data(self): self._action("get image data") diff --git a/dummy/src/toga_dummy/widgets/base.py b/dummy/src/toga_dummy/widgets/base.py index 4868be0eda..93743330f6 100644 --- a/dummy/src/toga_dummy/widgets/base.py +++ b/dummy/src/toga_dummy/widgets/base.py @@ -1,3 +1,5 @@ +from toga.types import Size + from ..utils import LoggedObject @@ -9,8 +11,8 @@ def __init__(self, interface): self.container = None self.create() - def get_size(self): - return (37, 42) + def get_size(self) -> Size: + return Size(37, 42) def create(self): self._action("create Widget") diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 2965d7c340..11b961d390 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -1,6 +1,7 @@ from pathlib import Path import toga_dummy +from toga.types import Size from .screens import Screen as ScreenImpl from .utils import LoggedObject @@ -30,11 +31,11 @@ def content(self, value): @property def width(self): - return self.content.get_size()[0] + return self.content.get_size().width @property def height(self): - return self.content.get_size()[1] + return self.content.get_size().height def refreshed(self): if self.content: @@ -72,8 +73,8 @@ def get_position(self): def set_position(self, position): self._set_value("position", position) - def get_size(self): - return self._get_value("size", (640, 480)) + def get_size(self) -> Size: + return self._get_value("size", Size(640, 480)) def set_size(self, size): self._set_value("size", size) diff --git a/gtk/src/toga_gtk/screens.py b/gtk/src/toga_gtk/screens.py index c095e4f7a5..f16ea792eb 100644 --- a/gtk/src/toga_gtk/screens.py +++ b/gtk/src/toga_gtk/screens.py @@ -1,6 +1,7 @@ import os from toga.screens import Screen as ScreenInterface +from toga.types import Position, Size from .libs import Gdk @@ -21,13 +22,13 @@ def __new__(cls, native): def get_name(self): return self.native.get_model() - def get_origin(self): + def get_origin(self) -> Position: geometry = self.native.get_geometry() - return geometry.x, geometry.y + return Position(geometry.x, geometry.y) - def get_size(self): + def get_size(self) -> Size: geometry = self.native.get_geometry() - return geometry.width, geometry.height + return Size(geometry.width, geometry.height) def get_image_data(self): if "WAYLAND_DISPLAY" in os.environ: diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 5189e4c1af..5562ba8b81 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -1,9 +1,17 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from toga.command import Separator +from toga.types import Position, Size from .container import TogaContainer from .libs import Gdk, Gtk from .screens import Screen as ScreenImpl +if TYPE_CHECKING: # pragma: no cover + from toga.types import PositionT, SizeT + class Window: def __init__(self, interface, title, position, size): @@ -159,11 +167,11 @@ def set_content(self, widget): # Window size ###################################################################### - def get_size(self): + def get_size(self) -> Size: size = self.native.get_size() - return size.width, size.height + return Size(size.width, size.height) - def set_size(self, size): + def set_size(self, size: SizeT): self.native.resize(size[0], size[1]) ###################################################################### @@ -175,11 +183,11 @@ def get_current_screen(self): monitor_native = display.get_monitor_at_window(self.native.get_window()) return ScreenImpl(monitor_native) - def get_position(self): + def get_position(self) -> Position: pos = self.native.get_position() - return pos.root_x, pos.root_y + return Position(pos.root_x, pos.root_y) - def set_position(self, position): + def set_position(self, position: PositionT): self.native.move(position[0], position[1]) ###################################################################### diff --git a/iOS/src/toga_iOS/screens.py b/iOS/src/toga_iOS/screens.py index c81f2b08c4..00570e1ebc 100644 --- a/iOS/src/toga_iOS/screens.py +++ b/iOS/src/toga_iOS/screens.py @@ -1,6 +1,7 @@ from rubicon.objc import Block, objc_id from toga.screens import Screen as ScreenInterface +from toga.types import Position, Size from toga_iOS.libs import UIGraphicsImageRenderer, UIImage @@ -21,11 +22,11 @@ def get_name(self): # Return a dummy name as UIScreen object has no name related attributes. return "iOS Screen" - def get_origin(self): - return (0, 0) + def get_origin(self) -> Position: + return Position(0, 0) - def get_size(self): - return ( + def get_size(self) -> Size: + return Size( int(self.native.bounds.size.width), int(self.native.bounds.size.height), ) diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 7ab0129e6d..a2ae85ef90 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -6,6 +6,7 @@ objc_id, ) +from toga.types import Position, Size from toga_iOS.container import RootContainer from toga_iOS.images import nsdata_to_bytes from toga_iOS.libs import ( @@ -104,8 +105,8 @@ def set_content(self, widget): # Window size ###################################################################### - def get_size(self): - return ( + def get_size(self) -> Size: + return Size( UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height, ) @@ -121,8 +122,8 @@ def set_size(self, size): def get_current_screen(self): return ScreenImpl(UIScreen.mainScreen) - def get_position(self): - return 0, 0 + def get_position(self) -> Position: + return Position(0, 0) def set_position(self, position): # Does nothing on mobile diff --git a/textual/src/toga_textual/screens.py b/textual/src/toga_textual/screens.py index cde07341e5..20e8d4e0e5 100644 --- a/textual/src/toga_textual/screens.py +++ b/textual/src/toga_textual/screens.py @@ -1,4 +1,5 @@ from toga.screens import Screen as ScreenInterface +from toga.types import Position, Size from .widgets.base import Scalable @@ -19,11 +20,11 @@ def __new__(cls, native): def get_name(self): return "Textual Screen" - def get_origin(self): - return (0, 0) + def get_origin(self) -> Position: + return Position(0, 0) - def get_size(self): - return ( + def get_size(self) -> Size: + return Size( self.scale_out_horizontal(self.native.size.width), self.scale_out_vertical(self.native.size.height), ) diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index e3026153f9..4154c927d5 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -1,6 +1,7 @@ from travertino.size import at_least from toga.style.pack import ROW +from toga.types import Size # We assume a terminal is 800x600 pixels, mapping to 80x25 characters. @@ -35,8 +36,8 @@ def __init__(self, interface): self.container = None self.create() - def get_size(self): - return (0, 0) + def get_size(self) -> Size: + return Size(0, 0) def create(self): pass diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 017aac5d8e..92955de168 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -5,6 +5,7 @@ from textual.screen import Screen as TextualScreen from textual.widget import Widget as TextualWidget from textual.widgets import Button as TextualButton +from toga import Position, Size from .container import Container from .screens import Screen as ScreenImpl @@ -169,8 +170,8 @@ def set_content(self, widget): # Window size ###################################################################### - def get_size(self): - return (self.native.size.width, self.native.size.height) + def get_size(self) -> Size: + return Size(self.native.size.width, self.native.size.height) def set_size(self, size): pass @@ -182,8 +183,8 @@ def set_size(self, size): def get_current_screen(self): return ScreenImpl(self.native) - def get_position(self): - return (0, 0) + def get_position(self) -> Position: + return Position(0, 0) def set_position(self, position): pass diff --git a/web/src/toga_web/screens.py b/web/src/toga_web/screens.py index 15e5d49ce1..5d73b69bbd 100644 --- a/web/src/toga_web/screens.py +++ b/web/src/toga_web/screens.py @@ -1,4 +1,5 @@ from toga.screens import Screen as ScreenInterface +from toga.types import Position, Size class Screen: @@ -17,11 +18,11 @@ def __new__(cls, native): def get_name(self): return "Web Screen" - def get_origin(self): - return (0, 0) + def get_origin(self) -> Position: + return Position(0, 0) - def get_size(self): - return self.native.clientWidth, self.native.clientHeight + def get_size(self) -> Size: + return Size(self.native.clientWidth, self.native.clientHeight) def get_image_data(self): self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 29eca4532e..76ded4a7b0 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -1,3 +1,4 @@ +from toga.types import Position, Size from toga_web.libs import create_element, js from .screens import Screen as ScreenImpl @@ -77,8 +78,8 @@ def set_content(self, widget): # Window size ###################################################################### - def get_size(self): - return self.native.offsetWidth, self.native.offsetHeight + def get_size(self) -> Size: + return Size(self.native.offsetWidth, self.native.offsetHeight) def set_size(self, size): # Does nothing on web @@ -91,8 +92,8 @@ def set_size(self, size): def get_current_screen(self): return ScreenImpl(js.document.documentElement) - def get_position(self): - return 0, 0 + def get_position(self) -> Position: + return Position(0, 0) def set_position(self, position): # Does nothing on web diff --git a/winforms/src/toga_winforms/screens.py b/winforms/src/toga_winforms/screens.py index 7a0001e9e1..a05295b86d 100644 --- a/winforms/src/toga_winforms/screens.py +++ b/winforms/src/toga_winforms/screens.py @@ -3,11 +3,12 @@ Graphics, Imaging, Point, - Size, + Size as WinSize, ) from System.IO import MemoryStream from toga.screens import Screen as ScreenInterface +from toga.types import Position, Size class Screen: @@ -29,18 +30,18 @@ def get_name(self): # non-text part to prevent any errors due to non-escaped characters. return name.split("\\")[-1] - def get_origin(self): - return self.native.Bounds.X, self.native.Bounds.Y + def get_origin(self) -> Position: + return Position(self.native.Bounds.X, self.native.Bounds.Y) - def get_size(self): - return self.native.Bounds.Width, self.native.Bounds.Height + def get_size(self) -> Size: + return Size(self.native.Bounds.Width, self.native.Bounds.Height) def get_image_data(self): bitmap = Bitmap(*self.get_size()) graphics = Graphics.FromImage(bitmap) source_point = Point(*self.get_origin()) destination_point = Point(0, 0) - copy_size = Size(*self.get_size()) + copy_size = WinSize(*self.get_size()) graphics.CopyFromScreen(source_point, destination_point, copy_size) stream = MemoryStream() bitmap.Save(stream, Imaging.ImageFormat.Png) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 2c12dfb8f3..75ccdb6231 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -1,15 +1,23 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import System.Windows.Forms as WinForms -from System.Drawing import Bitmap, Graphics, Point, Size +from System.Drawing import Bitmap, Graphics, Point, Size as WinSize from System.Drawing.Imaging import ImageFormat from System.IO import MemoryStream from toga.command import Separator +from toga.types import Position, Size from .container import Container from .libs.wrapper import WeakrefCallable from .screens import Screen as ScreenImpl from .widgets.base import Scalable +if TYPE_CHECKING: # pragma: no cover + from toga.types import PositionT, SizeT + class Window(Container, Scalable): def __init__(self, interface, title, position, size): @@ -151,7 +159,7 @@ def _top_bars_height(self): def refreshed(self): super().refreshed() layout = self.interface.content.layout - self.native.MinimumSize = Size( + self.native.MinimumSize = WinSize( self.scale_in(layout.min_width) + self._decor_width(), self.scale_in(layout.min_height) + self._top_bars_height() @@ -170,18 +178,17 @@ def resize_content(self): # Window size ###################################################################### - def get_size(self): + def get_size(self) -> Size: size = self.native.Size - return ( + return Size( self.scale_out(size.Width - self._decor_width()), self.scale_out(size.Height - self._decor_height()), ) - def set_size(self, size): - width, height = size - self.native.Size = Size( - self.scale_in(width) + self._decor_width(), - self.scale_in(height) + self._decor_height(), + def set_size(self, size: SizeT): + self.native.Size = WinSize( + self.scale_in(size[0]) + self._decor_width(), + self.scale_in(size[1]) + self._decor_height(), ) ###################################################################### @@ -191,11 +198,11 @@ def set_size(self, size): def get_current_screen(self): return ScreenImpl(WinForms.Screen.FromControl(self.native)) - def get_position(self): + def get_position(self) -> Position: location = self.native.Location - return tuple(map(self.scale_out, (location.X, location.Y))) + return Position(*map(self.scale_out, (location.X, location.Y))) - def set_position(self, position): + def set_position(self, position: PositionT): self.native.Location = Point(*map(self.scale_in, position)) ###################################################################### @@ -228,7 +235,7 @@ def set_full_screen(self, is_full_screen): ###################################################################### def get_image_data(self): - size = Size(self.native_content.Size.Width, self.native_content.Size.Height) + size = WinSize(self.native_content.Size.Width, self.native_content.Size.Height) bitmap = Bitmap(size.Width, size.Height) graphics = Graphics.FromImage(bitmap)