Skip to content

feat(mouse): add Zoom In/Out actions with continuous thumb-wheel zoom - #764

Open
NatsUIJM wants to merge 3 commits into
AprilNEA:masterfrom
NatsUIJM:feat/thumbwheel-zoom
Open

feat(mouse): add Zoom In/Out actions with continuous thumb-wheel zoom#764
NatsUIJM wants to merge 3 commits into
AprilNEA:masterfrom
NatsUIJM:feat/thumbwheel-zoom

Conversation

@NatsUIJM

Copy link
Copy Markdown

Motivation

Logi Options+ lets users turn the MX Master thumb wheel into a zoom control; OpenLogi has no equivalent. On macOS, iWork/Preview-class apps implement zoom only behind the trackpad magnify gesture — they ignore modifier-stamped scroll entirely — so this feature is synthesised as a real pinch gesture rather than ⌘+wheel.

What's added

Core / bindings

  • ZoomIn / ZoomOut scroll-family actions, bindable to any button or thumb-wheel direction, translated in all locales.

Injection (openlogi-inject)

  • macOS: zoom is a genuine gesture event — CGEvent type 29, HID subtype 8, magnification encoded as an f32 bit pattern in fields 115/117, phases in field 132 (wire format from CalfTrail's trackpad-traffic reverse engineering, cross-checked event-for-event against AbnormalMouseApp's EmulateEventPoster).
  • Button-bound zoom fires one complete micro-gesture per press.
  • Thumb-wheel zoom streams: every rotation report becomes a changed event inside a single ongoing pinch session (began on the first delta after a quiet spell, ended by a watcher ~250 ms after the wheel rests) — the same lifecycle as AbnormalMouse's ZoomAndRotateController, which makes zoom read as continuous like Logi Options+ instead of stepping per detent.
  • Linux/Windows bank fractional deltas and emit whole modifier-stamped wheel detents.

Capture watcher (openlogi-agent-core)

  • Zoom bindings skip the whole-line accumulator gate: each report emits its sensitivity-scaled fraction immediately, so total zoom gain per rotation is unchanged while delivery becomes continuous.

Desktop UI

  • Two paired presets ("Zoom Out / In", "Zoom In / Out") in the thumb-wheel picker, mirroring the existing dual-polarity volume rows.
  • Preset labels are now composed from each side's already-translated action name. Previously every preset row rendered English in all non-English apps, because the flat "X / Y" strings were keys in no locale file; composing fixes all 21 locales at once with zero new strings.

Verification

  • cargo test -p openlogi-inject -p openlogi-agent-core -p openlogi-desktop green (incl. new streaming-semantics and preset-pair tests); clippy clean on all three crates.
  • Manually verified on Apple Silicon macOS with an MX Master 3S over Bluetooth: continuous pinch zoom in Numbers/Pages/Preview at several sensitivity settings, both polarities, presets recognised in the picker, zh-CN labels render correctly.

🤖 Generated with Claude Code

Adds ZoomIn/ZoomOut as first-class bindable actions across the stack:

- core: two new scroll-family actions wired through effects, labels,
  action-ring icons and binding tests.
- inject: macOS renders zoom as a trackpad-pinch magnify gesture
  (CGEvent type 29 with the f32 bit-pattern payload in fields 115/117,
  the wire format reverse-engineered by CalfTrail and validated by
  AbnormalMouseApp). A button press fires one discrete began-to-ended
  gesture; the thumb wheel instead streams every rotation increment as
  changed events inside one ongoing session that closes ~250 ms after
  the wheel rests, so zoom reads as continuous like Logi Options+
  rather than stepping per detent. Linux/Windows bank fractional deltas
  into whole modifier-stamped wheel detents.
- agent-core: thumb-wheel rotation bound to zoom no longer gates on
  whole-line accumulation; each report emits its sensitivity-scaled
  fraction immediately, keeping total gain identical.
- desktop: paired Zoom presets in both polarities for the thumb-wheel
  picker, mirroring the volume rows. Preset labels now compose from
  each side's already-translated action name: the flat "X / Y" strings
  were keys in no locale file, so every preset row fell back to English
  regardless of app language; composing fixes all locales at once.
- ui: zoom-in/zoom-out icons registered and "Zoom In"/"Zoom Out"
  translated in all locales.

Co-Authored-By: Claude <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds bindable Zoom In and Zoom Out actions, platform-specific zoom injection, continuous thumb-wheel streaming, localized picker presets, and matching icons.

  • Adds macOS pinch-gesture synthesis with an idle-managed streaming lifecycle.
  • Adds Ctrl-wheel zoom and fractional banking on Linux and Windows.
  • Routes thumb-wheel zoom reports continuously through the agent watcher.
  • Extends the desktop picker, action catalog, translations, and icon set.

Confidence Score: 3/5

The PR is not yet safe to merge because fractional zoom state remains mixed across streams on Linux and Windows, while macOS likewise shares one pinch lifecycle across independent devices.

Independent devices can cancel or complete each other's banked zoom on Linux and Windows, and macOS discards device ownership before assigning deltas to its single ongoing pinch session.

Files Needing Attention: crates/openlogi-inject/src/inject/linux.rs, crates/openlogi-inject/src/inject/windows.rs, crates/openlogi-inject/src/inject/macos.rs, crates/openlogi-agent-core/src/watchers/gesture.rs

Important Files Changed

