# Molotov Phase 2D: SDL3 Text Input (IME Preedit) & Gamepad Rumble Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Implement the SDL3 Text Input (with IME preedit composition for fcitx5 and ibus) and Gamepad Support (with physical glyph mapping and dual-motor force feedback rumble) in `molotov-core`, completing the native Linux I/O bridges for Molotov. **Architecture:** Bridges native desktop input methods and gamepads via SDL3 APIs. Manages in-progress IME composition strings (`SDL_EVENT_TEXT_EDITING`) and candidate window positioning (`SDL_SetTextInputArea`). Translates gamepad inputs to Android `KeyEvent` / `MotionEvent` codes and intercepts Android `Vibrator` JNI calls to drive gamepad motor and trigger rumble. **Tech Stack:** Rust 1.75+, `libc`, `tracing`, `thiserror`. **Spec:** `docs/superpowers/specs/2026-09-29-molotov-architecture-design.md` (Sections 7.2 & 7.3) ## Global Constraints - **Language & Edition**: Rust 1.75+ (2021 edition), strictly zero Wine or Proton. - **License**: GNU General Public License v3.0 or later (GPL-3.0-or-later) with headers in all source files. - **IME Preedit Support**: Must support in-flight multi-byte character composition (CJK, accents) under Wayland and X11 without premature character commits. - **Accurate Glyph Rendering**: Maps physical controller layouts (Xbox, DualSense, Switch) accurately without inverted button prompts. - **Dual-Motor & Trigger Rumble**: Handles low/high frequency vibration effects and clamp durations. ## Review Focus 1. **Preedit Text Desynchronization**: Canceling or clearing an IME preedit composition must not leave orphan characters in the active text field. 2. **Multi-Byte UTF-8 Boundary Splitting**: Cursor position calculations in preedit strings must strictly index on valid UTF-8 character boundaries. 3. **Gamepad Disconnection & Hotplugging**: Disconnecting a controller during active rumble must cancel vibration timers without panics. 4. **Vibration Amplitude Clamping**: Float/integer amplitudes from Android `VibrationEffect` must clamp to valid 16-bit unsigned motor ranges (`0..=65535`). 5. **Simultaneous Multi-Controller Rumble**: Independent vibration actuation across multiple connected gamepads without cross-talk. --- ### Task 1: IME Preedit State Machine & Candidate Area Tracker **Files:** - Create: `crates/molotov-core/src/input/ime.rs` - Create: `crates/molotov-core/src/input/mod.rs` - Test: `crates/molotov-core/tests/ime_preedit_test.rs` **Interfaces:** - Consumes: None - Produces: - `molotov_core::input::ime::ImeManager` - `molotov_core::input::ime::ImeState` - `molotov_core::input::ime::Rect` - [ ] **Step 1: Write the failing test for IME preedit composition and commit** ```rust // crates/molotov-core/tests/ime_preedit_test.rs use molotov_core::input::ime::{ImeManager, Rect}; #[test] fn test_ime_preedit_composition_flow() { let mut ime = ImeManager::new(); ime.start_text_input(Rect { x: 100, y: 200, width: 20, height: 24 }); assert!(ime.is_active()); // User types in Japanese IME: preedit string "とうきょう" ime.update_preedit("とうきょう", 4); assert_eq!(ime.preedit_text(), "とうきょう"); assert_eq!(ime.cursor_pos(), 4); // User selects candidate: commit "東京" let committed = ime.commit_text("東京"); assert_eq!(committed, "東京"); assert_eq!(ime.preedit_text(), "", "Preedit must clear on commit"); ime.stop_text_input(); assert!(!ime.is_active()); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-core --test ime_preedit_test` Expected: FAIL with unexported module `input`. - [ ] **Step 3: Implement ImeManager** ```rust // crates/molotov-core/src/input/ime.rs // SPDX-License-Identifier: GPL-3.0-or-later #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Rect { pub x: i32, pub y: i32, pub width: i32, pub height: i32, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ImeState { Inactive, Active, } pub struct ImeManager { state: ImeState, cursor_area: Rect, preedit_text: String, cursor_pos: usize, } impl Default for ImeManager { fn default() -> Self { Self::new() } } impl ImeManager { pub fn new() -> Self { Self { state: ImeState::Inactive, cursor_area: Rect::default(), preedit_text: String::new(), cursor_pos: 0, } } pub fn is_active(&self) -> bool { self.state == ImeState::Active } pub fn start_text_input(&mut self, area: Rect) { self.state = ImeState::Active; self.cursor_area = area; self.preedit_text.clear(); self.cursor_pos = 0; } pub fn stop_text_input(&mut self) { self.state = ImeState::Inactive; self.preedit_text.clear(); self.cursor_pos = 0; } pub fn set_cursor_area(&mut self, area: Rect) { self.cursor_area = area; } pub fn cursor_area(&self) -> Rect { self.cursor_area } pub fn update_preedit(&mut self, text: &str, cursor_pos: usize) { self.preedit_text = text.to_string(); self.cursor_pos = cursor_pos.min(self.preedit_text.chars().count()); } pub fn preedit_text(&self) -> &str { &self.preedit_text } pub fn cursor_pos(&self) -> usize { self.cursor_pos } pub fn commit_text(&mut self, text: &str) -> String { self.preedit_text.clear(); self.cursor_pos = 0; text.to_string() } pub fn cancel_composition(&mut self) { self.preedit_text.clear(); self.cursor_pos = 0; } } ``` ```rust // crates/molotov-core/src/input/mod.rs // SPDX-License-Identifier: GPL-3.0-or-later pub mod ime; ``` Update `crates/molotov-core/src/lib.rs` to expose `pub mod input;`. - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-core --test ime_preedit_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-core/ git commit -m "feat(input): implement IME preedit state machine and cursor area tracker" ``` --- ### Task 2: Gamepad Button & Axis Mapping with Glyph Translation **Files:** - Create: `crates/molotov-core/src/input/gamepad.rs` - Modify: `crates/molotov-core/src/input/mod.rs` - Test: `crates/molotov-core/tests/gamepad_mapping_test.rs` **Interfaces:** - Consumes: None - Produces: - `molotov_core::input::gamepad::GamepadButton` - `molotov_core::input::gamepad::GamepadType` - `molotov_core::input::gamepad::AndroidKeyCode` - `molotov_core::input::gamepad::translate_button_to_android()` - [ ] **Step 1: Write the failing test for button mapping and glyph accuracy** ```rust // crates/molotov-core/tests/gamepad_mapping_test.rs use molotov_core::input::gamepad::{ translate_button_to_android, AndroidKeyCode, GamepadButton, GamepadType, }; #[test] fn test_xbox_and_playstation_button_mapping() { // Xbox 'A' button -> Android KEYCODE_BUTTON_A assert_eq!( translate_button_to_android(GamepadButton::South, GamepadType::Xbox), AndroidKeyCode::ButtonA ); // PlayStation 'Cross' button -> Android KEYCODE_BUTTON_A assert_eq!( translate_button_to_android(GamepadButton::South, GamepadType::PlayStation), AndroidKeyCode::ButtonA ); // D-Pad Up -> Android KEYCODE_DPAD_UP assert_eq!( translate_button_to_android(GamepadButton::DpadUp, GamepadType::Xbox), AndroidKeyCode::DpadUp ); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-core --test gamepad_mapping_test` Expected: FAIL with unexported module `gamepad`. - [ ] **Step 3: Implement GamepadButton, GamepadType, and translator** ```rust // crates/molotov-core/src/input/gamepad.rs // SPDX-License-Identifier: GPL-3.0-or-later #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GamepadType { Xbox, PlayStation, NintendoSwitch, Generic, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GamepadButton { South, // A (Xbox) / Cross (PS) / B (Switch) East, // B (Xbox) / Circle (PS) / A (Switch) West, // X (Xbox) / Square (PS) / Y (Switch) North, // Y (Xbox) / Triangle (PS) / X (Switch) Back, Guide, Start, LeftStick, RightStick, LeftShoulder, RightShoulder, DpadUp, DpadDown, DpadLeft, DpadRight, } #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AndroidKeyCode { DpadUp = 19, DpadDown = 20, DpadLeft = 21, DpadRight = 22, DpadCenter = 23, ButtonA = 96, ButtonB = 97, ButtonC = 98, ButtonX = 99, ButtonY = 100, ButtonZ = 101, ButtonL1 = 102, ButtonR1 = 103, ButtonL2 = 104, ButtonR2 = 105, ButtonThumbL = 106, ButtonThumbR = 107, ButtonStart = 108, ButtonSelect = 109, ButtonMode = 110, } pub fn translate_button_to_android(btn: GamepadButton, _gamepad_type: GamepadType) -> AndroidKeyCode { match btn { GamepadButton::South => AndroidKeyCode::ButtonA, GamepadButton::East => AndroidKeyCode::ButtonB, GamepadButton::West => AndroidKeyCode::ButtonX, GamepadButton::North => AndroidKeyCode::ButtonY, GamepadButton::Back => AndroidKeyCode::ButtonSelect, GamepadButton::Guide => AndroidKeyCode::ButtonMode, GamepadButton::Start => AndroidKeyCode::ButtonStart, GamepadButton::LeftStick => AndroidKeyCode::ButtonThumbL, GamepadButton::RightStick => AndroidKeyCode::ButtonThumbR, GamepadButton::LeftShoulder => AndroidKeyCode::ButtonL1, GamepadButton::RightShoulder => AndroidKeyCode::ButtonR1, GamepadButton::DpadUp => AndroidKeyCode::DpadUp, GamepadButton::DpadDown => AndroidKeyCode::DpadDown, GamepadButton::DpadLeft => AndroidKeyCode::DpadLeft, GamepadButton::DpadRight => AndroidKeyCode::DpadRight, } } ``` Update `crates/molotov-core/src/input/mod.rs` to expose `pub mod gamepad;`. - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-core --test gamepad_mapping_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-core/ git commit -m "feat(input): implement Gamepad button translation and Android keycodes" ``` --- ### Task 3: Dual-Motor Force Feedback & Trigger Rumble **Files:** - Create: `crates/molotov-core/src/input/haptics.rs` - Modify: `crates/molotov-core/src/input/mod.rs` - Test: `crates/molotov-core/tests/haptics_test.rs` **Interfaces:** - Consumes: None - Produces: - `molotov_core::input::haptics::RumbleManager` - `molotov_core::input::haptics::RumbleState` - [ ] **Step 1: Write the failing test for dual-motor rumble and trigger actuation** ```rust // crates/molotov-core/tests/haptics_test.rs use molotov_core::input::haptics::RumbleManager; #[test] fn test_rumble_actuation_and_clamping() { let mut manager = RumbleManager::new(); let pad_id = 1; // Actuate rumble: low_freq = 0.5 (32767), high_freq = 1.0 (65535), duration = 250ms manager.rumble(pad_id, 0.5, 1.0, 250); let state = manager.get_state(pad_id).expect("Should have rumble state"); assert_eq!(state.low_freq_motor, 32767); assert_eq!(state.high_freq_motor, 65535); assert_eq!(state.duration_ms, 250); // Stop rumble manager.stop(pad_id); let stopped = manager.get_state(pad_id).expect("Should have rumble state"); assert_eq!(stopped.low_freq_motor, 0); assert_eq!(stopped.high_freq_motor, 0); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-core --test haptics_test` Expected: FAIL with unexported module `haptics`. - [ ] **Step 3: Implement RumbleManager** ```rust // crates/molotov-core/src/input/haptics.rs // SPDX-License-Identifier: GPL-3.0-or-later use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct RumbleState { pub low_freq_motor: u16, pub high_freq_motor: u16, pub left_trigger: u16, pub right_trigger: u16, pub duration_ms: u32, } pub struct RumbleManager { states: HashMap, } impl Default for RumbleManager { fn default() -> Self { Self::new() } } impl RumbleManager { pub fn new() -> Self { Self { states: HashMap::new(), } } pub fn rumble(&mut self, gamepad_id: u32, low_freq: f32, high_freq: f32, duration_ms: u32) { let low = (low_freq.clamp(0.0, 1.0) * 65535.0) as u16; let high = (high_freq.clamp(0.0, 1.0) * 65535.0) as u16; let entry = self.states.entry(gamepad_id).or_default(); entry.low_freq_motor = low; entry.high_freq_motor = high; entry.duration_ms = duration_ms; } pub fn rumble_triggers(&mut self, gamepad_id: u32, left: f32, right: f32, duration_ms: u32) { let l = (left.clamp(0.0, 1.0) * 65535.0) as u16; let r = (right.clamp(0.0, 1.0) * 65535.0) as u16; let entry = self.states.entry(gamepad_id).or_default(); entry.left_trigger = l; entry.right_trigger = r; entry.duration_ms = duration_ms; } pub fn stop(&mut self, gamepad_id: u32) { if let Some(entry) = self.states.get_mut(&gamepad_id) { entry.low_freq_motor = 0; entry.high_freq_motor = 0; entry.left_trigger = 0; entry.right_trigger = 0; entry.duration_ms = 0; } } pub fn get_state(&self, gamepad_id: u32) -> Option { self.states.get(&gamepad_id).copied() } } ``` Update `crates/molotov-core/src/input/mod.rs` to expose `pub mod haptics;`. - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-core --test haptics_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-core/ git commit -m "feat(input): implement dual-motor force feedback and trigger rumble manager" ``` --- ### Task 4: Unified Input Manager & JNI Vibrator Bridge **Files:** - Create: `crates/molotov-core/src/input/manager.rs` - Modify: `crates/molotov-core/src/input/mod.rs` - Modify: `crates/molotov-core/src/jni/framework.rs` - Test: `crates/molotov-core/tests/input_manager_test.rs` **Interfaces:** - Consumes: `molotov_core::input::ime::ImeManager`, `molotov_core::input::haptics::RumbleManager` - Produces: - `molotov_core::input::manager::InputManager` - Integration with Android framework class `android/os/Vibrator` - [ ] **Step 1: Write the failing test for unified input coordination** ```rust // crates/molotov-core/tests/input_manager_test.rs use molotov_core::input::manager::InputManager; #[test] fn test_input_manager_coordination() { let mut input = InputManager::new(); assert!(!input.ime.is_active()); // Game requests text entry input.ime.start_text_input(Default::default()); assert!(input.ime.is_active()); // Game dispatches haptics input.haptics.rumble(0, 0.8, 0.8, 100); assert_eq!(input.haptics.get_state(0).unwrap().duration_ms, 100); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test -p molotov-core --test input_manager_test` Expected: FAIL with unexported module `manager`. - [ ] **Step 3: Implement InputManager** ```rust // crates/molotov-core/src/input/manager.rs // SPDX-License-Identifier: GPL-3.0-or-later use super::haptics::RumbleManager; use super::ime::ImeManager; pub struct InputManager { pub ime: ImeManager, pub haptics: RumbleManager, } impl Default for InputManager { fn default() -> Self { Self::new() } } impl InputManager { pub fn new() -> Self { Self { ime: ImeManager::new(), haptics: RumbleManager::new(), } } } ``` In `crates/molotov-core/src/jni/framework.rs`: add `"android/os/Vibrator".to_string()` and `"android/os/VibrationEffect".to_string()` to `known_classes`. - [ ] **Step 4: Run test to verify it passes** Run: `cargo test -p molotov-core --test input_manager_test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add crates/molotov-core/ git commit -m "feat(input): implement unified input manager and register Vibrator classes in JNI" ``` --- ### Task 5: End-to-End IME & Gamepad Rumble Integration Test **Files:** - Create: `crates/molotov-core/tests/input_haptics_test.rs` - Modify: `crates/molotov-core/src/input/mod.rs` **Interfaces:** - Consumes: All `molotov_core::input::*` modules - Produces: Verified end-to-end IME text composition and controller rumble actuation - [ ] **Step 1: Write the end-to-end integration test** ```rust // crates/molotov-core/tests/input_haptics_test.rs use molotov_core::input::gamepad::{translate_button_to_android, AndroidKeyCode, GamepadButton, GamepadType}; use molotov_core::input::ime::Rect; use molotov_core::input::manager::InputManager; #[test] fn test_end_to_end_ime_and_gamepad_rumble() { let mut manager = InputManager::new(); // 1. Text Chat IME Preedit flow manager.ime.start_text_input(Rect { x: 50, y: 100, width: 300, height: 30 }); manager.ime.update_preedit("nihao", 5); assert_eq!(manager.ime.preedit_text(), "nihao"); let committed = manager.ime.commit_text("你好"); assert_eq!(committed, "你好"); assert_eq!(manager.ime.preedit_text(), ""); // 2. Controller Input Translation let key = translate_button_to_android(GamepadButton::South, GamepadType::Xbox); assert_eq!(key, AndroidKeyCode::ButtonA); // 3. Controller Haptic Actuation manager.haptics.rumble(0, 0.75, 0.25, 150); let state = manager.haptics.get_state(0).unwrap(); assert_eq!(state.low_freq_motor, (0.75 * 65535.0) as u16); assert_eq!(state.high_freq_motor, (0.25 * 65535.0) as u16); assert_eq!(state.duration_ms, 150); } ``` - [ ] **Step 2: Run test to verify it passes** Run: `cargo test -p molotov-core --test input_haptics_test` Expected: PASS. - [ ] **Step 3: Commit** ```bash git add crates/molotov-core/tests/input_haptics_test.rs git commit -m "test(input): implement end-to-end IME text editing and gamepad rumble integration test" ``` ---