mujoco_rs/
viewer.rs

1//! Module related to implementation of the [`MjViewer`]. For implementation of the C++ wrapper,
2//! see [`crate::cpp_viewer::MjViewerCpp`].
3#[cfg(feature = "viewer-ui")] use glutin::display::GetGlDisplay;
4use glutin::prelude::PossiblyCurrentGlContext;
5use glutin::surface::GlSurface;
6
7use winit::event::{ElementState, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, WindowEvent};
8use winit::platform::pump_events::EventLoopExtPumpEvents;
9use winit::keyboard::{KeyCode, PhysicalKey};
10use winit::event_loop::EventLoop;
11use winit::dpi::PhysicalPosition;
12use winit::window::Fullscreen;
13
14use std::time::{Duration, Instant};
15use std::error::Error;
16use std::fmt::Display;
17use std::ops::Deref;
18
19use bitflags::bitflags;
20
21use crate::prelude::{MjrContext, MjrRectangle, MjtFont, MjtGridPos};
22use crate::render_base::{GlState, RenderBase, sync_geoms};
23use crate::wrappers::mj_primitive::MjtNum;
24use crate::wrappers::mj_visualization::*;
25use crate::wrappers::mj_model::MjModel;
26use crate::wrappers::mj_data::MjData;
27use crate::get_mujoco_version;
28
29
30#[cfg(feature = "viewer-ui")]
31mod ui;
32
33
34/****************************************** */
35// Rust native viewer
36/****************************************** */
37const MJ_VIEWER_DEFAULT_SIZE_PX: (u32, u32) = (1280, 720);
38const DOUBLE_CLICK_WINDOW_MS: u128 = 250;
39const TOUCH_BAR_ZOOM_FACTOR: f64 = 0.1;
40
41/// How much extra room to create in the internal [`MjvScene`]. Useful for drawing labels, etc.
42pub(crate) const EXTRA_SCENE_GEOM_SPACE: usize = 2000;
43
44const HELP_MENU_TITLES: &str = concat!(
45    "Toggle help\n",
46    "Toggle full screen\n",
47    "Free camera\n",
48    "Track camera\n",
49    "Camera orbit\n",
50    "Camera pan\n",
51    "Camera look at\n",
52    "Zoom\n",
53    "Object select\n",
54    "Selection rotate\n",
55    "Selection translate\n",
56    "Exit\n",
57    "Reset simulation\n",
58    "Cycle cameras\n",
59    "Visualization toggles",
60);
61
62const HELP_MENU_VALUES: &str = concat!(
63    "F1\n",
64    "F5\n",
65    "Escape\n",
66    "Control + Alt + double-left click\n",
67    "Left drag\n",
68    "Right [+Shift] drag\n",
69    "Alt + double-left click\n",
70    "Zoom, middle drag\n",
71    "Double-left click\n",
72    "Control + [Shift] + drag\n",
73    "Control + Alt + [Shift] + drag\n",
74    "Control + Q\n",
75    "Backspace\n",
76    "[ ]\n",
77    "See MjViewer docs"
78);
79
80#[derive(Debug)]
81pub enum MjViewerError {
82    EventLoopError(winit::error::EventLoopError),
83    GlutinError(glutin::error::Error)
84}
85
86impl Display for MjViewerError {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            Self::EventLoopError(e) => write!(f, "failed to initialize event_loop: {}", e),
90            Self::GlutinError(e) => write!(f, "glutin raised an error: {}", e)
91        }
92    }
93}
94
95impl Error for MjViewerError {
96    fn source(&self) -> Option<&(dyn Error + 'static)> {
97        match self {
98            Self::EventLoopError(e) => Some(e),
99            Self::GlutinError(e) => Some(e)
100        }
101    }
102}
103
104/// A Rust-native implementation of the MuJoCo viewer. To confirm to rust safety rules,
105/// the viewer doesn't store a mutable reference to the [`MjData`] struct, but it instead
106/// accepts it as a parameter at its methods.
107/// 
108/// The [`MjViewer::sync`] method must be called to sync the state of [`MjViewer`] and [`MjData`].
109/// 
110/// # Shortcuts
111/// Main keyboard and mouse shortcuts can be viewed by pressing ``F1``.
112/// Additionally, some visualization toggles are included, but not displayed
113/// in the ``F1`` help menu:
114/// - C: camera,
115/// - U: actuator,
116/// - J: joint,
117/// - M: center of mass,
118/// - H: convex hull,
119/// - Z: light,
120/// - T: transparent,
121/// - I: inertia,
122/// - E: constraint.
123/// 
124/// # Safety
125/// Due to the nature of OpenGL, this should only be run in the **main thread**.
126#[derive(Debug)]
127pub struct MjViewer<M: Deref<Target = MjModel> + Clone> {
128    /* MuJoCo rendering */
129    scene: MjvScene<M>,
130    context: MjrContext,
131    camera: MjvCamera,
132
133    /* Other MuJoCo related */
134    model: M,
135    pert: MjvPerturb,
136    opt: MjvOption,
137
138    /* Internal state */
139    last_x: f64,
140    last_y: f64,
141    last_bnt_press_time: Instant,
142    rect_view: MjrRectangle,
143    rect_full: MjrRectangle,
144
145    /* OpenGL */
146    adapter: RenderBase,
147    event_loop: EventLoop<()>,
148    modifiers: Modifiers,
149    buttons_pressed: ButtonsPressed,
150    raw_cursor_position: (f64, f64),
151
152    /* External interaction */
153    user_scene: MjvScene<M>,
154
155    /* User interface */
156    #[cfg(feature = "viewer-ui")]
157    ui: ui::ViewerUI<M>,
158
159    status: ViewerStatusBit
160}
161
162impl<M: Deref<Target = MjModel> + Clone> MjViewer<M> {
163    /// Launches the MuJoCo viewer. A [`Result`] struct is returned that either contains
164    /// [`MjViewer`] or a [`MjViewerError`]. The `max_user_geom` parameter
165    /// defines how much space will be allocated for additional, user-defined visual-only geoms.
166    /// It can thus be set to 0 if no additional geoms will be drawn by the user.
167    pub fn launch_passive(model: M, max_user_geom: usize) -> Result<Self, MjViewerError> {
168        let (w, h) = MJ_VIEWER_DEFAULT_SIZE_PX;
169        let mut event_loop = EventLoop::new().map_err(MjViewerError::EventLoopError)?;
170        let adapter = RenderBase::new(
171            w, h,
172            format!("MuJoCo Rust Viewer (MuJoCo {})", get_mujoco_version()),
173            &mut event_loop,
174            true  // process events
175        );
176
177        /* Initialize the OpenGL related things */
178        let GlState {
179            gl_context,
180            gl_surface,
181            #[cfg(feature = "viewer-ui")] window,
182            ..
183        } = adapter.state.as_ref().unwrap();
184        gl_context.make_current(gl_surface).map_err(MjViewerError::GlutinError)?;
185        gl_surface.set_swap_interval(gl_context, glutin::surface::SwapInterval::DontWait).map_err(
186            |e| MjViewerError::GlutinError(e)
187        )?;
188        event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
189
190        let ngeom = model.ffi().ngeom as usize;
191        let scene = MjvScene::new(model.clone(), ngeom + max_user_geom + EXTRA_SCENE_GEOM_SPACE);
192        let user_scene = MjvScene::new(model.clone(), max_user_geom);
193        let context = MjrContext::new(&model);
194        let camera  = MjvCamera::new_free(&model);
195
196        #[cfg(feature = "viewer-ui")]
197        let ui = ui::ViewerUI::new(model.clone(), &window, &gl_surface.display());
198
199        Ok(Self {
200            model,
201            scene,
202            context,
203            camera,
204            pert: MjvPerturb::default(),
205            opt: MjvOption::default(),
206            user_scene,
207            last_x: 0.0,
208            last_y: 0.0,
209            last_bnt_press_time: Instant::now(),
210            rect_view: MjrRectangle::default(),
211            rect_full: MjrRectangle::default(),
212            adapter,
213            event_loop,
214            modifiers: Modifiers::default(),
215            buttons_pressed: ButtonsPressed::empty(),
216            raw_cursor_position: (0.0, 0.0),
217            #[cfg(feature = "viewer-ui")] ui,
218            #[cfg(feature = "viewer-ui")] status: ViewerStatusBit::UI,
219            #[cfg(not(feature = "viewer-ui"))] status: ViewerStatusBit::HELP,
220        })
221    }
222
223    /// Checks whether the window is still open.
224    pub fn running(&self) -> bool {
225        self.adapter.running
226    }
227
228    /// Returns an immutable reference to a user scene for drawing custom visual-only geoms.
229    /// Geoms in the user scene are preserved between calls to [`MjViewer::sync`].
230    pub fn user_scene(&self) -> &MjvScene<M>{
231        &self.user_scene
232    }
233
234    /// Returns a mutable reference to a user scene for drawing custom visual-only geoms.
235    /// Geoms in the user scene are preserved between calls to [`MjViewer::sync`].
236    pub fn user_scene_mut(&mut self) -> &mut MjvScene<M>{
237        &mut self.user_scene
238    }
239
240    #[deprecated(since = "1.3.0", note = "use user_scene")]
241    pub fn user_scn(&self) -> &MjvScene<M> {
242        self.user_scene()
243    }
244
245    #[deprecated(since = "1.3.0", note = "use user_scene_mut")]
246    pub fn user_scn_mut(&mut self) -> &mut MjvScene<M> {
247        self.user_scene_mut()
248    }
249
250    /// Syncs the state of `data` with the viewer as well as perform
251    /// rendering on the viewer.
252    pub fn sync(&mut self, data: &mut MjData<M>) {
253        let GlState {
254            gl_context,
255            gl_surface,
256            ..
257        } = self.adapter.state.as_mut().unwrap();
258
259        /* Make sure everything is done on the viewer's window */
260        gl_context.make_current(gl_surface).expect("could not make OpenGL context current");
261
262        /* Read the screen size */
263        self.update_rectangles(self.adapter.state.as_ref().unwrap().window.inner_size().into());
264
265        /* Process mouse and keyboard events */
266        self.process_events(data);
267
268
269        /* Update the scene from data and render */
270        self.update_scene(data);
271
272        /* Draw the user menu on top */
273        #[cfg(feature = "viewer-ui")]
274        self.process_user_ui(data);
275
276        /* Update the user menu state and overlays */
277        self.update_menus();
278
279        /* Swap OpenGL buffers */
280        self.render();
281
282        /* Apply perturbations */
283        self.pert.apply(&self.model, data);
284    }
285
286    /// Renders the drawn content by swapping buffers.
287    fn render(&mut self) {
288        /* Display the drawn content */
289        let GlState {
290            gl_context,
291            gl_surface, ..
292        } = self.adapter.state.as_mut().unwrap();
293
294        gl_surface.swap_buffers(gl_context).expect("buffer swap in OpenGL failed");
295    }
296
297    /// Updates the scene and draws it to the display.
298    fn update_scene(&mut self, data: &mut MjData<M>) {
299        let model_data_ptr = unsafe {  data.model().__raw() };
300        let bound_model_ptr = unsafe { self.model.__raw() };
301        assert_eq!(model_data_ptr, bound_model_ptr, "'data' must be created from the same model as the viewer.");
302
303        /* Update the scene from the MjData state */
304        self.scene.update(data, &self.opt, &self.pert, &mut self.camera);
305
306        /* Draw user scene geoms */
307        sync_geoms(&self.user_scene, &mut self.scene)
308            .expect("could not sync the user scene with the internal scene; this is a bug, please report it.");
309
310        self.scene.render(&self.rect_full, &self.context);
311    }
312
313    /// Draws the user menu
314    fn update_menus(&mut self) {
315        let mut rectangle = self.rect_view;
316        rectangle.width = rectangle.width - rectangle.width / 4;
317
318        /* Overlay section */
319        if self.status.contains(ViewerStatusBit::HELP) {  // Help
320            self.context.overlay(
321                MjtFont::mjFONT_NORMAL, MjtGridPos::mjGRID_TOPLEFT,
322                rectangle,
323                HELP_MENU_TITLES,
324                Some(HELP_MENU_VALUES)
325            );
326        }
327    }
328
329
330    /// Draws the user UI
331    #[cfg(feature = "viewer-ui")]
332    fn process_user_ui(&mut self, data: &mut MjData<M>) {
333        /* Draw the user interface */
334
335        use crate::viewer::ui::UiEvent;
336        let GlState { window, .. } = &self.adapter.state.as_ref().unwrap();
337        let inner_size = window.inner_size();
338        let left = self.ui.process(
339            window, &mut self.status,
340            &mut self.scene, &mut self.opt,
341            &mut self.camera, data
342        );
343
344        /* Adjust the viewport so MuJoCo doesn't draw over the UI */
345        self.rect_view.left = left as i32;
346        self.rect_view.width = inner_size.width as i32;
347
348        /* Reset some OpenGL settings so that MuJoCo can still draw */
349        self.ui.reset();
350
351        /* Process events made in the user UI */
352        while let Some(event) = self.ui.drain_events() {
353            use UiEvent::*;
354            match event {
355                Close => self.adapter.running = false,
356                Fullscreen => self.toggle_full_screen(),
357                ResetSimulation => {
358                    data.reset();
359                    data.forward();
360                },
361                AlignCamera => {
362                    self.camera = MjvCamera::new_free(&self.model);
363                }
364            }
365        }
366    }
367
368    /// Updates the dimensions of the rectangles defining the dimensions of
369    /// the user interface, as well as the actual scene viewer.
370    fn update_rectangles(&mut self, viewport_size: (i32, i32)) {
371        // The scene (middle) rectangle
372        self.rect_view.width = viewport_size.0;
373        self.rect_view.height = viewport_size.1;
374
375        self.rect_full.width = viewport_size.0;
376        self.rect_full.height = viewport_size.1;
377    }
378
379    /// Processes user input events.
380    fn process_events(&mut self, data: &mut MjData<M>) {
381        self.event_loop.pump_app_events(Some(Duration::ZERO), &mut self.adapter);
382        while let Some(window_event) = self.adapter.queue.pop_front() {
383            #[cfg(feature = "viewer-ui")]
384            {
385                let window: &winit::window::Window = &self.adapter.state.as_ref().unwrap().window;
386                self.ui.handle_events(window, &window_event);
387            }
388
389            match window_event {
390                WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers,
391                WindowEvent::MouseInput {state, button, .. } => {
392                    let is_pressed = state == ElementState::Pressed;
393                    
394                    #[cfg(feature = "viewer-ui")]
395                    if self.ui.covered() && is_pressed {
396                        continue;
397                    }
398
399                    let index = match button {
400                        MouseButton::Left => {
401                            self.process_left_click(data, state);
402                            ButtonsPressed::LEFT
403                        },
404                        MouseButton::Middle => ButtonsPressed::MIDDLE,
405                        MouseButton::Right => ButtonsPressed::RIGHT,
406                        _ => return
407                    };
408
409                    self.buttons_pressed.set(index, is_pressed);
410                }
411
412                WindowEvent::CursorMoved { position, .. } => {
413                    let PhysicalPosition { x, y } = position;
414
415                    // The UI might not be detected as covered as dragging can happen slightly outside
416                    // of a (popup) window. This might seem like an ad-hoc solution, but is at the time the
417                    // shortest and most efficient one.
418                    #[cfg(feature = "viewer-ui")]
419                    if self.ui.dragged() {
420                        continue;
421                    }
422
423                    self.process_cursor_pos(x, y, data);
424                }
425
426                // Set the viewer's state to pending exit.
427                WindowEvent::KeyboardInput {
428                    event: KeyEvent {
429                        physical_key: PhysicalKey::Code(KeyCode::KeyQ),
430                        state: ElementState::Pressed, ..
431                    }, ..
432                } if self.modifiers.state().control_key()  => {
433                    self.adapter.running = false;
434                }
435
436                // Free the camera from tracking.
437                WindowEvent::KeyboardInput {
438                    event: KeyEvent {
439                        physical_key: PhysicalKey::Code(KeyCode::Escape),
440                        state: ElementState::Pressed, ..
441                    }, ..
442                } => {
443                    #[cfg(feature = "viewer-ui")]
444                    if self.ui.focused() {
445                        continue;
446                    }
447                    self.camera.free();
448                }
449
450                // Toggle help menu
451                WindowEvent::KeyboardInput {
452                    event: KeyEvent {
453                        physical_key: PhysicalKey::Code(KeyCode::F1),
454                        state: ElementState::Pressed, ..
455                    }, ..
456                } => {
457                    self.status.toggle(ViewerStatusBit::HELP);
458                }
459
460                // Full screen
461                WindowEvent::KeyboardInput {
462                    event: KeyEvent {
463                        physical_key: PhysicalKey::Code(KeyCode::F5),
464                        state: ElementState::Pressed, ..
465                    }, ..
466                } => {
467                    self.toggle_full_screen();
468                }
469
470                // Reset the simulation (the data).
471                WindowEvent::KeyboardInput {
472                    event: KeyEvent {
473                        physical_key: PhysicalKey::Code(KeyCode::Backspace),
474                        state: ElementState::Pressed, ..
475                    }, ..
476                } => {
477                    #[cfg(feature = "viewer-ui")]
478                    if self.ui.focused() {
479                        continue;
480                    }
481
482                    data.reset();
483                    data.forward();
484                }
485
486                // Cycle to the next camera
487                WindowEvent::KeyboardInput {
488                    event: KeyEvent {
489                        physical_key: PhysicalKey::Code(KeyCode::BracketRight),
490                        state: ElementState::Pressed, ..
491                    }, ..
492                } => {
493                    self.cycle_camera(1);
494                }
495
496                // Cycle to the previous camera
497                WindowEvent::KeyboardInput {
498                    event: KeyEvent {
499                        physical_key: PhysicalKey::Code(KeyCode::BracketLeft),
500                        state: ElementState::Pressed, ..
501                    }, ..
502                } => {
503                    self.cycle_camera(-1);
504                }
505
506                // Toggles camera visualization.
507                WindowEvent::KeyboardInput {
508                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyC), state: ElementState::Pressed, ..},
509                    ..
510                } => self.toggle_opt_flag(MjtVisFlag::mjVIS_CAMERA),
511
512                WindowEvent::KeyboardInput {
513                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyU), state: ElementState::Pressed, ..},
514                    ..
515                } => self.toggle_opt_flag(MjtVisFlag::mjVIS_ACTUATOR),
516
517                WindowEvent::KeyboardInput {
518                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyJ), state: ElementState::Pressed, ..},
519                    ..
520                } => self.toggle_opt_flag(MjtVisFlag::mjVIS_JOINT),
521
522                WindowEvent::KeyboardInput {
523                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyM), state: ElementState::Pressed, ..},
524                    ..
525                } => self.toggle_opt_flag(MjtVisFlag::mjVIS_COM),
526
527                WindowEvent::KeyboardInput {
528                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyH), state: ElementState::Pressed, ..},
529                    ..
530                } => self.toggle_opt_flag(MjtVisFlag::mjVIS_CONVEXHULL),
531
532                WindowEvent::KeyboardInput {
533                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyZ), state: ElementState::Pressed, ..},
534                    ..
535                } => self.toggle_opt_flag(MjtVisFlag::mjVIS_LIGHT),
536
537                WindowEvent::KeyboardInput {
538                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyT), state: ElementState::Pressed, ..},
539                    ..
540                } => self.toggle_opt_flag(MjtVisFlag::mjVIS_TRANSPARENT),
541
542                WindowEvent::KeyboardInput {
543                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyI), state: ElementState::Pressed, ..},
544                    ..
545                } => self.toggle_opt_flag(MjtVisFlag::mjVIS_INERTIA),
546
547                WindowEvent::KeyboardInput {
548                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyE), state: ElementState::Pressed, ..},
549                    ..
550                } => self.toggle_opt_flag(MjtVisFlag::mjVIS_CONSTRAINT),
551
552                #[cfg(feature = "viewer-ui")]
553                WindowEvent::KeyboardInput {
554                    event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyX), state: ElementState::Pressed, ..},
555                    ..
556                } => self.status.toggle(ViewerStatusBit::UI),
557
558                // Zoom in/out
559                WindowEvent::MouseWheel {delta, ..} => {
560                    #[cfg(feature = "viewer-ui")]
561                    if self.ui.covered() {
562                        continue;
563                    }
564
565                    let value = match delta {
566                        MouseScrollDelta::LineDelta(_, down) => down as f64,
567                        MouseScrollDelta::PixelDelta(PhysicalPosition {y, ..}) => y * TOUCH_BAR_ZOOM_FACTOR
568                    };
569                    self.process_scroll(value);
570                }
571
572                _ => {}  // ignore other events
573            }
574        }
575    }
576
577    /// Toggles visualization options.
578    fn toggle_opt_flag(&mut self, flag: MjtVisFlag) {
579        let index = flag as usize;
580        self.opt.flags[index] = 1 - self.opt.flags[index];
581    }
582
583    /// Cycle MJCF defined cameras.
584    fn cycle_camera(&mut self, direction: i32) {
585        let n_cam = self.model.ffi().ncam;
586        if n_cam == 0 {  // No cameras, ignore.
587            return;
588        }
589
590        self.camera.fix((self.camera.fixedcamid + direction).rem_euclid(n_cam) as u32);
591    }
592
593    /// Toggles full screen mode.
594    fn toggle_full_screen(&mut self) {
595        let window = &self.adapter.state.as_ref().unwrap().window;
596        if window.fullscreen().is_some() {
597            window.set_fullscreen(None);
598        }
599        else {
600            window.set_fullscreen(Some(Fullscreen::Borderless(None)));
601        }
602    }
603
604    /// Processes scrolling events.
605    fn process_scroll(&mut self, change: f64) {
606        self.camera.move_(MjtMouse::mjMOUSE_ZOOM, &self.model, 0.0, -0.05 * change, &self.scene);
607    }
608
609    /// Processes camera and perturbation movements.
610    fn process_cursor_pos(&mut self, x: f64, y: f64, data: &mut MjData<M>) {
611        self.raw_cursor_position = (x, y);
612        /* Calculate the change in mouse position since last call */
613        let dx = x - self.last_x;
614        let dy = y - self.last_y;
615        self.last_x = x;
616        self.last_y = y;
617        let window = &self.adapter.state.as_ref().unwrap().window;
618        let modifiers = &self.modifiers.state();
619        let buttons = &self.buttons_pressed;
620        let shift = modifiers.shift_key();
621
622        /* Check mouse presses and move the camera if any of them is pressed */
623        let action;
624        let height = window.outer_size().height as f64;
625        
626        if buttons.contains(ButtonsPressed::LEFT) {
627            if self.pert.active == MjtPertBit::mjPERT_TRANSLATE as i32 {
628                action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
629            }
630            else {
631                action = if shift {MjtMouse::mjMOUSE_ROTATE_H} else {MjtMouse::mjMOUSE_ROTATE_V};
632            }
633        }
634        else if buttons.contains(ButtonsPressed::RIGHT) {
635            action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
636        }
637        else if buttons.contains(ButtonsPressed::MIDDLE) {
638            action = MjtMouse::mjMOUSE_ZOOM;
639        }
640        else {
641            return;  // If buttons aren't pressed, ignore.
642        }
643
644        /* When the perturbation isn't active, move the camera */
645        if self.pert.active == 0 {
646            self.camera.move_(action, &self.model, dx / height, dy / height, &self.scene);
647        }
648        else {  // When the perturbation is active, move apply the perturbation.
649            self.pert.move_(&self.model, data, action, dx / height, dy / height, &self.scene);
650        }
651    }
652
653    /// Processes left clicks and double left clicks.
654    fn process_left_click(&mut self, data: &mut MjData<M>, state: ElementState) {
655        let modifier_state = self.modifiers.state();
656        match state {
657            ElementState::Pressed => {
658                /* Clicking and holding applies perturbation */
659                if self.pert.select > 0 && modifier_state.control_key() {
660                    let type_ = if modifier_state.alt_key() {
661                        MjtPertBit::mjPERT_TRANSLATE
662                    } else {
663                        MjtPertBit::mjPERT_ROTATE
664                    };
665                    self.pert.start(type_, &self.model, data, &self.scene);
666                }
667
668                /* Double click detection */
669                if self.last_bnt_press_time.elapsed().as_millis() < DOUBLE_CLICK_WINDOW_MS {
670                    let cp = self.raw_cursor_position;
671                    let x = cp.0;
672                    let y = self.rect_full.height as f64 - cp.1;
673
674                    /* Obtain the selection */ 
675                    let rect = &self.rect_full;
676                    let (body_id, _, flex_id, skin_id, xyz) = self.scene.find_selection(
677                        data, &self.opt,
678                        rect.width as MjtNum / rect.height as MjtNum,
679                        (x - rect.left as MjtNum) / rect.width as MjtNum,
680                        (y - rect.bottom as MjtNum) / rect.height as MjtNum
681                    );
682
683                    /* Set tracking camera */
684                    if modifier_state.alt_key() {
685                        if body_id >= 0 {
686                            self.camera.lookat = xyz;
687                            if modifier_state.control_key() {
688                                self.camera.track(body_id as u32);
689                            }
690                        }
691                    }
692                    else {
693                        /* Mark selection */
694                        if body_id >= 0 {
695                            self.pert.select = body_id;
696                            self.pert.flexselect = flex_id;
697                            self.pert.skinselect = skin_id;
698                            self.pert.active = 0;
699                            self.pert.update_local_pos(xyz, data);
700                        }
701                        else {
702                            self.pert.select = 0;
703                            self.pert.flexselect = -1;
704                            self.pert.skinselect = -1;
705                        }
706                    }
707                }
708                self.last_bnt_press_time = Instant::now();
709            },
710            ElementState::Released => {
711                // Clear perturbation when left click is released.
712                self.pert.active = 0;
713            },
714        };
715    }
716}
717
718bitflags! {
719    #[derive(Debug)]
720    struct ViewerStatusBit: u8 {
721        const HELP = 1 << 0;
722        #[cfg(feature = "viewer-ui")]
723        const UI = 1 << 1;
724    }
725}
726
727bitflags! {
728    /// Boolean flags for tracking button press events.
729    #[derive(Debug)]
730    struct ButtonsPressed: u8 {
731        const LEFT = 1 << 0;
732        const MIDDLE = 1 << 1;
733        const RIGHT = 1 << 2;
734    }
735}