Filename Overview
crates/openlogi-agent-core/src/watchers/gesture.rs Adds immediate sensitivity-scaled ZoomContinuous output, but drops the originating device identity at the injection boundary.
crates/openlogi-inject/src/inject/macos.rs Adds pinch event synthesis and fixes nested discrete gestures, while retaining one lifecycle shared across all device streams.
crates/openlogi-inject/src/inject/linux.rs Adds Ctrl-wheel zoom, but the previously reported process-global fractional accumulator remains.
crates/openlogi-inject/src/inject/windows.rs Adds modifier-wheel zoom and retains the same outstanding cross-stream fractional-state mixing as Linux.
crates/openlogi-core/src/binding/action.rs Safely appends ZoomIn and ZoomOut to the serialized action vocabulary and catalog.
crates/openlogi-desktop/src/features/mouse/thumbwheel.rs Adds paired zoom presets using independently translated action labels.

Sequence Diagram

sequenceDiagram
    participant A as Device A
    participant B as Device B
    participant W as Gesture watcher
    participant Z as Global macOS ZOOM_SESSION
    participant App as Focused application
    A->>W: Zoom delta with device key A
    W->>Z: post_zoom_continuous(delta)
    Z->>App: began
    B->>W: Zoom delta with device key B
    W->>Z: post_zoom_continuous(delta)
    Note over W,Z: Device identity is not forwarded
    Z->>App: changed in A's existing gesture
    Z-->>App: ended after global idle timeout
Loading

Fix all with Greploop Fix All in Codex Fix All in Claude Code

Reviews (3): Last reviewed commit: "fix(inject): replace nonexistent f32::Fr..." | Re-trigger Greptile

Comment thread crates/openlogi-inject/src/inject/linux.rs
Comment thread crates/openlogi-inject/src/inject/macos.rs
A button-bound Zoom In/Out press always posted a standalone began→ended
micro-gesture. While a thumb-wheel pinch session was open, that pair
ended the application's active gesture without clearing ZOOM_SESSION,
so subsequent wheel deltas posted as orphaned changed events after
ended — apps ignored them and zoom froze until the idle closer reset
the session. While a session is open, the button step now folds in as
one more changed delta instead.

Also documents the Linux/Windows fractional-bank design: a single signed
process-global accumulator is intentional — the output is focus-directed
(there is no per-stream identity for state to cross) and net-proportional
cancellation is the correct wheel semantics.

Co-Authored-By: Claude <noreply@anthropic.com>
@NatsUIJM

Copy link
Copy Markdown
Author

Thanks for the review. Both points are addressed in 8de4ae0:

  • macOS discrete zoom vs. open pinch session — valid, fixed. While a wheel-driven pinch session is open, a button-bound zoom step now folds in as one more changed delta instead of posting a competing began/ended pair, which previously ended the app's active gesture while our ZOOM_SESSION still read open.
  • Linux/Windows global fractional bank — intentional design, now documented in the code: zoom output is focus-directed, so there is no per-device/per-direction stream identity for state to cross, and a single signed accumulator gives the correct net semantics (rolling up 0.4 lines then down 0.4 cancels to zero instead of emitting two contradicting detents). Happy to switch to per-direction banking if you'd prefer.

@davidbudnick davidbudnick added type: feature New feature request platform: all Cross-platform issue labels Aug 21, 2026
@davidbudnick

Copy link
Copy Markdown
Collaborator

@NatsUIJM Looks like most of the github actions are failing can you fix those?

f32 implements From only for the lossless integer types (i8/i16/u8/u16),
so `f32::from(whole)` failed to compile on Linux and Windows — seven CI
jobs (MSRV, both clippies, rustdoc, tests-linux, both nix builds) all
traced back to that one line in each platform's post_zoom_continuous.
The macOS build never saw it because the per-OS inject modules are
cfg-gated out. Cast explicitly instead; cross-target clippy for
x86_64-unknown-linux-gnu and x86_64-pc-windows-msvc now passes locally,
with the expect list covering both casts' pedantic lints.

Co-Authored-By: Claude <noreply@anthropic.com>
@NatsUIJM

Copy link
Copy Markdown
Author

Fixed in 7406efa. All seven failing jobs traced to a single root cause: post_zoom_continuous in both the Linux and Windows backends called f32::from(i32), which doesn't exist (From is only implemented for lossless integer conversions). The macOS build never caught it because those modules are cfg-gated out on this platform — my local checks were a false negative.

Replaced with explicit casts (the pedantic lint expects cover both), and validated locally with cross-target clippy for x86_64-unknown-linux-gnu and x86_64-pc-windows-msvc before pushing. CI should be green on this commit.

Comment on lines +642 to +643
static ZOOM_SESSION: std::sync::LazyLock<std::sync::Mutex<ZoomSession>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(ZoomSession::default()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Global pinch session mixes devices

If two zoom-bound devices emit thumb-wheel deltas within the same 250 ms session window, the watcher discards their device identities and both update the process-global ZOOM_SESSION. The second device therefore posts changed events into the first device's pinch and extends its lifetime, merging otherwise independent zoom streams in the focused application.

Knowledge Base Used:

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

@NatsUIJM

Copy link
Copy Markdown
Author

Heads-up: the workflow runs for the fix commit (7406efa) are sitting in action_required under the fork-PR approval gate — could you approve them so CI re-runs? For what it's worth, the exact failing configs were validated locally beforehand: cross-target cargo clippy for x86_64-unknown-linux-gnu and x86_64-pc-windows-msvc both finish clean.

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

Labels

platform: all Cross-platform issue type: feature New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants