Skip to content

Add role option to Switch, with checkbox and switch options - #4667

Open
760ceb3b9c0ba4872cadf3ce35a7a494 wants to merge 30 commits into
beeware:mainfrom
760ceb3b9c0ba4872cadf3ce35a7a494:toga-toggle
Open

Add role option to Switch, with checkbox and switch options#4667
760ceb3b9c0ba4872cadf3ce35a7a494 wants to merge 30 commits into
beeware:mainfrom
760ceb3b9c0ba4872cadf3ce35a7a494:toga-toggle

Conversation

@760ceb3b9c0ba4872cadf3ce35a7a494

@760ceb3b9c0ba4872cadf3ce35a7a494 760ceb3b9c0ba4872cadf3ce35a7a494 commented Aug 20, 2026

Copy link
Copy Markdown

this PR aims to get closer to the end-state described in #2225 (comment) by providing a role parameter to make a Switch display like a checkbox, a switch, or automatically based on platform defaults.

image
  • rename Switch to Toggle (with backwards compatibility)
  • add role parameter and a SwitchRole enum to choose appearance
  • add Cocoa implementation of all SwitchRoles
  • update documentation
  • update tests

later on: implement all SwitchRoles for other backends (when possible)

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

@freakboy3742 freakboy3742 left a comment

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.

Thanks for the PR!

On first inspection, this is definitely on the right track; there's a couple of details that stand out on an initial review:

  1. You've implemented both Switch And Checkbox as a single class... and then almost every method is implemented as if switch:... else: .... To me, that's calling out for an abstract base class with two concrete implementations.

  2. There's no testbed tests - as a result, there's now coverage gaps for macOS (which is showing in CI).

  3. We need to be careful about backwards compatibility. If we're deprecating Switch, it should be raising a deprecating warning on use; and the existing examples that use Switch should be updated.

    However, on reflection, I'm not necessarily sure that's what we should be doing. As @mhsmith flagged in the original discussion, changing the widget will be a lot of code churn, for not a lot of real gain - Toggle isn't an inherently better name; on the other hand, Switch:

    • is an accurate name on a lot of platforms,
    • is broadly HIG-appropriate when used as a single standalone widget
    • isn't wildly off base as a name for a checkbox
  4. You've missed the other "possibly" option - the major/minor role. type is always a bit of a weird argument to use in Python (because it's a builtin type); role has the potential for use in other contexts (e.g, toga.Button(role="Cancel"))

Comment thread cocoa/src/toga_cocoa/widgets/toggle.py Outdated
Comment on lines +63 to +64
def create(self):
if self.toggle_type == ToggleType.CHECKBOX:

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.

To me, this says it should be implemented as a subclass.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i switched to an abstract base class, with a function to initialize the correct subclass for the switch role. does that seem okay?

@corranwebster

corranwebster commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Thanks for the contribution!

A couple of quick thoughts about the process of making the change.

The first is that while you're still working on this, you might want to use a draft pull request until you think the code is ready for review.

The second thought is that if we remove the existing Switch widget entirely and suddenly, it will break a lot of existing code. There needs to be a transition period where people can use continue to use Switch with its existing interface, perhaps with a warning message that it's going away. An elegant way of doing this would be to write the new Toggle widget, and then make the Switch widget use the same implementation, just created with a particular set of arguments to produce the current behaviour.

Edit: looks like @freakboy3742 beat me to it.

@760ceb3b9c0ba4872cadf3ce35a7a494
760ceb3b9c0ba4872cadf3ce35a7a494 marked this pull request as draft August 21, 2026 15:22
@760ceb3b9c0ba4872cadf3ce35a7a494

Copy link
Copy Markdown
Author

The first is that while you're still working on this, you might want to use a draft pull request until you think the code is ready for review.

good point, changed it.

The second thought is that if we remove the existing Switch widget entirely and suddenly, it will break a lot of existing code.

That's not what this does! Switch still works.

@760ceb3b9c0ba4872cadf3ce35a7a494

760ceb3b9c0ba4872cadf3ce35a7a494 commented Aug 21, 2026

