Skip to content

feat: virtual encoder controls (click/drag dial widget and touch simulation)#313

Draft
KatsuJinCode wants to merge 2 commits intonekename:mainfrom
KatsuJinCode:feature/virtual-encoder-controls
Draft

feat: virtual encoder controls (click/drag dial widget and touch simulation)#313
KatsuJinCode wants to merge 2 commits intonekename:mainfrom
KatsuJinCode:feature/virtual-encoder-controls

Conversation

@KatsuJinCode
Copy link
Copy Markdown

Per @Terrorwolf01's request to split the virtual encoder out: this PR adds a UI-only layer on top of #306 that lets users interact with the encoder from OpenDeck itself, without needing the physical device.

Depends on #306 (the touch_tap handler used by trigger_virtual_touch lives there).

Frontend (Svelte)

  • EncoderDial component per encoder: 30-detent visual indicator, click-drag to rotate, click to press / release, long-press for touch-and-hold, double-click for touchTap.
  • Physical device rotation is reflected in the UI dial via the encoder_rotated event, so the indicator tracks real hardware too.

Backend (Rust)

  • trigger_virtual_rotate, trigger_virtual_encoder_down / _up, trigger_virtual_touch Tauri commands. They feed the same inbound handlers as real device events, so plugins see no difference between virtual and physical input.
  • encoder_rotated frontend event emitted from encoder_change so the dial indicator can track physical rotation in real time.
  • TicksPayload made Serialize + Clone so it can be emitted to the webview.

Why split this out

@Terrorwolf01 noted the official Stream Deck software uses a circular dial UI for encoder slots; this PR gives OpenDeck the equivalent. The visible LCD strip rendering (PR #306) is independent of this -- you can ship that without this PR if you'd prefer to keep OpenDeck's encoder UX more minimal.

Full Stream Deck + touch strip support implementing the SDK feedback
layout spec.

## Backend (Rust)

- 200x100 LCD rendering per encoder slot (was a 72x72 thumbnail).
- setFeedback handler merges payload into persistent per-instance
  feedback state and notifies the frontend.
- setFeedbackLayout handler records the layout, resets feedback state
  (spec-level keys differ per layout so stale state would render
  wrong), and notifies the frontend.
- ActionInstance gains feedback_layout + feedback fields; preserved
  through DiskActionInstance so layouts survive restart.
- Action manifest parser now reads the Encoder.layout property so the
  plugin-declared default layout applies on instance creation.
- get_feedback_layout Tauri command reads custom layout JSON from the
  plugin folder with a canonical-path check against traversal.
- Touch screen press / long-press mapped to SDK touchTap events;
  touchTap outbound event carries coordinates, settings, and hold.

## Frontend (Svelte / TypeScript)

- feedbackLayouts.ts: the six built-in layouts ($X1, $A0, $A1, $B1,
  $B2, $C1) as TypeScript objects, verbatim from the SDK docs.
- feedbackRenderer.ts: Canvas 2D renderer for the four item types
  (pixmap, text, bar, gbar). Supports all five bar subtypes
  (Rectangle, DoubleRectangle, Trapezoid, DoubleTrapezoid, Groove),
  gradient colour specs, z-order, opacity, text alignment, and
  clip/ellipsis overflow. Pixmaps preserve aspect ratio except for
  the $A0 'full-canvas' item which is explicitly 200x100.
- Key.svelte: encoder slots with a feedback_layout render via the
  layout renderer and push the composed 200x100 pixmap to the device
  via update_image. Icon and title fall back to the action's
  state.image and state.text when the plugin hasn't provided its own,
  matching the 'Reserved Layout Item Keys' behaviour in the docs.
- Edge-to-edge encoder strip UI matching the physical 800x100 LCD.
- Dark theme CSS for Property Inspector select elements.

The virtual encoder UI widget is split to a separate PR.
…lation)

UI-only layer on top of nekename#306 that lets users interact with the encoder
from OpenDeck itself, without needing the physical device.

Frontend:
- EncoderDial component per encoder: 30-detent visual indicator,
  click-drag to rotate, click to press/release, long-press for
  touch-and-hold, double-click for touchTap.
- Physical device rotation reflected in the UI dial (encoder_rotated
  event).

Backend:
- trigger_virtual_rotate, trigger_virtual_encoder_down/up,
  trigger_virtual_touch Tauri commands feed the same inbound handlers
  as real device events; plugins see no difference.
- encoder_rotated frontend event emitted from encoder_change so the
  dial indicator tracks physical rotation.
- TicksPayload made Serialize + Clone for emit to webview.

Depends on nekename#306 for touch_tap handler.
@nekename
Copy link
Copy Markdown
Owner

I'm sorry, but there is no way I will be able to review or merge a PR that changes over 1,300 lines of code.

@nekename
Copy link
Copy Markdown
Owner

Just looking at the diff, and it seems like this PR contains all of the changes from #306 as well. If this is intended to be merged after that PR, then it needs to be marked as a draft.

@nekename nekename marked this pull request as draft April 16, 2026 11:41
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.

2 participants