@@ -19,7 +19,6 @@ const BASE_ANGULAR_SPEED: f64 = 0.8; // rad/s
1919const FAST_MULTIPLIER : f64 = 2.0 ; // Shift modifier
2020
2121/// Overlay styling
22- const OVERLAY_MARGIN : f32 = 12.0 ;
2322const OVERLAY_PADDING : f32 = 10.0 ;
2423const OVERLAY_ROUNDING : f32 = 8.0 ;
2524const OVERLAY_BG : egui:: Color32 = egui:: Color32 :: from_rgba_premultiplied ( 20 , 20 , 30 , 220 ) ;
@@ -66,11 +65,13 @@ impl KeyState {
6665}
6766
6867/// Handles keyboard input and publishes Twist via LCM.
68+ /// Must be activated by clicking the overlay before keys are captured.
6969pub struct KeyboardHandler {
7070 publisher : LcmPublisher ,
7171 state : KeyState ,
7272 was_active : bool ,
7373 estop_flash : bool , // true briefly after space pressed
74+ engaged : bool , // true when user has clicked the overlay to activate
7475}
7576
7677impl KeyboardHandler {
@@ -82,29 +83,30 @@ impl KeyboardHandler {
8283 state : KeyState :: new ( ) ,
8384 was_active : false ,
8485 estop_flash : false ,
86+ engaged : false ,
8587 } )
8688 }
8789
8890 /// Process keyboard input from egui and publish Twist if keys are held.
8991 /// Called once per frame from DimosApp.ui().
92+ /// Only captures keys when the overlay has been clicked (engaged).
9093 ///
9194 /// Returns true if any movement key is active (for UI overlay).
9295 pub fn process ( & mut self , ctx : & egui:: Context ) -> bool {
9396 self . estop_flash = false ;
9497
95- // Check if any text widget has focus - if so, skip keyboard capture
96- let text_has_focus = ctx. memory ( |m| m. focused ( ) . is_some ( ) ) ;
97- if text_has_focus {
98+ // If not engaged, don't capture any keys
99+ if !self . engaged {
98100 if self . was_active {
99101 if let Err ( e) = self . publish_stop ( ) {
100- re_log:: warn!( "Failed to send stop command on focus change : {e:?}" ) ;
102+ re_log:: warn!( "Failed to send stop on disengage : {e:?}" ) ;
101103 }
102104 self . was_active = false ;
103105 }
104106 return false ;
105107 }
106108
107- // Update key state from egui input
109+ // Update key state from egui input (engaged flag is the only gate)
108110 self . update_key_state ( ctx) ;
109111
110112 // Check for emergency stop (Space key pressed - one-shot action)
@@ -134,33 +136,85 @@ impl KeyboardHandler {
134136 self . state . any_active ( )
135137 }
136138
137- /// Draw keyboard overlay HUD. Always shown (dim when idle, bright when active).
138- pub fn draw_overlay ( & self , ctx : & egui:: Context ) {
139- egui:: Area :: new ( "keyboard_hud" . into ( ) )
140- . fixed_pos ( egui:: pos2 ( OVERLAY_MARGIN , OVERLAY_MARGIN ) )
139+ /// Draw keyboard overlay HUD at bottom-right of the 3D viewport area.
140+ /// Clickable: clicking the overlay toggles engaged state.
141+ pub fn draw_overlay ( & mut self , ctx : & egui:: Context ) {
142+ let screen_rect = ctx. content_rect ( ) ;
143+ // Default position: bottom-left, just above the timeline bar
144+ let overlay_height = 160.0 ;
145+ let left_margin = 12.0 ;
146+ let bottom_timeline_offset = 120.0 ;
147+ let default_pos = egui:: pos2 (
148+ screen_rect. min . x + left_margin,
149+ screen_rect. max . y - overlay_height - bottom_timeline_offset,
150+ ) ;
151+
152+ let area_response = egui:: Area :: new ( "keyboard_hud" . into ( ) )
153+ . pivot ( egui:: Align2 :: LEFT_BOTTOM )
154+ . default_pos ( default_pos)
155+ . movable ( true )
141156 . order ( egui:: Order :: Foreground )
142- . interactable ( false )
157+ . interactable ( true )
143158 . show ( ctx, |ui| {
144- egui:: Frame :: new ( )
159+ let border_color = if self . engaged {
160+ egui:: Color32 :: from_rgb ( 60 , 180 , 75 ) // green border when active
161+ } else {
162+ egui:: Color32 :: from_rgb ( 80 , 80 , 100 ) // dim border when inactive
163+ } ;
164+
165+ let response = egui:: Frame :: new ( )
145166 . fill ( OVERLAY_BG )
146167 . corner_radius ( egui:: CornerRadius :: same ( OVERLAY_ROUNDING as u8 ) )
147168 . inner_margin ( egui:: Margin :: same ( OVERLAY_PADDING as i8 ) )
169+ . stroke ( egui:: Stroke :: new ( 2.0 , border_color) )
148170 . show ( ui, |ui| {
149171 self . draw_hud_content ( ui) ;
150172 } ) ;
151- } ) ;
173+
174+ // Make the frame rect clickable (Frame doesn't have click sense by default)
175+ let click_response = ui. interact (
176+ response. response . rect ,
177+ ui. id ( ) . with ( "wasd_click" ) ,
178+ egui:: Sense :: click ( ) ,
179+ ) ;
180+
181+ // Force arrow cursor over the entire overlay (overrides label I-beam)
182+ if click_response. hovered ( ) {
183+ ctx. set_cursor_icon ( egui:: CursorIcon :: Default ) ;
184+ }
185+
186+ // Toggle engaged state on click
187+ if click_response. clicked ( ) {
188+ self . engaged = !self . engaged ;
189+ if !self . engaged {
190+ // Send stop when disengaging
191+ if let Err ( e) = self . publish_stop ( ) {
192+ re_log:: warn!( "Failed to send stop on disengage: {e:?}" ) ;
193+ }
194+ self . state . reset ( ) ;
195+ self . was_active = false ;
196+ }
197+ }
198+ } )
199+ . response ;
200+
201+ // Disengage when clicking anywhere outside the overlay
202+ if self . engaged
203+ && !ctx. rect_contains_pointer ( area_response. layer_id , area_response. interact_rect )
204+ && ctx. input ( |i| i. pointer . primary_clicked ( ) )
205+ {
206+ self . engaged = false ;
207+ if let Err ( e) = self . publish_stop ( ) {
208+ re_log:: warn!( "Failed to send stop on outside click: {e:?}" ) ;
209+ }
210+ self . state . reset ( ) ;
211+ self . was_active = false ;
212+ }
152213 }
153214
154215 fn draw_hud_content ( & self , ui : & mut egui:: Ui ) {
155- let active = self . state . any_active ( ) || self . estop_flash ;
156-
157216 // Title
158- let title_color = if active {
159- egui:: Color32 :: WHITE
160- } else {
161- egui:: Color32 :: from_rgb ( 120 , 120 , 140 )
162- } ;
163- ui. label ( egui:: RichText :: new ( "🎮 Keyboard Teleop" ) . color ( title_color) . size ( 13.0 ) ) ;
217+ ui. label ( egui:: RichText :: new ( "Keyboard Teleop" ) . color ( LABEL_COLOR ) . size ( 13.0 ) ) ;
164218 ui. add_space ( 4.0 ) ;
165219
166220 // Key grid: [Q] [W] [E]
@@ -352,6 +406,7 @@ mod tests {
352406 state,
353407 was_active : false ,
354408 estop_flash : false ,
409+ engaged : true ,
355410 } ;
356411 let ( lin_x, lin_y, _, _, _, ang_z) = handler. compute_twist ( ) ;
357412 assert_eq ! ( lin_x, BASE_LINEAR_SPEED ) ;
@@ -368,6 +423,7 @@ mod tests {
368423 state,
369424 was_active : false ,
370425 estop_flash : false ,
426+ engaged : true ,
371427 } ;
372428 let ( lin_x, lin_y, _, _, _, ang_z) = handler. compute_twist ( ) ;
373429 assert_eq ! ( lin_x, 0.0 ) ;
@@ -381,6 +437,7 @@ mod tests {
381437 state,
382438 was_active : false ,
383439 estop_flash : false ,
440+ engaged : true ,
384441 } ;
385442 let ( lin_x, lin_y, _, _, _, ang_z) = handler. compute_twist ( ) ;
386443 assert_eq ! ( lin_x, 0.0 ) ;
@@ -397,6 +454,7 @@ mod tests {
397454 state,
398455 was_active : false ,
399456 estop_flash : false ,
457+ engaged : true ,
400458 } ;
401459 let ( lin_x, lin_y, _, _, _, ang_z) = handler. compute_twist ( ) ;
402460 assert_eq ! ( lin_x, 0.0 ) ;
@@ -410,6 +468,7 @@ mod tests {
410468 state,
411469 was_active : false ,
412470 estop_flash : false ,
471+ engaged : true ,
413472 } ;
414473 let ( lin_x, lin_y, _, _, _, ang_z) = handler. compute_twist ( ) ;
415474 assert_eq ! ( lin_x, 0.0 ) ;
@@ -427,6 +486,7 @@ mod tests {
427486 state,
428487 was_active : false ,
429488 estop_flash : false ,
489+ engaged : true ,
430490 } ;
431491 let ( lin_x, lin_y, _, _, _, ang_z) = handler. compute_twist ( ) ;
432492 assert_eq ! ( lin_x, BASE_LINEAR_SPEED * FAST_MULTIPLIER ) ;
@@ -444,6 +504,7 @@ mod tests {
444504 state,
445505 was_active : false ,
446506 estop_flash : false ,
507+ engaged : true ,
447508 } ;
448509 let ( lin_x, lin_y, _, _, _, ang_z) = handler. compute_twist ( ) ;
449510 assert_eq ! ( lin_x, BASE_LINEAR_SPEED ) ;
@@ -471,6 +532,7 @@ mod tests {
471532 assert ! ( handler. is_ok( ) ) ;
472533 let handler = handler. unwrap ( ) ;
473534 assert ! ( !handler. was_active) ;
535+ assert ! ( !handler. engaged) ;
474536 assert ! ( !handler. state. any_active( ) ) ;
475537 }
476538
@@ -484,6 +546,7 @@ mod tests {
484546 state,
485547 was_active : false ,
486548 estop_flash : false ,
549+ engaged : true ,
487550 } ;
488551 let ( lin_x, lin_y, _, _, _, ang_z) = handler. compute_twist ( ) ;
489552 assert_eq ! ( lin_x, 0.0 ) ;
@@ -498,6 +561,7 @@ mod tests {
498561 state : KeyState :: new ( ) ,
499562 was_active : false ,
500563 estop_flash : false ,
564+ engaged : true ,
501565 } ;
502566 let ( lin_x, lin_y, lin_z, ang_x, ang_y, ang_z) = handler. compute_twist ( ) ;
503567 assert_eq ! ( lin_x, 0.0 ) ;
0 commit comments