Skip to content

WinUI 3 foundational implementation - #4600

Open
Oliver-Leigh wants to merge 111 commits into
beeware:mainfrom
Oliver-Leigh:winui-3
Open

WinUI 3 foundational implementation#4600
Oliver-Leigh wants to merge 111 commits into
beeware:mainfrom
Oliver-Leigh:winui-3

Conversation

@Oliver-Leigh

Copy link
Copy Markdown
Contributor

This PR adds the foundation of a WinUI 3 backend for the Windows platform. The backend is based on the win32more Microsoft App SDK projection. The currently implemented entry points are:

  • App
  • Command
  • Font
  • Icon
  • Paths
  • resources
  • MenuStatusIcon
  • SimpleStatusIcon
  • StatusIconSet
  • MainWindow
  • Window
  • Box
  • Button
  • Label
button_example_light button_example_dark

Note: Documentation is missing

Refs #2574

PR Checklist:

  • I will abide by the BeeWare Code of Conduct
  • I have read and have followed the CONTRIBUTING.md file
  • This PR was generated or assisted using an AI tool

Assisted-by:

@Oliver-Leigh
Oliver-Leigh marked this pull request as ready for review July 30, 2026 14:41
@Oliver-Leigh

Copy link
Copy Markdown
Contributor Author

Additional notes:

  • To run this you may need to install the Windows App SDK v2.3.1 from here. The Windows App SDK is installed by default on Windows 11 since some apps use it (e.g. Notepad), but it may be the wrong version. (In the future this can be redistributed in a packaged app.)
  • I haven't tested this on Windows 10.
  • When Native menu items on WinForms are never deleted #4580 is completed, this PR should be updated to align with the other backends.

Testbed notes:

  • There is some CI instability, but the x86_64 CI has run 7 times in a row without issue. I've put some debug code into the testbed to try to find the issue if it surfaces again. It records any native unhandled exceptions. It might be nice to put something like this into the testbed permanently, but I'm not sure on the shape that it should take.
  • The testbed is working on the GitHub ARM64 runner, but not for everything. It seems to not allow input focus, so I've put in some ad-hoc skips and allowed incomplete coverage. This better than the WinForms situation, but it will probably need to be reworked.

@johnzhou721

Copy link
Copy Markdown
Contributor
  • The testbed is working on the GitHub ARM64 runner, but not for everything. It seems to not allow input focus, so I've put in some ad-hoc skips and allowed incomplete coverage. This better than the WinForms situation, but it will probably need to be reworked.

WinForms situation is documented on the description on #4331 and is also not being able to accept focus and not being able to test dialogs (which aren't implemented in this PR). So not saying this should be crammed into this PR, but if ad hoc skips are acceptable then we might be able to run all tests on Windows ARM64 as well.

@Oliver-Leigh

Copy link
Copy Markdown
Contributor Author

@johnzhou721 Thanks for adding the link with the extra context.

I’ve used the skips to try and get as much coverage as possible, mainly because this is a new backend and I think we need all the coverage we can get. To apply these skips more broadly (e.g. for WinForms), I think some more structured approach is needed - one that fits better with the existing structure. On the other hand, it may not be worth spending the time on something which could be resolved soon.

On a side note, has anyone tried the new windows-11-vs2026-arm runner? It might be worth trialing that runner before proceeding further.

@johnzhou721 johnzhou721 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For en ren implementasjonen :)

I took a morning to go through this code and flag some small things inline. A lot of this just comes down to personal preference, but I did catch some things that can be fixed in regards to DPI scaling.

There's a lot of complexity in terms of how staging areas are handled, but I think it's very well-documented and understandable.

There's some minor grammatical issues in the comments, but I'm not flagging them as they're mostly still understandable, and to avoid coming off as overly nitpicky.

Overall nice work though! I'm reviewing solely because I'm really excited about WinUI3 backend coming alive, not because I'm in any authority to definitively suggest changes to anything. Feel free to discuss anything I flagged directly inline.

P.S. Haven't tested yet, this is all from a static analysis of the code.

Comment thread testbed/tests/app/test_screens.py Outdated

async def test_as_image(app):
"""A screen can be captured as an image"""
skip_on_backends("toga_winui3", reason="Screen.get_image_data is no implemented.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can use a pytest.skip in the probe method ScreenProbe.get_screenshot instead of doing this skip in the test (as we don't like to put platform specfics in testbed; exceptions exist such as skip_on_backends in each widget file.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've done this now. The other benefit is that ScreenProbe gets constructed now too.

on_alt_release_handler,
on_alt_drag_handler,
):
skip_on_backends("toga_winui3")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Each module under this canvas folder have a skip_on_backends line (in test_deprecated_code.py and test_canvas.py) with toga_textual listed, so I think it'd be more uniform if we can put this platform skip on these lines as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I talk about this in #4600 (comment).

