4
41
Fork
You've already forked tinyrwm
16

Rust implementation #2

Manually merged
ifreund merged 21 commits from jandrews271/tinyrwm:rust into main 2026年03月12日 10:54:10 +01:00
Contributor
Copy link

I hope unsolicited PRs aren't unwelcome!

Where possible I've aimed for simple/idiomatic Rust even if a more sophisticated approach would be more efficient. I don't think any of the inefficiencies should matter much for realistic numbers of windows/seats/outputs.

I'm very new to Wayland development, so I assume there will be some mistakes! Feedback welcome!

I hope unsolicited PRs aren't unwelcome! Where possible I've aimed for simple/idiomatic Rust even if a more sophisticated approach would be more efficient. I don't think any of the inefficiencies should matter much for realistic numbers of windows/seats/outputs. I'm very new to Wayland development, so I assume there will be some mistakes! Feedback welcome!

I hope unsolicited PRs aren't unwelcome!

As it states in the readme, contributions adding examples in new languages are very welcome!

Where possible I've aimed for simple/idiomatic Rust even if a more sophisticated approach would be more efficient. I don't think any of the inefficiencies should matter much for realistic numbers of windows/seats/outputs.

That sounds like a reasonable approach to me. My goal is for the the tinyrwm examples to be as small and straightforward as possible :)

> I hope unsolicited PRs aren't unwelcome! As it states in the readme, contributions adding examples in new languages are very welcome! > Where possible I've aimed for simple/idiomatic Rust even if a more sophisticated approach would be more efficient. I don't think any of the inefficiencies should matter much for realistic numbers of windows/seats/outputs. That sounds like a reasonable approach to me. My goal is for the the tinyrwm examples to be as small and straightforward as possible :)
ifreund left a comment
Copy link

Here's an initial round of review :)

Here's an initial round of review :)
rust/src/main.rs Outdated
@ -0,0 +84,4 @@
outputs: HashMap<ObjectId,Output>,
seats: HashMap<ObjectId,Seat>,
xkb_bindings: HashMap<ObjectId,XkbBinding>,
pointer_bindings: HashMap<ObjectId,PointerBinding>,
Owner
Copy link

Is it too awkward to store the xkb_bindings and pointer_bindings in the Seat struct instead? Or is there some other reason these are global state currently?

Is it too awkward to store the xkb_bindings and pointer_bindings in the Seat struct instead? Or is there some other reason these are global state currently?
Author
Contributor
Copy link

It's a little awkward, but we could do it.

The issue is that on binding events we need to lookup the binding using the proxy id without having the seat itself. There are two alternatives I can think of:

  1. Keep the bindings on the seats, and do a linear scan of all seats, or
  2. Use user data to pass the seat with the binding (like the C implementation does)

(1) would work fine, but if we're always accessing the bindings from the window manager, there's not much point to putting it on the seat

(2) is a little awkward in Rust because of the ownership model. We'd need to use something like an Rc<Refcell<Seat>>. I almost did this, but felt like it would add complexity for not much advantage.

I can see the argument for (2). On the one hand it makes the code a little harder to follow, which isn't ideal for a simple example. On the other hand, it would show a Rust pattern for using user data, which might be good to have in the example.

Let me know what you think makes sense. I honestly went back and forth on this question half a dozen times before deciding to just do the easier version.

It's a little awkward, but we could do it. The issue is that on binding events we need to lookup the binding using the proxy id without having the seat itself. There are two alternatives I can think of: 1. Keep the bindings on the seats, and do a linear scan of all seats, or 2. Use user data to pass the seat with the binding (like the C implementation does) (1) would work fine, but if we're always accessing the bindings from the window manager, there's not much point to putting it on the seat (2) is a little awkward in Rust because of the ownership model. We'd need to use something like an `Rc<Refcell<Seat>>`. I almost did this, but felt like it would add complexity for not much advantage. I can see the argument for (2). On the one hand it makes the code a little harder to follow, which isn't ideal for a simple example. On the other hand, it would show a Rust pattern for using user data, which might be good to have in the example. Let me know what you think makes sense. I honestly went back and forth on this question half a dozen times before deciding to just do the easier version.
Owner
Copy link

I see, that is indeed more awkward than I realized.

What if we set the user data of the bindings to the seat's ObjectId rather than a Rc/RefCell wrapped direct reference to the seat?

If that's also too awkward I'm fine with keeping the status quo.

I see, that is indeed more awkward than I realized. What if we set the user data of the bindings to the seat's `ObjectId` rather than a Rc/RefCell wrapped direct reference to the seat? If that's also too awkward I'm fine with keeping the status quo.
Author
Contributor
Copy link

