From 4cfe2fbb7bd9b086ef265be0e464aabf6c5214b7 Mon Sep 17 00:00:00 2001 From: Mae PUGIN <48982737+mornepousse@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:09:41 +0200 Subject: [PATCH] feat: Editable Tap Dance + combo creation validation Tap Dance: - Each TD action is now a clickable button that opens the key selector - Clicking an action -> pick key from popup -> sends TD_SET binary to firmware - Labels show 1-tap / 2-tap / 3-tap / hold columns Combo creation: - Added validation: "Pick both keys first" message if keys not selected - Debug log for COMBOSET command Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main.rs | 48 +++++++++++++++++++++++++++++++++++++- ui/globals.slint | 12 +++++++++- ui/tabs/tab_advanced.slint | 26 +++++++++++++++++++-- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 411533b..c720f4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -672,6 +672,7 @@ fn main() { let dispatch_keycode = { let apply_keycode = apply_keycode.clone(); let keys_arc = keys_arc.clone(); + let serial = serial.clone(); let macro_steps = macro_steps.clone(); let refresh_macro_display = refresh_macro_display.clone(); let window_weak = window.as_weak(); @@ -738,6 +739,41 @@ fn main() { } if count < 4 { adv.set_new_leader_seq_count(count + 1); } } + "td-action" => { + let adv = w.global::(); + let td_idx = adv.get_editing_td_index(); + let slot = adv.get_editing_td_slot() as usize; + if td_idx >= 0 && slot < 4 { + // Update model in place + let tds = adv.get_tap_dances(); + for i in 0..tds.row_count() { + let td = tds.row_data(i).unwrap(); + if td.index == td_idx { + let actions = td.actions; + let mut a = actions.row_data(slot).unwrap(); + a.name = name.clone(); + a.code = code as i32; + actions.set_row_data(slot, a); + + // Collect all 4 action codes and send to firmware + let mut codes = [0u16; 4]; + for j in 0..4.min(actions.row_count()) { + codes[j] = actions.row_data(j).unwrap().code as u16; + } + let payload = logic::binary_protocol::td_set_payload(td_idx as u8, &codes); + let serial = serial.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(logic::binary_protocol::cmd::TD_SET, &payload); + }); + w.global::().set_status_text( + SharedString::from(format!("TD{} slot {} = {}", td_idx, slot, name)) + ); + break; + } + } + } + } "macro-step" => { // Add key press (Down + Up) to macro steps let mut steps = macro_steps.borrow_mut(); @@ -1018,7 +1054,14 @@ fn main() { let r2 = adv.get_new_combo_r2() as u8; let c2 = adv.get_new_combo_c2() as u8; let result = adv.get_new_combo_result_code() as u8; + let key1_name = adv.get_new_combo_key1_name(); + let key2_name = adv.get_new_combo_key2_name(); + if key1_name == "Pick..." || key2_name == "Pick..." { + w.global::().set_status_text("Pick both keys first".into()); + return; + } let cmd = logic::protocol::cmd_comboset(255, r1, c1, r2, c2, result); + eprintln!("COMBOSET: {}", cmd); let serial = serial.clone(); let tx = tx.clone(); std::thread::spawn(move || { @@ -1481,7 +1524,10 @@ fn main() { .map(|(i, actions)| TapDanceData { index: i as i32, actions: ModelRc::from(Rc::new(VecModel::from( - actions.iter().map(|&a| SharedString::from(keycode::decode_keycode(a))).collect::>() + actions.iter().map(|&a| TapDanceAction { + name: SharedString::from(keycode::decode_keycode(a)), + code: a as i32, + }).collect::>() ))), }) .collect(); diff --git a/ui/globals.slint b/ui/globals.slint index 6fbcbbd..47e77a2 100644 --- a/ui/globals.slint +++ b/ui/globals.slint @@ -121,9 +121,14 @@ export global StatsBridge { // ---- Advanced ---- +export struct TapDanceAction { + name: string, + code: int, +} + export struct TapDanceData { index: int, - actions: [string], + actions: [TapDanceAction], } export struct ComboData { @@ -183,6 +188,11 @@ export global AdvancedBridge { in-out property new-leader-result-code: 0; in-out property new-leader-result-name: "Pick..."; in-out property new-leader-mod-idx: 0; + // TD editing: stores which TD slot+action is being edited + in-out property editing-td-index: -1; + in-out property editing-td-slot: -1; // 0-3 = which action + callback save-td(int, int, int, int, int); // index, a1, a2, a3, a4 (keycodes) + callback edit-td-action(int, int); // td_index, action_slot -> opens popup callback refresh-advanced(); callback delete-combo(int); callback delete-leader(int); diff --git a/ui/tabs/tab_advanced.slint b/ui/tabs/tab_advanced.slint index 8c7e2aa..0758397 100644 --- a/ui/tabs/tab_advanced.slint +++ b/ui/tabs/tab_advanced.slint @@ -88,14 +88,36 @@ export component TabAdvanced inherits Rectangle { spacing: 4px; SectionHeader { text: "Tap Dance"; } if AdvancedBridge.tap-dances.length == 0 : Text { text: "No tap dances"; color: Theme.fg-secondary; font-size: 11px; } + + Text { text: "1-tap 2-tap 3-tap hold"; color: Theme.fg-secondary; font-size: 10px; } + for td in AdvancedBridge.tap-dances : Rectangle { background: Theme.bg-primary; border-radius: 4px; height: 30px; HorizontalLayout { - padding-left: 8px; padding-right: 8px; spacing: 8px; + padding-left: 8px; padding-right: 8px; spacing: 4px; Text { text: "TD" + td.index; color: Theme.accent-purple; font-size: 11px; vertical-alignment: center; width: 35px; } - for action in td.actions : Text { text: action; color: Theme.fg-primary; font-size: 11px; vertical-alignment: center; } + for action[a-idx] in td.actions : Rectangle { + width: 60px; + height: 24px; + border-radius: 3px; + background: td-action-ta.has-hover ? Theme.button-hover : Theme.button-bg; + border-width: 1px; + border-color: Theme.button-border; + + Text { text: action.name; color: Theme.fg-primary; font-size: 10px; horizontal-alignment: center; vertical-alignment: center; } + + td-action-ta := TouchArea { + clicked => { + AdvancedBridge.editing-td-index = td.index; + AdvancedBridge.editing-td-slot = a-idx; + KeymapBridge.selector-target = "td-action"; + KeymapBridge.key-selector-open = true; + } + mouse-cursor: pointer; + } + } } } }