Copy link
Copy Markdown
Author

However, on reflection, I'm not necessarily sure that's what we should be doing. As @mhsmith flagged in the original discussion, changing the widget will be a lot of code churn, for not a lot of real gain - Toggle isn't an inherently better name;

i'm totally fine with the name remaining Switch. i'll defer to you on that one :)

In which case - let's keep it as Switch.

@corranwebster

Copy link
Copy Markdown
Contributor

That's not what this does! Switch still works.

Apologies - I missed that Switch was still there in the core.

@760ceb3b9c0ba4872cadf3ce35a7a494 760ceb3b9c0ba4872cadf3ce35a7a494 changed the title Replace Switch with Toggle, adding checkbox and switch options Add role option to Switch, with checkbox and switch options Aug 22, 2026
@760ceb3b9c0ba4872cadf3ce35a7a494
760ceb3b9c0ba4872cadf3ce35a7a494 marked this pull request as ready for review August 22, 2026 20:28

@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.

I'm not a core team member, but here are some concrete things that I personally think may be worth discussing. Below is all my personal opinion; take with a grain of salt.

Overall the implementation looks very clean though, I liked all the tricks you've used to abstract things.

Comment thread cocoa/src/toga_cocoa/widgets/switch.py Outdated
Comment thread cocoa/src/toga_cocoa/widgets/switch.py
Comment thread testbed/tests/widgets/test_switch.py
Comment thread core/src/toga/widgets/switch.py Outdated
Comment thread cocoa/src/toga_cocoa/widgets/switch.py Outdated
Comment on lines +210 to +214
def Switch(interface):
actual_role = determine_actual_toggle_role(interface._role)
if actual_role == SwitchRole.SWITCH:
return SwitchToggle(interface)
return CheckboxToggle(interface)

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 liked how pragmatic this is in not needing to change the other backends, but using a function disguised as a class is sort of uncommon... flagging this as such, but how or whether to refactor this is ultimately the core team's call.

(Same for SwitchProbe factory method)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yes, I agree, and I felt a little weird doing it , but I wasn't really sure how else to make this work, this being a "class" that conditionally uses one of two implementations based on a parameter. another option is to bring the distinction out of the cocoa backend and make core aware of switch versus checkbox stuff, but (as you note) that would require every current backend to be altered. SwitchProbe also working this way mirrors this decision.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

also, on some backends (such as I think UIKit with Mac Catalyst) checkbox/switch appearance is a parameter rather than a separate widget, which might suggest that it makes sense to keep the class separation out of core, as some backends may not want different classes here.

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... seems like you have a good point here. Some backends may not want to enforce this separation of different classes.

I'm not sure how to refactor this either, so let's address this only if the core team thinks it's a concern.

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 can think of three possible approaches that remove the need for the "class but a function":

  1. Register 5 different backend points. At present, we've got 1-1 correspondence between the frontend widgets and backend widgets. However, there's no reason why that has to be the case - we could define registration points of:
Switch = "toga_cocoa.widgets.switch:Switch"
Checkbox = "toga_cocoa.widgets.switch:Checkbox"
Switch_Major = "toga_cocoa.widgets.switch:Switch"
Switch_Minor = "toga_cocoa.widgets.switch:Checkbox"
Switch_Auto = "toga_cocoa.widgets.switch:Checkbox"

If the backend doesn't define Checkbox, Switch_Major, Switch_Minor, or Switch_Auto, the widget falls back to Switch. That doesn't require any changes to other backends.