Passing the ObjectId is actually perfect. This is a huge improvement, and I think the pattern of passing around ObjectIds as user data is probably the right default for Rust since it bypasses ownership issues neatly, so having that in the example is great.

Passing the `ObjectId` is actually perfect. This is a huge improvement, and I think the pattern of passing around `ObjectId`s as user data is probably the right default for Rust since it bypasses ownership issues neatly, so having that in the example is great.
Owner
Copy link

Perfect :)

Perfect :)
ifreund marked this conversation as resolved
rust/src/main.rs Outdated
@ -0,0 +117,4 @@
hovered: Option<RiverWindowV1>,
interacted: Option<RiverWindowV1>,
pending_action: Action,
op: SeatOp,
Owner
Copy link

The Go implementation of tinyrwm uses an interface for seat operations and encapsulates the operation-specific state, which I think worked out quite nicely.

Perhaps it would be more idiomatic Rust to have a SeatOp trait with implementations for move and resize operations?

The Go implementation of tinyrwm uses an interface for seat operations and encapsulates the operation-specific state, which I think worked out quite nicely. Perhaps it would be more idiomatic Rust to have a `SeatOp` trait with implementations for move and resize operations?
Author
Contributor
Copy link

Updated! I didn't follow the Go implementation exactly. For Rust at least, keeping the proxy in the enum with the rest of the operation-specific state is cleaner, and it didn't seem worth having a separate struct just for op_dx and op_dy.

There is an argument for putting op_dx and op_dy into the enum too. It would require updating the OpDelta handler to use a match statement and handle move and resize separately. Arguably that's more correct since in principle different operations might want to do different things on an OpDelta event, but it does add a bunch of lines of code for the same behavior.

I'm on the fence, so let me know if you have a preference!

Updated! I didn't follow the Go implementation exactly. For Rust at least, keeping the proxy in the enum with the rest of the operation-specific state is cleaner, and it didn't seem worth having a separate struct just for `op_dx` and `op_dy`. There is an argument for putting `op_dx` and `op_dy` into the enum too. It would require updating the `OpDelta` handler to use a `match` statement and handle move and resize separately. Arguably that's more correct since in principle different operations might want to do different things on an `OpDelta` event, but it does add a bunch of lines of code for the same behavior. I'm on the fence, so let me know if you have a preference!
Owner
Copy link

I think this is great as is, I don't think putting op_dx and op_dy into the enum would be an improvement :)

