One thing if you don't mind: I would consider making OldCursor a Message and using MessageWriter/MessageReader instead. Unless move_camera_after_zoom can be written so it works during Update, then i'd either inline it or use a Event/Observer.
I haven't kept up to date with a lot of the newer bevy features tbh; I've only recently started using observers and picking, and I never even knew Message was a thing. Can you show me what you mean? I've just read it and I see it's what used to be called an Event.
I did think about using a Message, but it felt somewhat like overkill, since I'll only ever produce it in one place, and consume it in one other place. I suppose it does fit the bill but it's just a way of passing data into the future.
Particularly interested in how you would inline it - one way I thought of would be to duplicate the code in camera_update_system and simulate the change to the camera. Alternatively I could insert another camera_update_system into the schedule, but either way I need to know the state before and after the "camera" has been updated. Seems to be a blindspot with the existing API; it certainly wasn't intuitive to me that changing the Projection would update the Camera towards the end of the frame, and the Camera was doing the position calculation.
How would you use an Event/Observer?
Plus i'd use a picking Observer with window picking from the get-go - that allows you to benefit from picking observers bubbling nature, allowing things to block the scroll from propagating
I had wondered this for pointer events on empty space (e.g. for deselecting entities) but have no idea how to add an observer to a window - also, I didn't think mouse scroll is one of the events, or are you suggesting creating a new picking backend?
I have personally struggled with the discoverability of bevy features; the best write-ups are always in the change logs (or the PRs) but the change logs do tend to be very, very long.