Comment on lines 39 to 43
skip_on_backends(
"toga_textual",
reason="Canvas is not implemented on Textual.",
allow_module_level=True,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See below.


@pytest.fixture
async def widget():
skip_on_backends("toga_winui3")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this can be put on line 15 with the toga_textual.

Some widgets use convention for skip_on_backends in the widget fixture, in that case I think it's fine to add to that list but I think it helps to consolidate all the skip_on_backend calls in one place (either module level or widget fixture) when possible. Whether to do this now and where to move into is up to the core team.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see you point, but I'm not sure which is the preferred solution. I think something to take into consideration is whether the widget/feature is expected to be implemented at all. Perhaps @freakboy3742 can weigh in on this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see you point, but I'm not sure which is the preferred solution. I think something to take into consideration is whether the widget/feature is expected to be implemented at all. Perhaps @freakboy3742 can weigh in on this?

If it’s expected to be implemented a skip is used, if it’s not then it’s an xfail (xfail_on_platforms). IiRC that’s how we differentiate expectation of implementation, using the type of the skip rather than where it’s placed in the code. I think there’s a page in the contribution guide about this, but I can’t find where right now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree the two should be consolidated - there should be a single consistent location for declaring when a the testbed won't test a widget. The module level skip will be faster to run (as it won't construct and fail every test in the module), and it's also convenient to have the first actual code in a module be "... and here's where it doesn't apply". It also avoids the need to update the cleanup test as well.

The description about the use of xfail vs skip is in the testbed topic guide.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great we got this sorted. I'll update all the missing WinUI 3 widgets to module level skips.

Comment thread testbed/tests/window/test_window.py Outdated
)
async def test_secondary_window_toolbar(app, second_window, second_window_probe):
"""A toolbar can be added to a secondary window"""
skip_on_backends("toga_winui3")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can make has_toolbar use pytest.skip in WinUI3 tests here instead of a platform skip.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed as suggested.

Comment thread winui3/src/toga_winui3/window.py Outdated
@property
def min_size(self):
"""The minimum size of the window in physical pixels (device pixels)."""
css_to_physical = self.get_current_screen().css_to_physical

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm... on some systems there's a slight timing difference between moving to a screen and using that screen's DPI.

I think we should directly use GetDpiForWindow here instead of querying the screen's properties for all DPI scale cases.

#4577

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated!

Comment thread winui3/src/toga_winui3/window.py Outdated
Comment on lines +310 to +312
# Window and client sizes are in physical pixels.
window_size = self.native.AppWindow.Size
client_size = self.native.AppWindow.ClientSize

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This approach proved to be sort of unreliable when doing DPI scaling on WinForms. I suggest AdjustWindowRectExForDpi which is also more direct.

#4577

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated!

Comment on lines +360 to +371
def get_position(self) -> Position:
position = self.native.AppWindow.Position
physical_to_css = App.app._impl.get_primary_screen().physical_to_css

return Position(physical_to_css(position.X), physical_to_css(position.Y))

def set_position(self, position: PositionT):
css_to_physical = App.app._impl.get_primary_screen().css_to_physical

self.native.AppWindow.Move(
PointInt32(css_to_physical(position.x), css_to_physical(position.y))
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here with obtaining DPI directly from GetDpiFromWindow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated!

if not self.interface.toolbar:
return

self.interface.factory.not_implemented("Window.create_toolbars")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
self.interface.factory.not_implemented("Window.create_toolbars")
self.interface.factory.not_implemented("window toolbars")

Comment on lines +36 to +40
parent_1.Name = "Parent Name"
assert parent_1.Name == parent_2.Name == "Parent Name"

parent_2.Name = "New Parent Name"
assert parent_1.Name == parent_2.Name == "New Parent Name"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suggest perhaps using a uuid here, because this is called on many different widgets, all of which may have different parent containers.

@johnzhou721

Copy link
Copy Markdown
Contributor

I’ve used the skips to try and get as much coverage as possible, mainly because this is a new backend and I think we need all the coverage we can get. To apply these skips more broadly (e.g. for WinForms), I think some more structured approach is needed - one that fits better with the existing structure. On the other hand, it may not be worth spending the time on something which could be resolved soon.

Agreed about this.

On a side note, has anyone tried the new windows-11-vs2026-arm runner? It might be worth trialing that runner before proceeding further.

No I have not. I think we can put it in this PR to see if the ARM64 coverage increases (since the handling inline is try-catch not direct skip in arm64 ci) and if so remove all the try-catches.

@Oliver-Leigh

Copy link
Copy Markdown
Contributor Author

Thanks for the review @johnzhou721! The DPI suggestions were especially helpful.

No I have not. I think we can put it in this PR to see if the ARM64 coverage increases (since the handling inline is try-catch not direct skip in arm64 ci) and if so remove all the try-catches.

Unfortunately it didn't work.

@johnzhou721

Copy link
Copy Markdown
Contributor

Thanks for the review @johnzhou721! The DPI suggestions were especially helpful.

No worries! I don't have much time right now, but if you want to test multiple monitors setup, you can use this project: https://github.com/VirtualDrivers/Virtual-Display-Driver -- note that it may require test signing, though. The console app in that repo isn't really reliable, so installing the drivers through Device Manager may be a better approach there.

(Hopefully my line of Norwegian at the start of my review wasn't too awkward...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants