Skip to content

Commit

Permalink
Rewrite DPI tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mhsmith committed Nov 6, 2024
1 parent 617c4fe commit 44f9124
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 115 deletions.
14 changes: 10 additions & 4 deletions core/src/toga/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __str__(self) -> str:


class Position(NamedTuple):
"""A 2D window position."""
"""A 2D position."""

#: X coordinate, in CSS pixels.
x: int
Expand All @@ -46,15 +46,21 @@ def __add__(self, other):
def __sub__(self, other):
return Position(self.x - other.x, self.y - other.y)

def __mul__(self, other):
return Position(self.x * other, self.y * other)


class Size(NamedTuple):
"""A 2D window size."""
"""A 2D size."""

#: Width
#: Width, in CSS pixels.
width: int

#: Height
#: Height, in CSS pixels.
height: int

def __str__(self) -> str:
return f"({self.width} x {self.height})"

def __mul__(self, other):
return Size(self.width * other, self.height * other)
23 changes: 22 additions & 1 deletion core/tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ def test_position_properties():
assert p.x == 1
assert p.y == 2
assert str(p) == "(1, 2)"
p == (1, 2) # Tuple equivalence for backwards-compatibility

assert p == Position(1, 2)
assert p != Position(1, 3)

assert p == (1, 2) # Tuple equivalence for backwards-compatibility
assert p != (1, 3)


def test_add_positions():
Expand All @@ -20,10 +25,26 @@ def test_sub_positions():
assert Position(1, 2) - Position(3, 4) == Position(-2, -2)


