Skip to content

Commit 0e6e1a4

Browse files
feat(viewer): click-to-engage overlay + restore click-to-nav (#19)
* feat: deag, click on enable * fix: remove logging * fix: reposition default * fix: default position * fix(viewer): restore click-to-nav via StartupOptionsPatch PR #14's run_with_app_wrapper rewrote viewer.rs and dropped the on_event handler for click-to-navigate. This restores it properly: - Add StartupOptionsPatch struct to rerun entrypoint with on_event callback - Wire Ctrl+click -> PointStamped LCM on /clicked_point in viewer.rs - Arc<AtomicBool> for ctrl state sharing (Send required by AppWrapper) - 100ms debounce on nav goal publishing --------- Co-authored-by: ruthwikdasyam <ruthwikdasyam@gmail.com>
1 parent 48b6e62 commit 0e6e1a4

File tree

6 files changed

+207
-73
lines changed

6 files changed

+207
-73
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3110,7 +3110,7 @@ dependencies = [
31103110

31113111
[[package]]
31123112
name = "dimos-viewer"
3113-
version = "0.30.0-alpha.4"
3113+
version = "0.30.0-alpha.5"
31143114
dependencies = [
31153115
"bincode",
31163116
"clap",

crates/top/rerun/src/commands/entrypoint.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1708,6 +1708,13 @@ fn record_cli_command_analytics(args: &Args) {
17081708
/// Used by dimos-viewer to inject keyboard teleop and other behaviors.
17091709
pub type AppWrapper = Box<dyn FnOnce(re_viewer::App) -> Result<Box<dyn re_viewer::external::eframe::App>, Box<dyn std::error::Error + Send + Sync>> + Send>;
17101710

1711+
/// Optional patches to [`re_viewer::StartupOptions`] injected by the app wrapper.
1712+
#[derive(Default)]
1713+
pub struct StartupOptionsPatch {
1714+
/// Callback invoked on viewer events (e.g. SelectionChange for click-to-nav).
1715+
pub on_event: Option<std::rc::Rc<dyn Fn(re_viewer::ViewerEvent)>>,
1716+
}
1717+
17111718
/// Like [`run`], but accepts an optional `app_wrapper` callback that wraps the
17121719
/// viewer App before it is handed to eframe. When `app_wrapper` is `None`,
17131720
/// behavior is identical to stock Rerun.
@@ -1720,6 +1727,7 @@ pub fn run_with_app_wrapper<I, T>(
17201727
call_source: CallSource,
17211728
args: I,
17221729
app_wrapper: Option<AppWrapper>,
1730+
startup_patch: Option<StartupOptionsPatch>,
17231731
) -> anyhow::Result<u8>
17241732
where
17251733
I: IntoIterator<Item = T>,
@@ -1812,6 +1820,7 @@ where
18121820
#[cfg(feature = "native_viewer")]
18131821
profiler,
18141822
app_wrapper,
1823+
startup_patch,
18151824
)
18161825
};
18171826

@@ -1838,6 +1847,7 @@ fn run_impl_with_wrapper(
18381847
tokio_runtime_handle: &tokio::runtime::Handle,
18391848
#[cfg(feature = "native_viewer")] profiler: re_tracing::Profiler,
18401849
app_wrapper: Option<AppWrapper>,
1850+
startup_patch: Option<StartupOptionsPatch>,
18411851
) -> anyhow::Result<()> {
18421852
let connection_registry = re_redap_client::ConnectionRegistry::new_with_stored_credentials();
18431853

@@ -1959,6 +1969,7 @@ fn run_impl_with_wrapper(
19591969
#[cfg(feature = "server")]
19601970
server_options,
19611971
app_wrapper,
1972+
startup_patch,
19621973
)
19631974
} else {
19641975
Err(anyhow::anyhow!(
@@ -1985,11 +1996,17 @@ fn start_native_viewer_with_wrapper(
19851996
#[cfg(feature = "server")] server_addr: std::net::SocketAddr,
19861997
#[cfg(feature = "server")] server_options: re_sdk::ServerOptions,
19871998
app_wrapper: Option<AppWrapper>,
1999+
startup_patch: Option<StartupOptionsPatch>,
19882000
) -> anyhow::Result<()> {
19892001
use re_viewer::external::re_viewer_context;
19902002
use crate::external::re_ui::{UICommand, UICommandSender as _};
19912003

1992-
let startup_options = native_startup_options_from_args(args)?;
2004+
let mut startup_options = native_startup_options_from_args(args)?;
2005+
if let Some(patch) = startup_patch {
2006+
if patch.on_event.is_some() {
2007+
startup_options.on_event = patch.on_event;
2008+
}
2009+
}
19932010

19942011
let connect = args.connect.is_some();
19952012
let follow = args.follow;

crates/top/rerun/src/commands/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ mod analytics;
3535

3636
#[cfg(feature = "analytics")]
3737
pub(crate) use self::analytics::AnalyticsCommands;
38-
pub use self::entrypoint::{run, run_with_app_wrapper, AppWrapper, Args as RerunArgs, native_startup_options_from_args};
38+
pub use self::entrypoint::{run, run_with_app_wrapper, AppWrapper, StartupOptionsPatch, Args as RerunArgs, native_startup_options_from_args};
3939
#[cfg(feature = "data_loaders")]
4040
pub use self::mcap::McapCommands;
4141
pub use self::rrd::RrdCommands;

crates/top/rerun/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ pub mod demo_util;
124124
pub mod log_integration;
125125

126126
#[cfg(feature = "run")]
127-
pub use commands::{CallSource, run, run_with_app_wrapper, AppWrapper, RerunArgs, native_startup_options_from_args};
127+
pub use commands::{CallSource, run, run_with_app_wrapper, AppWrapper, StartupOptionsPatch, RerunArgs, native_startup_options_from_args};
128128
#[cfg(feature = "log")]
129129
pub use log_integration::Logger;
130130
#[cfg(feature = "log")]

dimos/src/interaction/keyboard.rs

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const BASE_ANGULAR_SPEED: f64 = 0.8; // rad/s
1919
const FAST_MULTIPLIER: f64 = 2.0; // Shift modifier
2020

2121
/// Overlay styling
22-
const OVERLAY_MARGIN: f32 = 12.0;
2322
const OVERLAY_PADDING: f32 = 10.0;
2423
const OVERLAY_ROUNDING: f32 = 8.0;
2524
const 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.
6969
pub 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

7677
impl 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

Comments
 (0)