In the Catalyst case where it's the same widget but with an option, it can either be one class whose construction is based on the role argument extracted from the interface; or two subclasses where the only difference is a single argument at construction.

  1. Expose determine_actual_toggle_role as part of the interface (although I'd probably use a name like switch_for_role:
switch_for_role = "toga_cocoa.widgets.switch:switch_for_role"

That way, we wouldn't need Switch_Major, Switch_Minor etc; you call the backend function to determine which switch widget is needed; if the function isn't defined, fall back to Switch.

  1. Expose Switch and Checkbox as explicit backend widgets, but have class properties on those classes that define the roles they can be used for.
class Switch(Toggle):
    roles = {SwitchRole.MAJOR}
    ...
class CheckBox(Toggle):
    roles = {SwitchRole.MINOR, SwitchRole.AUTO}

This has the advantage that the backend explicitly defines only widgets that exist; but provides enough metadata to determine when that widget is appropriate. The role lookup can be cached on the core class.


Of the three options, I think option 3 appeals to me the most. The one notable downside is that if a backend comes up with some new format for "switch" content, it's not something that backend can automatically accommodate. But we'd need to add a new constant value to allow that anyway; and new widgets don't get added that often.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

thanks for this response. just now I went with option 2 but i'm open to changing it again. it feels very clean, and i guess the core being neutral to SwitchRoles just appeals to me more than making it even a little bit explicit in the class names.

These override matching properties on the `style` argument.
:param kwargs: Initial style properties.
"""
self._role = role

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.

Raising for discussion: Should there be a public readonly property exposing the role?

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.

If we're capturing the parameter, we might as well expose it as a property.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done!

Comment thread cocoa/src/toga_cocoa/widgets/switch.py Outdated
Comment on lines +212 to +214
if actual_role == SwitchRole.SWITCH:
return SwitchToggle(interface)
return CheckboxToggle(interface)

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.

But also, this can be simplified a bit using inline if syntax (return SwitchToggle(interface) if actual_role == SwitchRole.SWITCH else CheckboxToggle(interface))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

that's certainly shorter, but I don't know if it's simpler!

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.

My personal opinion is that inline if/else is easier to read, but I think waiting for a core team member to answer here is the better option here.

Comment thread cocoa/tests_backend/widgets/switch.py Outdated

@johnzhou721 johnzhou721 Aug 22, 2026

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.

Not sure if showcasing both forms of toggles are needed (let's wait for core team response), but if it is, the screenshot example app needs to be updated to match the layout you've used here, so that those images can be reproduced later.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

ohh i see, i was not aware of the screenshot app!

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.

Yeah, I think we should leave it for now... there's advantages to showing one or both forms in the screenshot, so my personal inclination would be to wait for a core team member to respond at their convenienc here before we update the example app to match our final form of showcase.

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.

We should definitely update the example app before we land this - providing examples of each role in the screenshot.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i reverted the screenshot change and updated the switch_demo example to show off auto/major/minor roles.

Comment thread changes/4667.feature.md Outdated

@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.

I'm personally good with how this PR looks now, although I'm not affiliated with BeeWare. I did flag out some small things inline for the last pass, but these are all minor concerns. I'm personally not overly concerned about them, but I'm not part of the core team so it's at their discretion if they're actual concerns.

Also: From my read of the contribution guide, it's usually not customary to mark other reviewer's conversations as Resolved (https://toga.beeware.org/en/stable/how-to/contribute/next/pr-review/#work-through-requested-changes) here. Personally, all the comments you marked as resolved were indeed properly worked on (good work on this front!), so there's no issue this time, but since I also missed this guidance when I first started contributing, I just wanted to point you to the project's conventions.

@760ceb3b9c0ba4872cadf3ce35a7a494

Copy link
Copy Markdown
Author

thank you for the reminder, clearly I had missed that!!

@freakboy3742 freakboy3742 left a comment

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.

Nice work! The tests and widget implemenations all look good; the only part left is finessing the specifics of the "which widget?" logic, and a little more in the documentation and examples.

Comment thread cocoa/src/toga_cocoa/widgets/switch.py Outdated
return role


class BaseToggle(Widget, ABC):

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.

If we're not using Toggle as a public name, we can use Toggle as the base class, and then Switch and Checkbox as the concrete classes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

renamed!

Comment thread cocoa/src/toga_cocoa/widgets/switch.py Outdated
Comment on lines +210 to +214
def Switch(interface):
actual_role = determine_actual_toggle_role(interface._role)
if actual_role == SwitchRole.SWITCH:
return SwitchToggle(interface)
return CheckboxToggle(interface)

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 can think of three possible approaches that remove the need for the "class but a function":

  1. Register 5 different backend points. At present, we've got 1-1 correspondence between the frontend widgets and backend widgets. However, there's no reason why that has to be the case - we could define registration points of:
Switch = "toga_cocoa.widgets.switch:Switch"
Checkbox = "toga_cocoa.widgets.switch:Checkbox"
Switch_Major = "toga_cocoa.widgets.switch:Switch"
Switch_Minor = "toga_cocoa.widgets.switch:Checkbox"
Switch_Auto = "toga_cocoa.widgets.switch:Checkbox"

If the backend doesn't define Checkbox, Switch_Major, Switch_Minor, or Switch_Auto, the widget falls back to Switch. That doesn't require any changes to other backends.

In the Catalyst case where it's the same widget but with an option, it can either be one class whose construction is based on the role argument extracted from the interface; or two subclasses where the only difference is a single argument at construction.

  1. Expose determine_actual_toggle_role as part of the interface (although I'd probably use a name like switch_for_role:
switch_for_role = "toga_cocoa.widgets.switch:switch_for_role"

That way, we wouldn't need Switch_Major, Switch_Minor etc; you call the backend function to determine which switch widget is needed; if the function isn't defined, fall back to Switch.

  1. Expose Switch and Checkbox as explicit backend widgets, but have class properties on those classes that define the roles they can be used for.
class Switch(Toggle):
    roles = {SwitchRole.MAJOR}
    ...
class CheckBox(Toggle):
    roles = {SwitchRole.MINOR, SwitchRole.AUTO}

This has the advantage that the backend explicitly defines only widgets that exist; but provides enough metadata to determine when that widget is appropriate. The role lookup can be cached on the core class.


Of the three options, I think option 3 appeals to me the most. The one notable downside is that if a backend comes up with some new format for "switch" content, it's not something that backend can automatically accommodate. But we'd need to add a new constant value to allow that anyway; and new widgets don't get added that often.

return self.impl.label_native.font


# noinspection PyPep8Naming

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.

What is this annotation for?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

it's for the capitalized function name, although i realize it might not be required as i omitted it in the other function and nothing complained.

Comment thread cocoa/tests_backend/widgets/switch.py Outdated
xfail("Can't get/set the text color of a switch on macOS")


class SwitchSwitchProbe(CheckboxSwitchProbe):

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.

SwitchSwitch is a little... convoluted. I'd be inclined to name these _CheckboxProbe and _SwitchProbe.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

renamed!

These override matching properties on the `style` argument.
:param kwargs: Initial style properties.
"""
self._role = role

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.

If we're capturing the parameter, we might as well expose it as a property.

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.

We should definitely update the example app before we land this - providing examples of each role in the screenshot.

Comment thread core/src/toga/widgets/switch.py Outdated
Comment thread docs/en/reference/api/widgets/switch.md Outdated

- The button and the label are considered a single widget for layout purposes.
- The visual appearance of a Switch is not guaranteed. On some platforms, it will render as a checkbox. On others, it will render as a physical "switch" whose position (and color) indicates if the switch is active. When rendered as a checkbox, the label will appear to the right of the checkbox. When rendered as a switch, the label will be left-aligned, and the switch will be right-aligned.
- The `role` parameter, which determines the visual appearance of a switch, is currently only supported on macOS. On other platforms, the appearance of a Switch is not guaranteed. On some platforms, it will render as a checkbox. On others, it will render as a physical "switch" whose position (and color) indicates if the switch is active. When rendered as a checkbox, the label will appear to the right of the checkbox. When rendered as a switch, the label will be left-aligned, and the switch will be right-aligned.

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.

Historically, a platform note was appropriate to explain platform discrepancies; I think this now needs to graduate to full documentation, giving a full explanation of what the "role" is for, and advising the use of MAJOR and MINOR over "just a switch".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i just expanded on this in the docs, which should make things more clear.

@760ceb3b9c0ba4872cadf3ce35a7a494

Copy link
Copy Markdown
Author

ok, the last round of comments should all be addressed now :>

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.

4 participants