def test_mul_position():
"""Multiplying a Position multiplies its X and Y values"""
assert Position(1, 2) * 2 == Position(2, 4)
assert Position(1, 2) * 0.5 == Position(0.5, 1)
assert Position(1, 2) * 0 == Position(0, 0)
assert Position(1, 2) * -1 == Position(-1, -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


def test_mul_size():
"""Multiplying a Size multiplies its width and height values"""
assert Size(1, 2) * 2 == Size(2, 4)
assert Size(1, 2) * 0.5 == Size(0.5, 1)
assert Size(1, 2) * 0 == Size(0, 0)
assert Size(1, 2) * -1 == Size(-1, -2)
206 changes: 108 additions & 98 deletions testbed/tests/app/test_desktop.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from functools import reduce
from functools import partial
from unittest.mock import Mock

import pytest
from System import EventArgs

import toga
from toga import Position, Size
from toga.colors import CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE
from toga.style.pack import Pack

Expand Down Expand Up @@ -392,126 +394,134 @@ async def test_current_window(app, app_probe, main_window):
assert app.current_window == window3


@pytest.mark.parametrize("flex_direction", ["row", "column"])
@pytest.mark.parametrize(
"dpi_change_event_string",
"event_name",
[
"app._impl.winforms_DisplaySettingsChanged",
"main_window._impl.winforms_LocationChanged",
"main_window._impl.winforms_Resize",
],
)
@pytest.mark.parametrize(
"initial_dpi_scale, final_dpi_scale",
[
(1.0, 1.25),
(1.0, 1.5),
(1.0, 1.75),
(1.0, 2.0),
(1.25, 1.5),
(1.25, 1.75),
(1.25, 2.0),
(1.5, 1.75),
(1.5, 2.0),
(1.75, 2.0),
# FIXME DpiChangedAfterParent
"LocationChanged",
"Resize",
],
)
@pytest.mark.parametrize("mock_scale", [1.0, 1.25, 1.5, 1.75, 2.0])
async def test_system_dpi_change(
monkeypatch,
app,
app_probe,
main_window,
main_window_probe,
flex_direction,
dpi_change_event_string,
initial_dpi_scale,
final_dpi_scale,
main_window, main_window_probe, event_name, mock_scale
):
if toga.platform.current_platform != "windows":
pytest.xfail("This test is winforms backend specific")

# Get the dpi change event from the string
obj_name, *attr_parts = dpi_change_event_string.split(".")
obj = locals()[obj_name]
dpi_change_event = reduce(getattr, attr_parts, obj)
real_scale = main_window_probe.scale_factor
if real_scale == mock_scale:
pytest.skip("mock scale and real scale are the same")
scale_change = mock_scale / real_scale
content_size = main_window_probe.content_size

# Patch the internal dpi scale method
from toga_winforms.libs import shcore

GetScaleFactorForMonitor_original = getattr(shcore, "GetScaleFactorForMonitor")

def set_mock_dpi_scale(value):
def GetScaleFactorForMonitor_mock(hMonitor, pScale):
pScale.value = int(value * 100)

monkeypatch.setattr(
"toga_winforms.libs.shcore.GetScaleFactorForMonitor",
GetScaleFactorForMonitor_mock,
)

# Set initial DPI scale value
set_mock_dpi_scale(initial_dpi_scale)
dpi_change_event(None, None)
await main_window_probe.redraw(
f"Initial dpi scale is {initial_dpi_scale} before starting test"
)

# Store original window content
main_window_content_original = main_window.content
# Get the dpi change event from the string
dpi_change_event = getattr(main_window_probe.native, f"On{event_name}")

# Setup window for testing
# Include widgets which are sized in different ways, plus padding and fixed sizes in
# both dimensions.
main_window.content = toga.Box(
style=Pack(direction=flex_direction),
style=Pack(direction="row"),
children=[
toga.Box(style=Pack(flex=1)),
toga.Button(text="hello"),
toga.Label(text="toga"),
toga.Button(text="world"),
toga.Box(style=Pack(flex=1)),
toga.Label(
"fixed",
id="fixed",
style=Pack(background_color="yellow", padding_left=20, width=100),
),
toga.Label(
"minimal", # Shrink to fit content
id="minimal",
style=Pack(background_color="cyan", font_size=16),
),
toga.Label(
"flex",
id="flex",
style=Pack(background_color="pink", flex=1, padding_top=15, height=50),
),
],
)
await main_window_probe.redraw("main_window is ready for testing")

widget_dimension_to_compare = "width" if flex_direction == "row" else "height"
ids = ["fixed", "minimal", "flex"]
probes = {id: get_probe(main_window.widgets[id]) for id in ids}

# Store original widget dimension
original_widget_dimension = dict()
for widget in main_window.content.children:
widget_probe = get_probe(widget)
original_widget_dimension[widget] = getattr(
widget_probe, widget_dimension_to_compare
def get_metrics():
return (
{id: Position(probes[id].x, probes[id].y) for id in ids},
{id: Size(probes[id].width, probes[id].height) for id in ids},
{id: probes[id].font_size for id in ids},
)

# Set and Trigger dpi change event with the specified dpi scale
set_mock_dpi_scale(final_dpi_scale)
dpi_change_event(None, None)
await main_window_probe.redraw(
f"Triggered dpi change event with {final_dpi_scale} dpi scale"
)
positions, sizes, font_sizes = get_metrics()

# Check Widget size DPI scaling
for widget in main_window.content.children:
if isinstance(widget, toga.Box):
# Dimension of spacer boxes should decrease when dpi scale increases
getattr(
get_probe(widget), widget_dimension_to_compare
) < original_widget_dimension[widget]
else:
# Dimension of other widgets should increase when dpi scale increases
getattr(
get_probe(widget), widget_dimension_to_compare
) > original_widget_dimension[widget]
# Because of hinting, font size changes can have non-linear effects on pixel sizes.
approx_fixed = partial(pytest.approx, abs=1)
approx_font = partial(pytest.approx, rel=0.25)

# Restore original state
monkeypatch.setattr(
"toga_winforms.libs.shcore.GetScaleFactorForMonitor",
GetScaleFactorForMonitor_original,
)
dpi_change_event(None, None)
main_window.content.window = None
main_window.content = main_window_content_original
main_window.show()
await main_window_probe.redraw("Restored original state of main_window")
assert font_sizes["fixed"] == 9 # Default font size on Windows
assert positions["fixed"] == approx_fixed((20, 0))
assert sizes["fixed"].width == approx_fixed(100)

assert font_sizes["minimal"] == 16
assert positions["minimal"] == approx_fixed((120, 0))
assert sizes["minimal"].height == approx_font(sizes["fixed"].height * 16 / 9)

assert font_sizes["flex"] == 9
assert positions["flex"] == approx_fixed((120 + sizes["minimal"].width, 15))
assert sizes["flex"] == approx_fixed((content_size.width - positions["flex"].x, 50))

# Mock the function Toga uses to get the scale factor.
from toga_winforms.libs import shcore

def GetScaleFactorForMonitor_mock(hMonitor, pScale):
pScale.value = int(mock_scale * 100)

try:
GetScaleFactorForMonitor_original = shcore.GetScaleFactorForMonitor
shcore.GetScaleFactorForMonitor = GetScaleFactorForMonitor_mock

# Set and Trigger dpi change event with the specified dpi scale
dpi_change_event(EventArgs.Empty)
await main_window_probe.redraw(
f"Triggered dpi change event with {mock_scale} dpi scale"
)

# Check Widget size DPI scaling
positions_scaled, sizes_scaled, font_sizes_scaled = get_metrics()
for id in ids:
assert font_sizes_scaled[id] == approx_fixed(font_sizes[id] * scale_change)

assert positions_scaled["fixed"] == approx_fixed(Position(20, 0) * scale_change)
assert sizes_scaled["fixed"] == (
approx_fixed(100 * scale_change),
approx_font(sizes["fixed"].height * scale_change),
)

assert positions_scaled["minimal"] == approx_fixed(
Position(120, 0) * scale_change
)
assert sizes_scaled["minimal"] == approx_font(sizes["minimal"] * scale_change)

assert positions_scaled["flex"] == approx_fixed(
(
positions_scaled["minimal"].x + sizes_scaled["minimal"].width,
15 * scale_change,
)
)
assert sizes_scaled["flex"] == approx_fixed(
(
content_size.width - positions_scaled["flex"].x,
50 * scale_change,
)
)

finally:
# Restore original state
shcore.GetScaleFactorForMonitor = GetScaleFactorForMonitor_original
dpi_change_event(EventArgs.Empty)
await main_window_probe.redraw("Restored original state of main_window")
assert get_metrics() == (positions, sizes, font_sizes)


async def test_session_based_app(
Expand Down
9 changes: 5 additions & 4 deletions winforms/src/toga_winforms/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from toga.types import Position, Size

from .container import Container
from .fonts import DEFAULT_FONT
from .libs.wrapper import WeakrefCallable
from .screens import Screen as ScreenImpl
from .widgets.base import Scalable
Expand All @@ -28,7 +29,7 @@ def __init__(self, interface, title, position, size):
self._FormClosing_handler = WeakrefCallable(self.winforms_FormClosing)
self.native.FormClosing += self._FormClosing_handler
super().__init__(self.native)
self._dpi_scale = self._original_dpi_scale = self.get_current_screen().dpi_scale
self._dpi_scale = self.get_current_screen().dpi_scale

self.native.MinimizeBox = self.interface.minimizable
self.native.MaximizeBox = self.interface.resizable
Expand Down Expand Up @@ -57,7 +58,7 @@ def dpi_scale(self):
def scale_font(self, native_font):
return WinFont(
native_font.FontFamily,
native_font.Size * (self.dpi_scale / self._original_dpi_scale),
native_font.Size * self.dpi_scale,
native_font.Style,
)

Expand Down Expand Up @@ -301,7 +302,7 @@ def create_menus(self):

submenu.DropDownItems.Add(item)

self.original_menubar_font = menubar.Font
self.original_menubar_font = DEFAULT_FONT
self.resize_content()

def create_toolbar(self):
Expand Down Expand Up @@ -337,7 +338,7 @@ def create_toolbar(self):
item.Click += WeakrefCallable(cmd._impl.winforms_Click)
cmd._impl.native.append(item)
self.toolbar_native.Items.Add(item)
self.original_toolbar_font = self.toolbar_native.Font
self.original_toolbar_font = DEFAULT_FONT

elif self.toolbar_native:
self.native.Controls.Remove(self.toolbar_native)
Expand Down
9 changes: 6 additions & 3 deletions winforms/tests_backend/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ def assert_font_options(self, weight=NORMAL, style=NORMAL, variant=NORMAL):
else:
assert NORMAL == variant

@property
def font_size(self):
return round(self.font.SizeInPoints / self.scale_factor)

def assert_font_size(self, expected):
if expected == SYSTEM_DEFAULT_FONT_SIZE:
assert int(self.font.SizeInPoints) == 9
else:
assert int(self.font.SizeInPoints) == expected
expected = 9
assert self.font_size == expected

def assert_font_family(self, expected):
assert str(self.font.Name) == {
Expand Down
Loading

0 comments on commit 44f9124

Please sign in to comment.