I think this is great as is, I don't think putting `op_dx` and `op_dy` into the enum would be an improvement :)
ifreund marked this conversation as resolved
rust/src/main.rs Outdated
@ -0,0 +275,4 @@
ifseat.focused.as_ref()==Some(&window.proxy){
seat.focused=None;
}
ifletSeatOp::Move{..}|SeatOp::Resize{..}=seat.op{
Owner
Copy link

This should only end the seat operation if the closed window is the target.

This should only end the seat operation if the closed window is the target.
Author
Contributor
Copy link

Whoops! Should be fixed now.

Whoops! Should be fixed now.
ifreund marked this conversation as resolved
rust/src/main.rs Outdated
@ -0,0 +288,4 @@
}
fn remove_seats(&mutself){
letremoved_ids: HashSet<_>=self
Owner
Copy link

This removed_ids hash set is unnecessary. I think it would be simpler to nest the xkb_bindings.retain() and pointer_bindings.retain() calls in the seats.retain() call.

This `removed_ids` hash set is unnecessary. I think it would be simpler to nest the `xkb_bindings.retain()` and `pointer_bindings.retain()` calls in the `seats.retain()` call.
Author
Contributor
Copy link

This should be fixed with the bindings -> seat refactor. I originally had this in place to deal with some borrow checker complaints, but the new data model fixed that entirely.

This should be fixed with the bindings -> seat refactor. I originally had this in place to deal with some borrow checker complaints, but the new data model fixed that entirely.
ifreund marked this conversation as resolved
rust/src/main.rs Outdated
@ -0,0 +354,4 @@
forwindowinself.windows.iter_mut(){
ifletSome(seat_proxy)=window.pointer_move_requested.take(){
letseat=find_or_log!(self.seats,seat_proxy.id(),"seat");
seat.interacted=Some(window.proxy.clone());
Owner
Copy link

Interesting, this differs from the other tinyrwm implementations which call seat.focus() directly here. I do kindof like this though...

Interesting, this differs from the other tinyrwm implementations which call `seat.focus()` directly here. I do kindof like this though...
Author
Contributor
Copy link

So, this is definitely one source of differences from the C implementation.

In the C implementation, focusing each window in turn means if there are multiple seats doing simultaneous operations, we'll shuffle around the window list for each one, keeping a record of all that. This implementation only reorders based on the last operation, so the order of windows might be different.

Also, if somehow we've got a seat operation going on, but seat.interacted points at a different window (maybe multiple WindowInteraction events in the same manage cycle?), then the C implementation will prefer whatever last interaction happened, but this version will prefer the last move or resize.

I wasn't really sure how specific the tinyrwm focus spec is. I like the simplicity of this code, but if we want identical focus behavior, this definitely isn't it!

Let me know what you prefer on this front. Implementing an exact match for the C behavior will definitely be a little uglier because the borrow checker doesn't like me passing a mutable reference to self.windows to seat.pointer_move() while I'm iterating over the windows, so that's what I was trying to avoid, but maybe a little ugliness is the right approach here.

So, this is definitely one source of differences from the C implementation. In the C implementation, focusing each window in turn means if there are multiple seats doing simultaneous operations, we'll shuffle around the window list for each one, keeping a record of all that. This implementation only reorders based on the last operation, so the order of windows might be different. Also, if somehow we've got a seat operation going on, but `seat.interacted` points at a different window (maybe multiple `WindowInteraction` events in the same manage cycle?), then the C implementation will prefer whatever last interaction happened, but this version will prefer the last move or resize. I wasn't really sure how specific the `tinyrwm` focus spec is. I like the simplicity of this code, but if we want identical focus behavior, this definitely isn't it! Let me know what you prefer on this front. Implementing an exact match for the C behavior will definitely be a little uglier because the borrow checker doesn't like me passing a mutable reference to `self.windows` to `seat.pointer_move()` while I'm iterating over the windows, so that's what I was trying to avoid, but maybe a little ugliness is the right approach here.
rust/src/main.rs Outdated
@ -0,0 +370,4 @@
ifself.windows.is_empty(){
seat.proxy.clear_focus();
}else{
matchseat.interacted.take(){
Owner
Copy link

The focus behavior of this rust implementation differs from that of the other tinyrwm implementations.

At least one bug I see in the rust focus logic is failing to reset seat.interacted to None after reading its value during a manage sequence.

The focus behavior of this rust implementation differs from that of the other tinyrwm implementations. At least one bug I see in the rust focus logic is failing to reset `seat.interacted` to `None` after reading its value during a manage sequence.
Author
Contributor
Copy link

I think this probably isn't an issue. seat.interacted.take() resets the value to None (and is the idiomatic way to clear an Option and use the contents).

I think this probably isn't an issue. `seat.interacted.take()` resets the value to `None` (and is the idiomatic way to clear an `Option` and use the contents).
Owner
Copy link

Ah my bad, I don't read or write rust very often and didn't realize the behavior of take.

I'll take a fresh look at the focus logic soon :)

Ah my bad, I don't read or write rust very often and didn't realize the behavior of take. I'll take a fresh look at the focus logic soon :)
ifreund marked this conversation as resolved
rust/src/main.rs Outdated
@ -0,0 +570,4 @@
fn focus(&mutself,window: &Window){
self.proxy.focus_window(&window.proxy);
window.node.place_top();
Owner
Copy link

This needs to move the window to the back of the windows deque to keep it in sync with rendering order. This is also contributing to the buggy focus behavior of this window manager implementation.

This needs to move the window to the back of the `windows` deque to keep it in sync with rendering order. This is also contributing to the buggy focus behavior of this window manager implementation.
rust/src/main.rs Outdated
@ -0,0 +702,4 @@
useriver_window_management::river_window_v1::Event;
letwindow=matchstate.wm.windows.iter_mut().find(|o|&o.proxy==proxy){
Some(window)=>window,
None=>return,
Owner
Copy link

It doesn't seem possible for this to be None, perhaps we should simply unwrap() here?

It doesn't seem possible for this to be None, perhaps we should simply unwrap() here?
Author
Contributor
Copy link

Yup! Done.

Yup! Done.
ifreund marked this conversation as resolved
rust/src/main.rs Outdated
@ -0,0 +705,4 @@
None=>return,
};
matchevent{
Event::Closed=>window.handle_closed(),
Owner
Copy link

I'd rather set window.closed = true directly here rather than having a single line handle_closed() function. The same goes for all the other trivial handle_foo() functions that currently exist.

I'd rather set `window.closed = true` directly here rather than having a single line `handle_closed()` function. The same goes for all the other trivial `handle_foo()` functions that currently exist.
Author
Contributor
Copy link

Done!

That does feel better.

Done! That does feel better.
ifreund marked this conversation as resolved
rust/src/main.rs Outdated
@ -0,0 +765,4 @@
_qh: &QueueHandle<Self>,
){
useriver_window_management::river_seat_v1::Event;
letseat=find_or_log!(state.wm.seats,proxy.id(),"seat");
Owner
Copy link

In what case is proxy.id() not present in state.wm.seats? I don't see one.

I think this should be an assertion (unwrap() in rust I guess) and find_or_log should be deleted.

In what case is proxy.id() not present in state.wm.seats? I don't see one. I think this should be an assertion (`unwrap()` in rust I guess) and `find_or_log` should be deleted.
Author
Contributor
Copy link

I don't think it should ever happen without a bug. I think at the time I was thinking that logging an error message was better than panicking if it did happen, but given that it really shouldn't happen panicking seems fine to me.

Switched to expect() (unwrap() but with a message on failure). I also went through and made the use of unwrap() and expect() more consistent - defaulting to expect(), and only using unwrap() where the code is locally and obviously correct.

I don't think it should ever happen without a bug. I think at the time I was thinking that logging an error message was better than panicking if it did happen, but given that it really shouldn't happen panicking seems fine to me. Switched to `expect()` (`unwrap()` but with a message on failure). I also went through and made the use of `unwrap()` and `expect()` more consistent - defaulting to `expect()`, and only using `unwrap()` where the code is locally and obviously correct.
Owner
Copy link

That seems like a nice approach to me :)

That seems like a nice approach to me :)
ifreund marked this conversation as resolved
jandrews271 force-pushed rust from 2ec013fff4
Some checks reported errors
builds.sr.ht/alpine Job failed
builds.sr.ht/archlinux Job failed
to 3da9ae5789
Some checks reported errors
builds.sr.ht/alpine Job failed
builds.sr.ht/archlinux Job failed
2026年03月11日 12:58:23 +01:00
Compare
jandrews271 force-pushed rust from 3da9ae5789
Some checks reported errors
builds.sr.ht/alpine Job failed
builds.sr.ht/archlinux Job failed
to 3c84b20805
Some checks reported errors
builds.sr.ht/alpine Job failed
builds.sr.ht/archlinux Job failed
2026年03月11日 14:16:29 +01:00
Compare
jandrews271 force-pushed rust from 3c84b20805
Some checks reported errors
builds.sr.ht/alpine Job failed
builds.sr.ht/archlinux Job failed
to 7c120f04ad
Some checks reported errors
builds.sr.ht/alpine Job failed
builds.sr.ht/archlinux Job failed
2026年03月11日 14:16:55 +01:00
Compare
Author
Contributor
Copy link

I've pushed fixes that I think should be good for most of the issues. The focus issues still need attention, but I may not get to that until the weekend. Quick summary of where I'm leaving off:

  • FocusNext handling was backwards, so I've fixed that, but focus handling generally still isn't identical to the C implementation. I left a comment above with some details - let me know how you want to resolve that.
  • We could include op_dx and op_dy in the SeatOp enum instead of on the seat. Let me know if I should do that.
  • Other than that, I'm happy with how things look, so let me know if anything else needs attention!
I've pushed fixes that I think should be good for most of the issues. The focus issues still need attention, but I may not get to that until the weekend. Quick summary of where I'm leaving off: - `FocusNext` handling was backwards, so I've fixed that, but focus handling generally still isn't identical to the C implementation. I left a comment above with some details - let me know how you want to resolve that. - We could include `op_dx` and `op_dy` in the `SeatOp` enum instead of on the seat. Let me know if I should do that. - Other than that, I'm happy with how things look, so let me know if anything else needs attention!

Nice work! I'll take a more in-depth look at the focus thing myself soon so I can give more clear/useful feedback there.

Other than that, this looks pretty much ready to land :)

Nice work! I'll take a more in-depth look at the focus thing myself soon so I can give more clear/useful feedback there. Other than that, this looks pretty much ready to land :)
ifreund force-pushed rust from 40244e9edc
Some checks reported errors
builds.sr.ht/alpine Job failed
builds.sr.ht/archlinux Job failed
to ca96654234
All checks were successful
builds.sr.ht/alpine Job completed
builds.sr.ht/archlinux Job completed
2026年03月12日 10:51:17 +01:00
Compare
ifreund left a comment
Copy link

I fixed the focus inconsistencies and made a few more minor cleanups.

Will land this in a minute when the CI hopefully passes

Thank you for your work on this! I never would have found the motivation to write an implementation in Rust on my own :)

I fixed the focus inconsistencies and made a few more minor cleanups. Will land this in a minute when the CI hopefully passes Thank you for your work on this! I never would have found the motivation to write an implementation in Rust on my own :)
ifreund manually merged commit ca96654234 into main 2026年03月12日 10:54:10 +01:00
Sign in to join this conversation.
No reviewers
Labels
Clear labels
No items
No labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
river/tinyrwm!2
Reference in a new issue
river/tinyrwm
No description provided.
Delete branch "jandrews271/tinyrwm:rust"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?