Skip to content

Conversation

dhardy
Copy link
Contributor

@dhardy dhardy commented Aug 7, 2025

  • Tested on all platforms changed:
    • win32
    • macos
    • wayland
    • x11
    • web
  • Added an entry to the changelog module if knowledge of this change could be valuable to users
  • Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior
  • Created or updated an example program if it would help users understand this functionality

It turns out that at least Wayland, X11 and macOS support more than 5 mouse buttons. Previously, these all used incompatible Mouse::Other(_) codes, while Mouse::Other was also used for potential errors.

Now, MouseButton is a wrapper around u8 with portable button mappings, modelled on AppKit codes.

Tested:

  • X11 supports codes up to 255, but since 0 is unused and 4..=7 are wheel codes, we get 250 usable codes.
  • Wayland names eight mouse buttons (see here), but there's a catch: most apps use BTN_SIDE and BTN_EXTRA for back/forward, not BTN_BACK / BTN_FORWARD. My mice report up to button 10 (code 0x1A0); since the next named code is BTN_JOYSTICK = 0x120 I hypothesize that 16 buttons are usable.
  • Win32, but this platform does not support more than 5 buttons.

Alternative

We could keep using an enum and name more buttons, but how many would be named? Note that while we can now report consistent numbers for more than 5 buttons across platforms, these buttons don't have consistent names.

Further work

ButtonId could obviously be a MouseButton in some cases, a ButtonSource::Unknown(_) in others. But I don't want to replace it with ButtonSource since that has a lot of extra data in the Touch variant.

@nicoburns
Copy link
Contributor

Prior art: the ui-events crate uses an enum with 32 variants. https://docs.rs/ui-events/latest/ui_events/pointer/enum.PointerButton.html

@kchibisov
Copy link
Member

Wayland names eight mouse buttons (see here), but there's a catch: most apps use BTN_SIDE and BTN_EXTRA for back/forward, not BTN_BACK / BTN_FORWARD. My mice report up to button 10 (code 0x1A0); since the next named code is BTN_JOYSTICK = 0x120 I hypothesize that 16 buttons are usable.

This is not really true if we're speaking from the point of Wayland protocol. https://wayland.app/protocols/wayland#wl_pointer:event:button:arg:button . I'd not tie here to kernel, but rather what written in the spec.

Honestly speaking, I feel that the proposed design is better, since it tends to solve the issue of adding a button, assuming that we don't mess around with undefined button values.

@dhardy
Copy link
Contributor Author

dhardy commented Aug 8, 2025

Quoting the Wayland protocol docs:

The button is a button code as defined in the Linux kernel's linux/input-event-codes.h header file, e.g. BTN_LEFT.

Any 16-bit button code value is reserved for future additions to the kernel's event code list. All other button codes above 0xFFFF are currently undefined but may be used in future versions of this protocol.

