From 2e25ade4c9268aa83c7626ebfb122b3b77c763c1 Mon Sep 17 00:00:00 2001 From: ruthwikdasyam Date: Sat, 14 Mar 2026 01:01:30 -0700 Subject: [PATCH 1/5] feat: deag, click on enable --- dimos/src/interaction/keyboard.rs | 88 ++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index b6cdcd809c37..9e7d2b79776a 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -19,7 +19,6 @@ const BASE_ANGULAR_SPEED: f64 = 0.8; // rad/s const FAST_MULTIPLIER: f64 = 2.0; // Shift modifier /// Overlay styling -const OVERLAY_MARGIN: f32 = 12.0; const OVERLAY_PADDING: f32 = 10.0; const OVERLAY_ROUNDING: f32 = 8.0; const OVERLAY_BG: egui::Color32 = egui::Color32::from_rgba_premultiplied(20, 20, 30, 220); @@ -66,11 +65,13 @@ impl KeyState { } /// Handles keyboard input and publishes Twist via LCM. +/// Must be activated by clicking the overlay before keys are captured. pub struct KeyboardHandler { publisher: LcmPublisher, state: KeyState, was_active: bool, estop_flash: bool, // true briefly after space pressed + engaged: bool, // true when user has clicked the overlay to activate } impl KeyboardHandler { @@ -82,29 +83,30 @@ impl KeyboardHandler { state: KeyState::new(), was_active: false, estop_flash: false, + engaged: false, }) } /// Process keyboard input from egui and publish Twist if keys are held. /// Called once per frame from DimosApp.ui(). + /// Only captures keys when the overlay has been clicked (engaged). /// /// Returns true if any movement key is active (for UI overlay). pub fn process(&mut self, ctx: &egui::Context) -> bool { self.estop_flash = false; - // Check if any text widget has focus - if so, skip keyboard capture - let text_has_focus = ctx.memory(|m| m.focused().is_some()); - if text_has_focus { + // If not engaged, don't capture any keys + if !self.engaged { if self.was_active { if let Err(e) = self.publish_stop() { - re_log::warn!("Failed to send stop command on focus change: {e:?}"); + re_log::warn!("Failed to send stop on disengage: {e:?}"); } self.was_active = false; } return false; } - // Update key state from egui input + // Update key state from egui input (engaged flag is the only gate) self.update_key_state(ctx); // Check for emergency stop (Space key pressed - one-shot action) @@ -134,33 +136,71 @@ impl KeyboardHandler { self.state.any_active() } - /// Draw keyboard overlay HUD. Always shown (dim when idle, bright when active). - pub fn draw_overlay(&self, ctx: &egui::Context) { + /// Draw keyboard overlay HUD at bottom-right of the 3D viewport area. + /// Clickable: clicking the overlay toggles engaged state. + pub fn draw_overlay(&mut self, ctx: &egui::Context) { + let screen_rect = ctx.content_rect(); + // Default position: bottom-right of the 3D viewport area + let overlay_width = 140.0; + let overlay_height = 160.0; + let right_panel_offset = 320.0; + let bottom_timeline_offset = 120.0; + let default_pos = egui::pos2( + screen_rect.max.x - overlay_width - right_panel_offset, + screen_rect.max.y - overlay_height - bottom_timeline_offset, + ); + egui::Area::new("keyboard_hud".into()) - .fixed_pos(egui::pos2(OVERLAY_MARGIN, OVERLAY_MARGIN)) + .default_pos(default_pos) + .movable(true) .order(egui::Order::Foreground) - .interactable(false) + .interactable(true) .show(ctx, |ui| { - egui::Frame::new() + let border_color = if self.engaged { + egui::Color32::from_rgb(60, 180, 75) // green border when active + } else { + egui::Color32::from_rgb(80, 80, 100) // dim border when inactive + }; + + let response = egui::Frame::new() .fill(OVERLAY_BG) .corner_radius(egui::CornerRadius::same(OVERLAY_ROUNDING as u8)) .inner_margin(egui::Margin::same(OVERLAY_PADDING as i8)) + .stroke(egui::Stroke::new(2.0, border_color)) .show(ui, |ui| { self.draw_hud_content(ui); }); + + // Make the frame rect clickable (Frame doesn't have click sense by default) + let click_response = ui.interact( + response.response.rect, + ui.id().with("wasd_click"), + egui::Sense::click(), + ).on_hover_cursor(egui::CursorIcon::Default); + + // Toggle engaged state on click + if click_response.clicked() { + self.engaged = !self.engaged; + if !self.engaged { + // Send stop when disengaging + if let Err(e) = self.publish_stop() { + re_log::warn!("Failed to send stop on disengage: {e:?}"); + } + self.state.reset(); + self.was_active = false; + } + re_log::info!( + "Keyboard teleop {}", + if self.engaged { "ENGAGED" } else { "DISENGAGED" } + ); + } }); + } fn draw_hud_content(&self, ui: &mut egui::Ui) { - let active = self.state.any_active() || self.estop_flash; - // Title - let title_color = if active { - egui::Color32::WHITE - } else { - egui::Color32::from_rgb(120, 120, 140) - }; - ui.label(egui::RichText::new("🎮 Keyboard Teleop").color(title_color).size(13.0)); + ui.label(egui::RichText::new("Keyboard Teleop").color(LABEL_COLOR).size(13.0)); ui.add_space(4.0); // Key grid: [Q] [W] [E] @@ -352,6 +392,7 @@ mod tests { state, was_active: false, estop_flash: false, + engaged: true, }; let (lin_x, lin_y, _, _, _, ang_z) = handler.compute_twist(); assert_eq!(lin_x, BASE_LINEAR_SPEED); @@ -368,6 +409,7 @@ mod tests { state, was_active: false, estop_flash: false, + engaged: true, }; let (lin_x, lin_y, _, _, _, ang_z) = handler.compute_twist(); assert_eq!(lin_x, 0.0); @@ -381,6 +423,7 @@ mod tests { state, was_active: false, estop_flash: false, + engaged: true, }; let (lin_x, lin_y, _, _, _, ang_z) = handler.compute_twist(); assert_eq!(lin_x, 0.0); @@ -397,6 +440,7 @@ mod tests { state, was_active: false, estop_flash: false, + engaged: true, }; let (lin_x, lin_y, _, _, _, ang_z) = handler.compute_twist(); assert_eq!(lin_x, 0.0); @@ -410,6 +454,7 @@ mod tests { state, was_active: false, estop_flash: false, + engaged: true, }; let (lin_x, lin_y, _, _, _, ang_z) = handler.compute_twist(); assert_eq!(lin_x, 0.0); @@ -427,6 +472,7 @@ mod tests { state, was_active: false, estop_flash: false, + engaged: true, }; let (lin_x, lin_y, _, _, _, ang_z) = handler.compute_twist(); assert_eq!(lin_x, BASE_LINEAR_SPEED * FAST_MULTIPLIER); @@ -444,6 +490,7 @@ mod tests { state, was_active: false, estop_flash: false, + engaged: true, }; let (lin_x, lin_y, _, _, _, ang_z) = handler.compute_twist(); assert_eq!(lin_x, BASE_LINEAR_SPEED); @@ -471,6 +518,7 @@ mod tests { assert!(handler.is_ok()); let handler = handler.unwrap(); assert!(!handler.was_active); + assert!(!handler.engaged); assert!(!handler.state.any_active()); } @@ -484,6 +532,7 @@ mod tests { state, was_active: false, estop_flash: false, + engaged: true, }; let (lin_x, lin_y, _, _, _, ang_z) = handler.compute_twist(); assert_eq!(lin_x, 0.0); @@ -498,6 +547,7 @@ mod tests { state: KeyState::new(), was_active: false, estop_flash: false, + engaged: true, }; let (lin_x, lin_y, lin_z, ang_x, ang_y, ang_z) = handler.compute_twist(); assert_eq!(lin_x, 0.0); From c582ddd9ba5ba22462bd1ef3cf97c63cab903a43 Mon Sep 17 00:00:00 2001 From: ruthwikdasyam Date: Sat, 14 Mar 2026 01:31:29 -0700 Subject: [PATCH 2/5] feat: click anywhere else to disengage --- dimos/src/interaction/keyboard.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index 9e7d2b79776a..88a556c9c042 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -150,7 +150,7 @@ impl KeyboardHandler { screen_rect.max.y - overlay_height - bottom_timeline_offset, ); - egui::Area::new("keyboard_hud".into()) + let area_response = egui::Area::new("keyboard_hud".into()) .default_pos(default_pos) .movable(true) .order(egui::Order::Foreground) @@ -176,7 +176,12 @@ impl KeyboardHandler { response.response.rect, ui.id().with("wasd_click"), egui::Sense::click(), - ).on_hover_cursor(egui::CursorIcon::Default); + ); + + // Force arrow cursor over the entire overlay (overrides label I-beam) + if click_response.hovered() { + ctx.set_cursor_icon(egui::CursorIcon::Default); + } // Toggle engaged state on click if click_response.clicked() { @@ -194,8 +199,22 @@ impl KeyboardHandler { if self.engaged { "ENGAGED" } else { "DISENGAGED" } ); } - }); - + }) + .response; + + // Disengage when clicking anywhere outside the overlay + if self.engaged + && !ctx.rect_contains_pointer(area_response.layer_id, area_response.interact_rect) + && ctx.input(|i| i.pointer.primary_clicked()) + { + self.engaged = false; + if let Err(e) = self.publish_stop() { + re_log::warn!("Failed to send stop on outside click: {e:?}"); + } + self.state.reset(); + self.was_active = false; + re_log::info!("Keyboard teleop DISENGAGED (clicked outside)"); + } } fn draw_hud_content(&self, ui: &mut egui::Ui) { From 2c89c56f2e6f51346993bde0fe9e05fda2f7d7e8 Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 14 Mar 2026 12:51:26 -0700 Subject: [PATCH 3/5] fix: remove logging --- dimos/src/interaction/keyboard.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index 88a556c9c042..b3f631b711ec 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -194,10 +194,6 @@ impl KeyboardHandler { self.state.reset(); self.was_active = false; } - re_log::info!( - "Keyboard teleop {}", - if self.engaged { "ENGAGED" } else { "DISENGAGED" } - ); } }) .response; @@ -213,7 +209,6 @@ impl KeyboardHandler { } self.state.reset(); self.was_active = false; - re_log::info!("Keyboard teleop DISENGAGED (clicked outside)"); } } From 51bb991e67e044c8d7f0f82947686f928dcbebea Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 14 Mar 2026 12:57:40 -0700 Subject: [PATCH 4/5] fix: reposition default --- dimos/src/interaction/keyboard.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index b3f631b711ec..67afe4e39c31 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -140,13 +140,12 @@ impl KeyboardHandler { /// Clickable: clicking the overlay toggles engaged state. pub fn draw_overlay(&mut self, ctx: &egui::Context) { let screen_rect = ctx.content_rect(); - // Default position: bottom-right of the 3D viewport area - let overlay_width = 140.0; + // Default position: bottom-left, just above the timeline bar let overlay_height = 160.0; - let right_panel_offset = 320.0; + let left_margin = 12.0; let bottom_timeline_offset = 120.0; let default_pos = egui::pos2( - screen_rect.max.x - overlay_width - right_panel_offset, + screen_rect.min.x + left_margin, screen_rect.max.y - overlay_height - bottom_timeline_offset, ); From ac0ae0983074c96a380805e1c3682aa77d0b4ca4 Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 14 Mar 2026 13:16:51 -0700 Subject: [PATCH 5/5] fix: default position --- dimos/src/interaction/keyboard.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/dimos/src/interaction/keyboard.rs b/dimos/src/interaction/keyboard.rs index 67afe4e39c31..1e7d51f3c71f 100644 --- a/dimos/src/interaction/keyboard.rs +++ b/dimos/src/interaction/keyboard.rs @@ -150,6 +150,7 @@ impl KeyboardHandler { ); let area_response = egui::Area::new("keyboard_hud".into()) + .pivot(egui::Align2::LEFT_BOTTOM) .default_pos(default_pos) .movable(true) .order(egui::Order::Foreground)