I don't feel that this is very useful:
Firstly, because we can receive unspecified codes in practice, as I noted above. (What happens when mice have more than 16 buttons I don't know.)
Secondly, because names like BTN_SIDE, BTN_EXTRA and BTN_TASK do not specify an action, and are therefore no more useful than numeric enumeration in practice.
And thirdly, because this slots into a world with many existing apps whose transition to Wayland apparently missed the memo that they should now use BTN_FORWARD and BTN_BACK for forward/back; of the apps I tested, I found Firefox supported BTN_FORWARD (but not BTN_BACK) while no other app appears to respond to these keys; meanwhile everything responds to BTN_SIDE and BTN_EXTRA as if these were back/forward buttons.

The existing approach in winit (map both pairs of buttons to back/forward) may be the most compatible but it also prevents other usages of these buttons.

Prior art: the ui-events crate uses an enum with 32 variants.

That looks like it was written for a state-map and based on Windows button naming? We could use a 32-variant enum, but I'd like (1) to use Back, Forward names at least, (2) I'm unsure if the name Erase is appropriate, (3) be able to convert to numeric codes in the range 0..=31, not powers of 2.


Another approach I would consider is to report both a named enumeration (the existing enum MouseButton minus the Unknown variant, instead returning None in this case) and a numeric code (similar to the struct MouseButton in this PR).

Or, possibly, drop the last two commits from this PR: in this case MouseButton::Unknown at least has standardised mappings across platforms and unknown codes are reported as ButtonSource::Unknown instead.

My expectations on the API are essentially:

  • Be able to extract a number in the range 0..32 (or 0..255) representing the button press in a cross-platform standardised order. Ideally this should be compatible with the button number reported by AppKit and the number reported by libinput debug-events (after subtracting 272), though I haven't tested that these are consistent.
  • Have a defined interpretation where available. This might need to be device-specific, for example the "erase" button appears to be specific to pens and tablets.

@kchibisov
Copy link
Member

The thing that should be preserved IMO, ability to bind unknown codes by e.g. let user just press the button. So you can not really recognize, but you can kind of identify, so we should just never throw things away and be able to distinguish them. Which is still the thing with your patch from what I can see, so it's fine.

Just to make it clear, is the point to kind of normalize buttons so they can be kind of used in cross platform manner, or at least make them more likely to be usable? Like it certainly can not work from(since you can not really guarantee that people on every platform will do things exactly the same).

@kchibisov
Copy link
Member

I general, +1 on this, I'd say. But would need to hear from other maintainers @madsmtm @daxpedda

Copy link
Contributor Author

@dhardy dhardy left a comment

Choose a reason for hiding this comment

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

ability to bind unknown codes

From what I could tell most of these "unknown codes" should never be encountered in practice, though it's hard to rule out that ever happening.

is the point to kind of normalize buttons so they can be kind of used in cross platform manner

This + more precisely specify which button codes are likely to be seen in practice. I.e. it is now almost guaranteed that any observable mouse button code will fit in a u8 and probably be <32.

Comment on lines 530 to 532
ButtonSource::Mouse(mouse) => mouse,
ButtonSource::Touch { .. } => MouseButton::Left,
ButtonSource::Unknown(button) => match button {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
3 => MouseButton::Back,
4 => MouseButton::Forward,
_ => MouseButton::Other(button),
},
ButtonSource::Touch { .. } => MouseButton::LEFT,
ButtonSource::Unknown(_) => MouseButton::UNKNOWN,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The old mapping of Unknown codes made no sense since values were entirely unspecified.

However, there were several things mapping to Unknown(0); that might need changing.

Comment on lines 63 to 72
match button.0 {
0 => MB::LEFT.into(),
1 => MB::MIDDLE.into(),
2 => MB::RIGHT.into(),
3 => MB::BACK.into(),
4 => MB::FORWARD.into(),
// Codes above 4 are not observed on Firefox or Chromium. 5 is defined as an eraser,
// which is not a mouse button. No other codes are defined.
i => ButtonSource::Unknown(i),
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The "eraser" button could map to ButtonSource::Unknown on web, except that this code path shouldn't be used for non-mouse input. In my testing (using web forms not winit) I never saw any other codes output. So I don't think web platforms can produce any Unknown output codes in practice, though that could conceivably change in the future.

Copy link
Member

Choose a reason for hiding this comment

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

eraser button is a tablet button, so it shouldn't be here in the first place, I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's part of "the button property" spec:

https://www.w3.org/TR/pointerevents3/#the-button-property

Copy link
Member

Choose a reason for hiding this comment

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

But it says pen eraser button, so I'd assume that it's pen and will go there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, so I don't think it would occur here.

Comment on lines 1756 to 1757
// 1 is defined as back, 2 as forward; other codes are unexpected.
button: MouseButton(xbutton as u8 + MouseButton::BACK.0 - 1).into(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's well documented exactly which codes are produced here, and my testing agreed: this code should only ever yield MouseButton::BACK and FORWARD.

Comment on lines 409 to 415
if (BTN_MOUSE..BTN_JOYSTICK).contains(&button) {
// Mapping orders match
MouseButton((button - BTN_MOUSE) as u8).into()
} else {
ButtonSource::Unknown(button as u16)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This goes beyond the Wayland specification. I haven't seen any evidence that codes outside this range would be produced from mouse input, but maybe if a mouse has more than 16 buttons? Maybe there is some way buttons on a mouse could be mapped to other codes?

If anyone does have evidence that Wayland can produce codes outside this range (0x110..=0x11F) I'd like to see it.

Copy link
Member

Choose a reason for hiding this comment

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

I mean, it's possible, I guess, but not right now. Like we can use u16 for button just in case, since it doesn't really matter, I guess, just so we don't have a breaking change in the future if the problem appears?

Copy link
Contributor Author

@dhardy dhardy Aug 14, 2025

Choose a reason for hiding this comment

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

I put this in ButtonSource::Unknown instead of within MouseButton for a reason. Allowing the latter to contain undocumented u16 codes defeats that reason.

I would prefer to revisit this when we have evidence of such values, then map those to values within MouseButton. I view changes to Unknown codes as non-breaking (well, maybe not appropriate for patch releases), while changes to MouseButton codes would be breaking changes.

Copy link
Member

Choose a reason for hiding this comment

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

I mean, we can generally just map to u8 one away or another anyway, so don't think it'll be a problem tbf.

@kchibisov kchibisov added the C - nominated Nominated for discussion in the next meeting label Aug 28, 2025
@madsmtm madsmtm removed the C - nominated Nominated for discussion in the next meeting label Aug 29, 2025
@madsmtm
Copy link
Member

madsmtm commented Aug 29, 2025

We discussed this at our meeting yesterday, and while we can see the technical point that there might be unknown mouse buttons, it is unclear that this is actually a problem in practice? Are there variants that we'd want to expose, that we don't expose yet?

The unfortunate effect of this is that it makes things like rust-analyzer less able to fill out variants when matching on the mouse button; structs with consts are generally less supported than true enums. If this were not the case, such as if RFC 3803 was implemented, then this would definitely be a no-brainer.

(That said, there are a few cleanups in this PR that would be worthwhile to land anyhow).

@madsmtm madsmtm added the S - api Design and usability label Aug 29, 2025
@dhardy
Copy link
Contributor Author

dhardy commented Aug 30, 2025

We discussed this at our meeting yesterday, and while we can see the technical point that there might be unknown mouse buttons, it is unclear that this is actually a problem in practice? Are there variants that we'd want to expose, that we don't expose yet?

Whether the buttons should be named or just numbered I don't know, but I'd like to see support for at least a dozen buttons; maybe 32 like MacOS.

The unfortunate effect of this is that it makes things like rust-analyzer less able to fill out variants when matching on the mouse button; structs with consts are generally less supported than true enums. If this were not the case, such as if RFC 3803 was implemented, then this would definitely be a no-brainer.

Personally I'd like to be able to pull out a small enum or number. E.g.:

enum Button {
    Primary,
    Secondary,
    Middle,
    Back,
    Forward,
    Button6,
    // ...
    Button31,
}

would be fine. I don't like the existing MouseButton because it's harder (requires a big match) to convert that to something like Result<u8, u16> if one only cares about known buttons.

(That said, there are a few cleanups in this PR that would be worthwhile to land anyhow).

I'll try to find time this week. Still not 100% sure of the direction; should I use an enum like Button above?

@madsmtm
Copy link
Member

madsmtm commented Sep 4, 2025

I would prefer the 32-variant enum you propose, yes.

@kchibisov
Copy link
Member

Just in addition, we can also number the enum fields, like Primary = 1, etc and have a conversion to u8 if one wants it.

@dhardy dhardy force-pushed the push-oxmvmpxmqlqx branch 3 times, most recently from 8f13667 to cffd5a0 Compare September 8, 2025 14:15
Copy link
Contributor Author

@dhardy dhardy left a comment

Choose a reason for hiding this comment

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

Rebased and updated.

Comment on lines -524 to +527
pub fn mouse_button(self) -> MouseButton {
pub fn mouse_button(self) -> Option<MouseButton> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ButtonSource::mouse_button now returns None on ButtonSource::Unknown (no other reasonable option).

Comment on lines +1108 to +1110
impl MouseButton {
/// Construct from a `u8` if within the range `0..=31`
pub fn try_from_u8(b: u8) -> Option<MouseButton> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

An alternative would be to use the num_enum crate, I guess. I didn't use TryFrom since that returns a Result.

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

Labels

S - api Design and usability

Development

Successfully merging this pull request may close these issues.

4 participants