stereokit_rust/framework/
hand_menu.rs

1use crate::{
2    material::Material,
3    maths::{Matrix, Plane, Pose, Quat, Vec2, Vec3, lerp, units::CM},
4    mesh::{Inds, Mesh, Vertex},
5    prelude::*,
6    sound::Sound,
7    system::{
8        Align, Backend, BackendXRType, FingerId, Hand, Handed, Hierarchy, Input, JointId, Key, Lines, Text, TextStyle,
9    },
10    tex::Tex,
11    ui::{Ui, UiColor},
12    util::{
13        Color128, Time,
14        named_colors::{GREEN, WHITE},
15    },
16};
17use std::{borrow::BorrowMut, collections::VecDeque};
18
19/// This is a collection of display and behavior information for a single item on the hand menu.
20/// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuItem.html>
21///
22/// see example in [`HandMenuRadial`]
23pub struct HandMenuItem {
24    pub name: String,
25    pub image: Option<Material>,
26    pub action: RefCell<HandMenuAction>,
27    pub callback: RefCell<Box<dyn FnMut()>>,
28}
29
30impl HandMenuItem {
31    /// Makes a menu item!
32    /// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuItem/HandMenuItem.html>
33    /// * `name` - Display name of the item.
34    /// * `image` - Image to display on the item.
35    /// * `callback` - The callback that should be performed when this menu item is selected.
36    /// * `action` - Describes the menu related behavior of this menu item, should it close the menu? Open another
37    ///   layer? Check/Uncheck this item? Go back to the previous menu?
38    pub fn new<C: FnMut() + 'static>(
39        name: impl AsRef<str>,
40        image: Option<Material>,
41        callback: C,
42        action: HandMenuAction,
43    ) -> Self {
44        Self {
45            name: name.as_ref().to_owned(),
46            image,
47            callback: RefCell::new(Box::new(callback)),
48            action: RefCell::<HandMenuAction>::new(action),
49        }
50    }
51
52    /// This draws the menu item on the radial menu!
53    /// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuItem/Draw.html>
54    /// * `token` - The main thread token.
55    /// * `at` - Center of the radial slice.
56    /// * `focused` - If the current menu slice has focus.
57    pub fn draw_basic(&self, token: &MainThreadToken, at: Vec3, focused: bool) {
58        let scale = match focused {
59            true => Vec3::ONE * 0.6,
60            false => Vec3::ONE * 0.5,
61        };
62        Text::add_at(
63            token,
64            &self.name,
65            Matrix::t_s(at, scale),
66            None,
67            None,
68            None,
69            Some(Align::BottomCenter),
70            None,
71            None,
72            None,
73        );
74    }
75}
76
77/// A Cell of the radial menu which can be a [`Layer`](HandRadialLayer) or an [`item`](HandMenuItem).
78///
79/// see example in [`HandMenuRadial`]
80pub enum HandRadial {
81    Item(HandMenuItem),
82    Layer(HandRadialLayer),
83}
84
85impl HandRadial {
86    /// Creates a new [HandRadial] item.
87    /// * `name` - Display name of the item.
88    /// * `image` - Image to display on the item.
89    /// * `callback` - The callback that should be performed when this menu item is selected.
90    /// * `action` - Describes the menu related behavior of this menu item, should it close the menu? Open another
91    ///   layer? Check/Uncheck this item? Go back to the previous menu?
92    pub fn item<C: FnMut() + 'static>(
93        name: impl AsRef<str>,
94        image: Option<Material>,
95        callback: C,
96        action: HandMenuAction,
97    ) -> Self {
98        Self::Item(HandMenuItem::new(name, image, callback, action))
99    }
100
101    /// Creates a new [HandRadial] layer.
102    /// * `name` - Name of the layer, this is used for layer traversal, so make sure you get the spelling right! Perhaps
103    ///   use const strings for these.
104    /// * `image` - Image to display in the center of the radial menu.
105    /// * `start_angle` - An angle offset for the layer, if you want a specific orientation for the menu’s contents. Note
106    ///   this may not behave as expected if you’re setting this manually and using the backAngle as well.
107    /// * `items` - A list of menu items to display in this menu layer.
108    pub fn layer(
109        name: impl AsRef<str>,
110        image: Option<Material>,
111        start_angle: Option<f32>,
112        items: Vec<HandRadial>,
113    ) -> Self {
114        Self::Layer(HandRadialLayer::new(name, image, start_angle, items))
115    }
116
117    /// Returns the number of items in this layer. 0 if this is an item.
118    pub fn items_count(&self) -> usize {
119        match self {
120            HandRadial::Item(_) => 0,
121            HandRadial::Layer(layer) => layer.items.len(),
122        }
123    }
124
125    /// Returns the items in this layer. Empty if this is an item.
126    pub fn items(&self) -> &Vec<Rc<HandRadial>> {
127        match self {
128            HandRadial::Item(_) => panic!("Cannot get items from an item"),
129            HandRadial::Layer(layer) => &layer.items,
130        }
131    }
132
133    /// Returns true if this is an item and it is a back action.
134    pub fn is_back_action(&self) -> bool {
135        match self {
136            HandRadial::Item(item) => {
137                let value = item.action.borrow();
138                *value == HandMenuAction::Back
139            }
140            HandRadial::Layer(_) => false,
141        }
142    }
143
144    /// Returns the group number if this is an item and it is a checked action.
145    pub fn is_checked_action(&self) -> Option<u8> {
146        match self {
147            HandRadial::Item(item) => {
148                let value = item.action.borrow();
149                if let HandMenuAction::Checked(group) = *value { Some(group) } else { None }
150            }
151            HandRadial::Layer(_) => None,
152        }
153    }
154
155    /// Returns the group number if this is an item and it is an unchecked action.
156    pub fn is_unchecked_action(&self) -> Option<u8> {
157        match self {
158            HandRadial::Item(item) => {
159                let value = item.action.borrow();
160                if let HandMenuAction::Unchecked(group) = *value { Some(group) } else { None }
161            }
162            HandRadial::Layer(_) => None,
163        }
164    }
165
166    /// Returns the start angle of the layer. 0.0 if this is an item.
167    pub fn get_start_angle(&self) -> f32 {
168        match self {
169            HandRadial::Item(_) => 0.0,
170            HandRadial::Layer(layer) => layer.start_angle,
171        }
172    }
173
174    /// Returns the back angle of the layer. 0.0 if this is an item.
175    pub fn get_back_angle(&self) -> f32 {
176        match self {
177            HandRadial::Item(_) => 0.0,
178            HandRadial::Layer(layer) => layer.back_angle,
179        }
180    }
181
182    /// Returns the name of the item or layer.
183    pub fn get_name(&self) -> &str {
184        match self {
185            HandRadial::Item(item) => &item.name,
186            HandRadial::Layer(layer) => &layer.layer_name,
187        }
188    }
189}
190
191/// This class represents a single layer in the HandRadialMenu. Each item in the layer is displayed around the radial
192/// menu’s circle.
193/// <https://stereokit.net/Pages/StereoKit.Framework/HandRadialLayer.html>
194///
195/// see example in [`HandMenuRadial`]
196pub struct HandRadialLayer {
197    pub layer_name: String,
198    pub items: Vec<Rc<HandRadial>>,
199    pub start_angle: f32,
200    pub back_angle: f32,
201    parent: Option<String>,
202    layer_item: HandMenuItem,
203}
204
205/// Creates a menu layer, this overload will calculate a back_angle if there are any back actions present in the item
206/// list.
207/// <https://stereokit.net/Pages/StereoKit.Framework/HandRadialLayer/HandRadialLayer.html>
208/// * `name` - Name of the layer, this is used for layer traversal, so make sure you get the spelling right! Perhaps
209///   use const strings for these.
210/// * `image` - Image to display in the center of the radial menu.
211/// * `start_angle` - An angle offset for the layer, if you want a specific orientation for the menu’s contents. Note
212///   this may not behave as expected if you’re setting this manually and using the backAngle as well.
213/// * `items` - A list of menu items to display in this menu layer.
214impl HandRadialLayer {
215    pub fn new(
216        name: impl AsRef<str>,
217        image: Option<Material>,
218        start_angle_opt: Option<f32>,
219        items_in: Vec<HandRadial>,
220    ) -> Self {
221        let name = name.as_ref().to_owned();
222        let mut items = vec![];
223        for item in items_in {
224            items.push(Rc::new(item));
225        }
226
227        let mut back_angle = 0.0;
228        let mut start_angle = 0.0;
229        match start_angle_opt {
230            Some(value) => start_angle = value,
231            None => {
232                let mut i = 0.0;
233                for item in items.iter() {
234                    if item.is_back_action() {
235                        let step = 360.0 / (items.len() as f32);
236                        back_angle = (i + 0.5) * step;
237                    }
238                    i += 1.0;
239                }
240            }
241        }
242
243        Self {
244            layer_name: name.clone(),
245            items,
246            start_angle,
247            back_angle,
248            parent: None,
249            layer_item: HandMenuItem::new(name.clone(), image, || {}, HandMenuAction::Callback),
250        }
251    }
252
253    /// This adds a menu layer as a child item of this layer.
254    /// <https://stereokit.net/Pages/StereoKit.Framework/HandRadialLayer/AddChild.html>
255    pub fn add_child(&mut self, mut layer: HandRadialLayer) -> &mut Self {
256        layer.parent = Some(self.layer_name.clone());
257        self.items.push(Rc::new(HandRadial::Layer(layer)));
258        self
259    }
260
261    /// Find a child menu layer by name. Recursive function
262    /// <https://stereokit.net/Pages/StereoKit.Framework/HandRadialLayer/FindChild.html>
263    pub fn find_child(&self, name: impl AsRef<str>) -> Option<&HandRadialLayer> {
264        for line in self.items.iter() {
265            let line = line.as_ref();
266            match line {
267                HandRadial::Layer(s) => {
268                    if s.layer_name.eq(&name.as_ref().to_string()) {
269                        return Some(s);
270                    } else if let Some(sub_s) = s.find_child(&name) {
271                        return Some(sub_s);
272                    };
273                }
274                HandRadial::Item(_) => {}
275            }
276        }
277
278        None
279    }
280
281    /// Finds the layer in the list of child layers, and removes it, if it exists.
282    /// Not recursive. self must be the layer containing the one to delete
283    /// <https://stereokit.net/Pages/StereoKit.Framework/HandRadialLayer/RemoveChild.html>
284    pub fn remove_child(&mut self, name: impl AsRef<str>) -> bool {
285        for (index, line) in self.items.iter().enumerate() {
286            let line = line.as_ref();
287            match line {
288                HandRadial::Layer(s) => {
289                    if s.layer_name.eq(&name.as_ref().to_string()) {
290                        self.items.remove(index);
291                        return true;
292                    }
293                }
294                HandRadial::Item(_) => {}
295            }
296        }
297
298        false
299    }
300
301    /// This appends a new menu item to the end of the menu’s list.
302    /// <https://stereokit.net/Pages/StereoKit.Framework/HandRadialLayer/AddItem.html>
303    pub fn add_item(&mut self, menu_item: HandMenuItem) -> &mut Self {
304        self.items.push(Rc::new(HandRadial::Item(menu_item)));
305        self
306    }
307
308    /// Find a menu item by name.
309    /// <https://stereokit.net/Pages/StereoKit.Framework/HandRadialLayer/FindItem.html>
310    pub fn find_item(&mut self, name: impl AsRef<str>) -> Option<&HandMenuItem> {
311        for line in self.items.iter() {
312            let line = line.as_ref();
313            match line {
314                HandRadial::Item(s) => {
315                    if s.name.eq(name.as_ref()) {
316                        return Some(s);
317                    }
318                }
319                HandRadial::Layer(_) => {}
320            }
321        }
322
323        None
324    }
325
326    /// Finds the item in the list, and removes it, if it exists.
327    /// <https://stereokit.net/Pages/StereoKit.Framework/HandRadialLayer/RemoveItem.html>
328    pub fn remove_item(&mut self, name: impl AsRef<str>) -> bool {
329        for (index, line) in self.items.iter().enumerate() {
330            let line = line.as_ref();
331            match line {
332                HandRadial::Item(s) => {
333                    if s.name.eq(name.as_ref()) {
334                        self.items.remove(index);
335                        return true;
336                    }
337                }
338                HandRadial::Layer(_) => {}
339            }
340        }
341
342        false
343    }
344}
345
346/// This enum specifies how HandMenuItems should behave
347/// when selected! This is in addition to the HandMenuItem's
348/// callback function.
349/// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuAction.html>
350///
351/// see example in [`HandMenuRadial`]
352#[derive(Copy, Clone, Debug, PartialEq)]
353pub enum HandMenuAction {
354    /// Execute the callback only and stay open (Warning ! this will send multiple time the callback)
355    Callback,
356    /// Go back to the previous layer.
357    Back,
358    /// Close the hand menu entirely! We're finished here.
359    Close,
360    /// Execute the callback only and stay open (Warning ! this will send multiple time the callback)
361    /// Mark the Item as checked (could be changed to Unchecked)
362    Checked(u8),
363    /// Execute the callback only and stay open (Warning ! this will send multiple time the callback)
364    /// Mark the Item as unchecked (could be changed to Checked)
365    Unchecked(u8),
366}
367
368///The way to swap between more than one hand_menu_radial. Use this prefix for your ID to your HandMenuRadial menus
369pub const HAND_MENU_RADIAL: &str = "hand_menu_radial_";
370///If this menu is the one who takes the focus (true) or if he returns the focus on the menu previously active (false)
371pub const HAND_MENU_RADIAL_FOCUS: &str = "hand_menu_radial_focus";
372
373/// A menu that shows up in circle around the user’s hand! Selecting an item can perform an action, or even spawn a
374/// sub-layer of menu items. This is an easy way to store actions out of the way, yet remain easily accessible to the
375/// user.
376///
377/// The user faces their palm towards their head, and then makes a grip motion to spawn the menu. The user can then
378/// perform actions by making fast, direction based motions that are easy to build muscle memory for.
379/// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuRadial.html>
380///
381/// ### Examples
382/// ```
383/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
384/// use stereokit_rust::{framework::*, material::Material, system::{Input, Key}};
385///
386/// // swapping a value
387/// let mut swap_value = true;
388///
389/// // nice icon
390/// let mut menu_ico = Material::pbr_clip()
391///     .tex_file_copy("icons/hamburger.png", true, None).unwrap_or_default();
392/// menu_ico.clip_cutoff(0.1);
393///
394/// //---Create then load hand menu radial
395/// let mut hand_menu_stepper =
396///     HandMenuRadial::new(HandRadialLayer::new("root", None, Some(100.0),
397///     vec![
398///         HandRadial::layer("Todo!", Some(menu_ico), None,
399///             vec![
400///                 HandRadial::item("Back", None, || {}, HandMenuAction::Back),
401///                 HandRadial::item("Close", None, || {}, HandMenuAction::Close),
402///             ],
403///         ),
404///         HandRadial::item("Swap", None,
405///             move || {
406///                 swap_value = !swap_value;
407///             },
408///             HandMenuAction::Checked(1),
409///         ),
410///         HandRadial::item("Close", None, || {}, HandMenuAction::Close),
411///     ],
412/// ));
413/// let id = HandMenuRadial::build_id("1");
414/// SkInfo::send_event(&Some(sk.get_sk_info_clone()),
415///     StepperAction::add(id.clone(), hand_menu_stepper));
416///
417/// number_of_steps=10;
418/// test_steps!(// !!!! Get a proper main loop !!!!
419///     if iter == 1 {
420///         SkInfo::send_event(&Some(sk.get_sk_info_clone()),
421///             StepperAction::event(id.as_str(), HAND_MENU_RADIAL_FOCUS, &true.to_string()));
422///     }
423///     if iter == 8 {
424///         SkInfo::send_event(&Some(sk.get_sk_info_clone()),
425///             StepperAction::remove(id.clone()));
426///     }
427/// );
428/// ```
429#[derive(IStepper)]
430pub struct HandMenuRadial {
431    id: StepperId,
432    sk_info: Option<Rc<RefCell<SkInfo>>>,
433    enabled: bool,
434
435    menu_stack: Vec<String>,
436    menu_pose: Pose,
437    dest_pose: Pose,
438    root: Rc<HandRadial>,
439    active_layer: Rc<HandRadial>,
440    last_selected: Rc<HandRadial>,
441    last_selected_time: f32,
442    nav_stack: VecDeque<Rc<HandRadial>>,
443    active_hand: Handed,
444    activation: f32,
445    menu_scale: f32,
446    angle_offset: f32,
447
448    background: Mesh,
449    background_edge: Mesh,
450    activation_button: Mesh,
451    activation_hamburger: Mesh,
452    activation_ring: Mesh,
453    child_indicator: Mesh,
454    img_frame: Mesh,
455    pub checked_material: Material,
456    pub on_checked_material: Material,
457    pub text_style: TextStyle,
458}
459
460unsafe impl Send for HandMenuRadial {}
461
462impl HandMenuRadial {
463    pub fn build_id(id: &str) -> String {
464        format!("{HAND_MENU_RADIAL}{id}")
465    }
466
467    /// Part of IStepper, you shouldn’t be calling this yourself.
468    /// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuRadial/Initialize.html>
469    fn start(&mut self) -> bool {
470        true
471    }
472
473    /// Called from IStepper::step, here you can check the event report
474    fn check_event(&mut self, id: &StepperId, key: &str, value: &str) {
475        if key == HAND_MENU_RADIAL_FOCUS {
476            if value.parse().unwrap_or_default() {
477                if *id == self.id {
478                    self.enabled = true
479                } else if self.enabled {
480                    self.menu_stack.push(id.clone());
481                    self.enabled = false;
482                }
483            } else if *id == self.id {
484                self.enabled = false
485            } else {
486                if let Some(index) = self.menu_stack.iter().position(|x| *x == *id) {
487                    self.menu_stack.remove(index);
488                }
489                if self.menu_stack.is_empty() {
490                    self.enabled = true;
491                }
492            }
493        }
494    }
495
496    /// Part of IStepper, you shouldn’t be calling this yourself.
497    /// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuRadial/Step.html>
498    fn draw(&mut self, token: &MainThreadToken) {
499        if self.active_hand == Handed::Max {
500            for hand in [Handed::Left, Handed::Right] {
501                self.step_menu_indicator(token, hand);
502            }
503        } else {
504            self.step_menu(token, Input::hand(self.active_hand));
505        }
506    }
507
508    /// When using the Simulator, this key will activate the menu on the current hand, regardless of which direction it
509    /// is facing.
510    pub const SIMULATOR_KEY: Key = Key::F1;
511    pub const MIN_DIST: f32 = 0.03;
512    pub const MID_DIST: f32 = 0.065;
513    pub const MAX_DIST: f32 = 0.1;
514    pub const MIN_SCALE: f32 = 0.05;
515    pub const SLICE_GAP: f32 = 0.002;
516    pub const OUT_OF_VIEW_ANGLE: f32 = 0.866;
517    pub const ACTIVATION_ANGLE: f32 = 0.978;
518
519    /// Creates a hand menu from the provided array of menu layers! HandMenuRadial is an IStepper, so proper usage is to
520    /// add it to the Stepper list via (Sk|SkInfo).send_event(StepperAction::add_default()).
521    /// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuRadial/HandMenuRadial.html>
522    pub fn new(root_layer: HandRadialLayer) -> Self {
523        let root = Rc::new(HandRadial::Layer(root_layer));
524        let active_layer = root.clone();
525        let last_selected = root.clone();
526        let last_selected_time = Time::get_total_unscaledf();
527        let activation_btn_radius = 1.0 * CM;
528        let activation_button = generate_activation_button(activation_btn_radius);
529        let activation_hamburger = generate_activation_hamburger(activation_btn_radius);
530        let mut activation_ring = Mesh::new();
531        generate_slice_mesh(360.0, activation_btn_radius, activation_btn_radius + 0.005, 0.0, &mut activation_ring);
532        let child_indicator = generate_child_indicator(Self::MAX_DIST - 0.008, 0.004);
533        let img_frame = generate_img_frame(Self::MIN_DIST + 0.013, 0.012);
534        let tex_checked = Tex::from_file("icons/radio.png", true, None).unwrap_or_default();
535        let mut checked_material = Material::pbr_clip().copy();
536        checked_material.diffuse_tex(tex_checked).clip_cutoff(0.1);
537        let tex_on_checked = Tex::from_file("icons/checked.png", true, None).unwrap_or_default();
538        let mut on_checked_material = Material::pbr_clip().copy();
539        on_checked_material.diffuse_tex(tex_on_checked).clip_cutoff(0.1).color_tint(GREEN);
540        let mut text_style = TextStyle::default();
541        text_style.layout_height(0.016);
542        Self {
543            id: "HandleMenuRadial_not_initialized".to_string(),
544            sk_info: None,
545            enabled: false,
546
547            menu_stack: Vec::new(),
548            menu_pose: Pose::default(),
549            dest_pose: Pose::default(),
550            root,
551            active_layer,
552            last_selected,
553            last_selected_time,
554            nav_stack: VecDeque::new(),
555            active_hand: Handed::Max,
556            activation: 0.0,
557            menu_scale: 0.0,
558            angle_offset: 0.0,
559            background: Mesh::new(),
560            background_edge: Mesh::new(),
561            activation_button,
562            activation_hamburger,
563            activation_ring,
564            child_indicator,
565            img_frame,
566            text_style,
567            checked_material,
568            on_checked_material,
569        }
570    }
571
572    /// Force the hand menu to show at a specific location. This will close the hand menu if it was already open, and
573    /// resets it to the root menu layer. Also plays an opening sound.
574    /// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuRadial/Show.html>
575    pub fn show(&mut self, at: impl Into<Vec3>, hand: Handed) {
576        if self.active_hand != Handed::Max {
577            self.close();
578        }
579        let at_pos = &at.into();
580        Sound::click().play(*at_pos, None);
581        self.dest_pose.position = *at_pos;
582        self.dest_pose.orientation = Quat::look_at(*at_pos, Input::get_head().position, None);
583        Log::diag(format!("dest_pose at show{}", self.dest_pose));
584        self.active_layer = self.root.clone();
585        self.active_hand = hand;
586
587        generate_slice_mesh(
588            360.0 / (self.root.items_count() as f32),
589            Self::MIN_DIST,
590            Self::MAX_DIST,
591            Self::SLICE_GAP,
592            &mut self.background,
593        );
594        generate_slice_mesh(
595            360.0 / (self.root.items_count() as f32),
596            Self::MAX_DIST,
597            Self::MAX_DIST + 0.005,
598            Self::SLICE_GAP,
599            &mut self.background_edge,
600        );
601    }
602
603    /// Closes the menu if it’s open! Plays a closing sound.
604    /// <https://stereokit.net/Pages/StereoKit.Framework/HandMenuRadial/Close.html>
605    pub fn close(&mut self) {
606        if self.active_hand != Handed::Max {
607            Sound::unclick().play(self.menu_pose.position, None);
608            self.menu_scale = Self::MIN_SCALE;
609            self.active_hand = Handed::Max;
610            self.angle_offset = 0.0;
611            self.nav_stack.clear();
612        }
613    }
614
615    fn step_menu_indicator(&mut self, token: &MainThreadToken, handed: Handed) {
616        let hand = Input::hand(handed);
617        if !hand.is_tracked() {
618            return;
619        };
620        let mut show_menu = false;
621        if Backend::xr_type() == BackendXRType::Simulator {
622            if Input::key(Self::SIMULATOR_KEY).is_just_active() {
623                show_menu = true
624            }
625        } else if (Input::get_controller_menu_button().is_just_active()) && handed == Handed::Left {
626            show_menu = true;
627        }
628        if show_menu {
629            self.menu_pose = hand.palm;
630            let mut at = hand.get(FingerId::Index, JointId::Tip).position;
631            if at == Vec3::ZERO {
632                self.menu_pose = Input::controller(handed).aim;
633                at = self.menu_pose.position
634            }
635            self.show(at, handed);
636            return;
637        }
638
639        let head_fwd = Input::get_head().get_forward();
640        let at = hand.palm.position;
641        if at == Vec3::ZERO {
642            //No way we get a palm pose equivalent, we do not show the hamburguer
643            return;
644        }
645        let hand_dir = (at - Input::get_head().position).get_normalized();
646
647        let in_view = Vec3::dot(head_fwd, hand_dir) > Self::OUT_OF_VIEW_ANGLE;
648
649        if !in_view {
650            return;
651        }
652
653        let palm_direction = hand.palm.get_forward();
654        self.menu_pose = hand.palm;
655
656        let direction_to_head = -hand_dir;
657        let facing = Vec3::dot(palm_direction, direction_to_head);
658
659        if facing < 0.0 {
660            return;
661        }
662
663        let color_primary = Ui::get_theme_color(UiColor::Primary, None).to_linear();
664        let color_common = Ui::get_theme_color(UiColor::Background, None).to_linear();
665        let color_text = Ui::get_theme_color(UiColor::Text, None).to_linear();
666
667        self.menu_pose.position += (1.0 - hand.grip_activation) * palm_direction * CM * 4.5;
668        self.activation_button.draw(
669            token,
670            Material::ui(),
671            self.menu_pose.to_matrix(None),
672            Some(Color128::lerp(
673                color_common,
674                color_primary,
675                ((facing - (Self::ACTIVATION_ANGLE - 0.01)) / 0.001).clamp(0.0, 1.0),
676            )),
677            None,
678        );
679        self.activation_hamburger
680            .draw(token, Material::ui(), self.menu_pose.to_matrix(None), Some(color_text), None);
681        self.menu_pose.position += (1.0 - hand.grip_activation) * palm_direction * CM * 2.0;
682        self.activation_ring
683            .draw(token, Material::ui(), self.menu_pose.to_matrix(None), Some(color_primary), None);
684
685        if facing < Self::ACTIVATION_ANGLE {
686            return;
687        }
688
689        if hand.is_just_gripped() {
690            let mut at = hand.get(FingerId::Index, JointId::Tip).position;
691            if at == Vec3::ZERO {
692                at = Input::controller(hand.handed).aim.position
693            }
694            self.show(at, handed);
695        }
696    }
697
698    fn step_menu(&mut self, token: &MainThreadToken, hand: Hand) {
699        // animate the menu a bit
700        let time = f32::min(1.0, Time::get_step_unscaledf() * 24.0);
701        self.menu_pose.position = Vec3::lerp(self.menu_pose.position, self.dest_pose.position, time);
702        self.menu_pose.orientation = Quat::slerp(self.menu_pose.orientation, self.dest_pose.orientation, time);
703        self.activation = lerp(self.activation, 1.0, time);
704        self.menu_scale = lerp(self.menu_scale, 1.0, time);
705
706        // pre-calculate some circle traversal values
707        let layer = self.active_layer.as_ref();
708        let count = layer.items_count();
709        let step = 360.0 / (count as f32);
710        let half_step = step / 2.0;
711
712        // Push the Menu's pose onto the stack, so we can draw, and work
713        // in local space.
714        Hierarchy::push(token, self.menu_pose.to_matrix(Some(self.menu_scale * Vec3::ONE)), None);
715
716        // Calculate the status of the menu!
717        let mut tip_world = hand.get(FingerId::Index, JointId::Tip).position;
718        if tip_world == Vec3::ZERO {
719            tip_world = Input::controller(hand.handed).aim.position
720        }
721        let tip_local = self.dest_pose.to_matrix(None).get_inverse().transform_point(tip_world);
722        let mag_sq = tip_local.magnitude_squared();
723        let on_menu = tip_local.z > -0.02 && tip_local.z < 0.02;
724        let focused = on_menu && mag_sq > Self::MIN_DIST.powi(2);
725        let selected = on_menu && mag_sq > Self::MID_DIST.powi(2);
726        let cancel = mag_sq > Self::MAX_DIST.powi(2);
727        //let arc_length = (Self::MIN_DIST * f32::min(90.0, step)).to_radians();
728
729        // Find where our finger is pointing to, and draw that
730        let mut finger_angle =
731            tip_local.y.atan2(tip_local.x).to_degrees() - (layer.get_start_angle() + self.angle_offset);
732        while finger_angle < 0.0 {
733            finger_angle += 360.0;
734        }
735        let angle_id = (finger_angle / step).trunc() as usize;
736        Lines::add(token, Vec3::new(0.0, 0.0, -0.008), Vec3::new(tip_local.x, tip_local.y, -0.008), WHITE, None, 0.006);
737
738        // Now draw each of the menu items !
739        let color_primary = Ui::get_theme_color(UiColor::Primary, None).to_linear();
740        let color_common = Ui::get_theme_color(UiColor::Background, None).to_linear();
741        for (i, line) in layer.items().iter().enumerate() {
742            let curr_angle = (i as f32) * step + layer.get_start_angle() + self.angle_offset;
743            let highlight = focused && angle_id == i && self.activation >= 0.99;
744            let depth = if highlight { -0.005 } else { 0.0 };
745            let mut at = Vec3::angle_xy(curr_angle + half_step, 0.0) * Self::MID_DIST;
746            at.z = depth;
747
748            let r = Matrix::t_r(Vec3::new(0.0, 0.0, depth), Quat::from_angles(0.0, 0.0, curr_angle));
749            self.background.draw(
750                token,
751                Material::ui(),
752                r,
753                Some(color_common * (if highlight { 2.0 } else { 1.0 })),
754                None,
755            );
756            self.background_edge.draw(
757                token,
758                Material::ui(),
759                r,
760                Some(color_primary * (if highlight { 2.0 } else { 1.0 })),
761                None,
762            );
763            let mut add_offset = 1.0;
764            let item_to_draw: &HandMenuItem;
765
766            match line.as_ref() {
767                HandRadial::Item(item) => {
768                    item_to_draw = item;
769                    match *item.action.borrow() {
770                        HandMenuAction::Back => self.child_indicator.draw(
771                            token,
772                            Material::ui(),
773                            Matrix::t_r(
774                                Vec3::new(0.0, 0.0, depth),
775                                Quat::from_angles(0.0, 0.0, curr_angle + half_step),
776                            ),
777                            None,
778                            None,
779                        ),
780                        HandMenuAction::Close => (),
781                        HandMenuAction::Callback => (),
782                        HandMenuAction::Checked(_group) => {
783                            let checked_material = if item_to_draw.image.is_none() {
784                                &self.checked_material
785                            } else {
786                                &self.on_checked_material
787                            };
788                            self.img_frame.draw(
789                                token,
790                                checked_material,
791                                Matrix::t_r(
792                                    Vec3::new(0.0, 0.0, depth - 0.01),
793                                    Quat::from_angles(0.0, 0.0, curr_angle + half_step),
794                                ),
795                                None,
796                                None,
797                            );
798                            add_offset = 1.2;
799                        }
800                        HandMenuAction::Unchecked(_group) => (),
801                    };
802                }
803                HandRadial::Layer(layer) => {
804                    item_to_draw = &layer.layer_item;
805                    self.child_indicator.draw(
806                        token,
807                        Material::ui(),
808                        Matrix::t_r(Vec3::new(0.0, 0.0, depth), Quat::from_angles(0.0, 0.0, curr_angle + half_step)),
809                        None,
810                        None,
811                    );
812                }
813            };
814            if let Some(image_material) = &item_to_draw.image {
815                self.img_frame.draw(
816                    token,
817                    image_material,
818                    Matrix::t_r(Vec3::new(0.0, 0.0, depth), Quat::from_angles(0.0, 0.0, curr_angle + half_step)),
819                    None,
820                    None,
821                );
822                add_offset = 1.2;
823            }
824
825            Ui::push_text_style(self.text_style);
826            item_to_draw.draw_basic(token, at * add_offset, highlight);
827            Ui::pop_text_style();
828        }
829        // Done with local work
830        Hierarchy::pop(token);
831
832        if self.activation < 0.99 {
833            return;
834        }
835        if selected {
836            if let Some(item_selected) = layer.items().get(angle_id) {
837                if Rc::ptr_eq(item_selected, &self.last_selected)
838                    && Time::get_total_unscaledf() - self.last_selected_time < 1.5
839                {
840                    return;
841                };
842                self.last_selected = item_selected.clone();
843                self.last_selected_time = Time::get_total_unscaledf();
844
845                if let Some(group_to_change) = item_selected.as_ref().is_unchecked_action() {
846                    for line in layer.items().iter() {
847                        if let Some(group) = line.as_ref().is_checked_action()
848                            && group == group_to_change
849                        {
850                            let mut to_reverse = line.as_ref();
851                            let to_to_reverse = to_reverse.borrow_mut();
852
853                            if let HandRadial::Item(menu_item) = to_to_reverse {
854                                menu_item.action.replace(HandMenuAction::Unchecked(group));
855                            }
856                        }
857                    }
858                    let mut to_reverse = item_selected.as_ref();
859                    let to_to_reverse = to_reverse.borrow_mut();
860                    if let HandRadial::Item(menu_item) = to_to_reverse {
861                        menu_item.action.replace(HandMenuAction::Checked(group_to_change));
862                    }
863                } else if let Some(group_to_change) = item_selected.as_ref().is_checked_action() {
864                    // If there is only one of this group this is a toggle button
865                    let mut cpt = 0;
866                    for line in layer.items().iter() {
867                        if let Some(group) = line.as_ref().is_checked_action() {
868                            if group_to_change == group {
869                                cpt += 1
870                            }
871                        } else if let Some(group) = line.as_ref().is_unchecked_action()
872                            && group_to_change == group
873                        {
874                            cpt += 1
875                        }
876                    }
877                    if cpt == 1 {
878                        let mut to_reverse = item_selected.as_ref();
879                        let to_to_reverse = to_reverse.borrow_mut();
880                        if let HandRadial::Item(menu_item) = to_to_reverse {
881                            menu_item.action.replace(HandMenuAction::Unchecked(group_to_change));
882                        }
883                    }
884                }
885
886                self.select_item(item_selected.clone(), tip_world, ((angle_id as f32) + 0.5) * step)
887            } else {
888                Log::err(format!("HandMenuRadial : Placement error for index {angle_id}"));
889            }
890        }
891        if cancel {
892            self.close()
893        };
894        let mut close_menu = false;
895        if Backend::xr_type() == BackendXRType::Simulator {
896            if Input::key(Self::SIMULATOR_KEY).is_just_active() {
897                close_menu = true
898            }
899        } else if Input::get_controller_menu_button().is_just_active() {
900            close_menu = true;
901        }
902        if close_menu {
903            self.close()
904        }
905    }
906
907    fn select_layer(&mut self, new_layer_rc: Rc<HandRadial>) {
908        let new_layer = match new_layer_rc.as_ref() {
909            HandRadial::Item(_) => {
910                Log::err("HandMenuRadial : Item is not a valid layer");
911                return;
912            }
913            HandRadial::Layer(layer) => layer,
914        };
915        Sound::click().play(self.menu_pose.position, None);
916        self.nav_stack.push_back(self.active_layer.clone());
917        self.active_layer = new_layer_rc.clone();
918        let divisor = new_layer.items.len() as f32;
919        generate_slice_mesh(360.0 / divisor, Self::MIN_DIST, Self::MAX_DIST, Self::SLICE_GAP, &mut self.background);
920        generate_slice_mesh(
921            360.0 / divisor,
922            Self::MAX_DIST,
923            Self::MAX_DIST + 0.005,
924            Self::SLICE_GAP,
925            &mut self.background_edge,
926        );
927        Log::diag(format!("HandRadialMenu : Layer {} opened", new_layer.layer_name));
928    }
929
930    fn back(&mut self) {
931        Sound::unclick().play(self.menu_pose.position, None);
932        if let Some(prev_layer) = self.nav_stack.pop_back() {
933            self.active_layer = prev_layer.clone();
934        } else {
935            Log::err("HandMenuRadial : No back layer !!")
936        }
937        let divisor = self.active_layer.items_count() as f32;
938        generate_slice_mesh(360.0 / divisor, Self::MIN_DIST, Self::MAX_DIST, Self::SLICE_GAP, &mut self.background);
939        generate_slice_mesh(
940            360.0 / divisor,
941            Self::MAX_DIST,
942            Self::MAX_DIST + 0.005,
943            Self::SLICE_GAP,
944            &mut self.background_edge,
945        );
946    }
947
948    fn select_item(&mut self, line: Rc<HandRadial>, at: Vec3, from_angle: f32) {
949        match line.as_ref() {
950            HandRadial::Item(item) => {
951                match *item.action.borrow() {
952                    HandMenuAction::Close => self.close(),
953                    HandMenuAction::Callback => {}
954                    HandMenuAction::Checked(_) => {}
955                    HandMenuAction::Unchecked(_) => {}
956                    HandMenuAction::Back => {
957                        self.back();
958                        self.reposition(at, from_angle)
959                    }
960                };
961                let mut callback = item.callback.borrow_mut();
962                callback()
963            }
964            HandRadial::Layer(layer) => {
965                Log::diag(format!("HandRadialMenu : open Layer {}", layer.layer_name));
966                self.select_layer(line.clone());
967                self.reposition(at, from_angle)
968            }
969        }
970    }
971
972    fn reposition(&mut self, at: Vec3, from_angle: f32) {
973        let plane = Plane::from_point(self.menu_pose.position, self.menu_pose.get_forward());
974        self.dest_pose.position = plane.closest(at);
975
976        self.activation = 0.0;
977
978        if self.active_layer.get_back_angle() != 0.0 {
979            self.angle_offset = (from_angle - self.active_layer.get_back_angle()) + 180.0;
980            while self.angle_offset < 0.0 {
981                self.angle_offset += 360.0;
982            }
983            while self.angle_offset > 360.0 {
984                self.angle_offset -= 360.0;
985            }
986        } else {
987            self.angle_offset = 0.0
988        };
989    }
990}
991
992fn generate_slice_mesh(angle: f32, min_dist: f32, max_dist: f32, gap: f32, mesh: &mut Mesh) {
993    let count = angle * 0.25;
994
995    let inner_start_angle = gap / min_dist.to_radians();
996    let inner_angle = angle - inner_start_angle * 2.0;
997    let inner_step = inner_angle / (count - 1.0);
998
999    let outer_start_angle = gap / max_dist.to_radians();
1000    let outer_angle = angle - outer_start_angle * 2.0;
1001    let outer_step = outer_angle / (count - 1.0);
1002
1003    let mut verts: Vec<Vertex> = vec![];
1004    let mut inds: Vec<Inds> = vec![];
1005
1006    let icount = count as u32;
1007    for i in 0..icount {
1008        let inner_dir = Vec3::angle_xy(inner_start_angle + (i as f32) * inner_step, 0.005);
1009        let outer_dir = Vec3::angle_xy(outer_start_angle + (i as f32) * outer_step, 0.005);
1010        verts.push(Vertex::new(inner_dir * min_dist, Vec3::FORWARD, None, None));
1011        verts.push(Vertex::new(outer_dir * max_dist, Vec3::FORWARD, None, None));
1012
1013        if i != icount - 1 {
1014            inds.push((i + 1) * 2 + 1);
1015            inds.push(i * 2 + 1);
1016            inds.push(i * 2);
1017
1018            inds.push((i + 1) * 2);
1019            inds.push((i + 1) * 2 + 1);
1020            inds.push(i * 2);
1021        }
1022    }
1023
1024    mesh.set_verts(verts.as_slice(), true);
1025    mesh.set_inds(inds.as_slice());
1026}
1027
1028fn generate_activation_button(radius: f32) -> Mesh {
1029    let spokes = 36;
1030    let mut verts: Vec<Vertex> = vec![];
1031    let mut inds: Vec<Inds> = vec![];
1032
1033    for i in 0..spokes {
1034        verts.push(Vertex::new(
1035            Vec3::angle_xy((i as f32) * (360.0 / (spokes as f32)) * radius, 0.0),
1036            Vec3::FORWARD,
1037            None,
1038            None,
1039        ))
1040    }
1041
1042    for i in 0..(spokes - 2) {
1043        let half = i / 2;
1044
1045        if i % 2 == 0 {
1046            inds.push(spokes - 1 - half);
1047            inds.push(half + 1);
1048            inds.push((spokes - half) % spokes);
1049        } else {
1050            inds.push(half + 1);
1051            inds.push(spokes - (half + 1));
1052            inds.push(half + 2);
1053        }
1054    }
1055
1056    let mut mesh = Mesh::new();
1057
1058    mesh.set_inds(inds.as_slice());
1059    mesh.set_verts(verts.as_slice(), true);
1060    mesh
1061}
1062
1063fn generate_activation_hamburger(radius: f32) -> Mesh {
1064    let mut verts: Vec<Vertex> = vec![];
1065    let mut inds: Vec<Inds> = vec![];
1066
1067    let w = radius / 3.0;
1068    let h = radius / 16.0;
1069    let z = -0.003;
1070
1071    for i in 0..3 {
1072        let y = -radius / 3.0 + (i as f32) * radius / 3.0;
1073
1074        let a = i * 4;
1075        let b = i * 4 + 1;
1076        let c = i * 4 + 2;
1077        let d = i * 4 + 3;
1078
1079        verts.push(Vertex::new(Vec3::new(-w, y - h, z), Vec3::FORWARD, None, None));
1080        verts.push(Vertex::new(Vec3::new(w, y - h, z), Vec3::FORWARD, None, None));
1081        verts.push(Vertex::new(Vec3::new(w, y + h, z), Vec3::FORWARD, None, None));
1082        verts.push(Vertex::new(Vec3::new(-w, y + h, z), Vec3::FORWARD, None, None));
1083
1084        inds.push(c);
1085        inds.push(b);
1086        inds.push(a);
1087
1088        inds.push(d);
1089        inds.push(c);
1090        inds.push(a);
1091    }
1092
1093    let mut mesh = Mesh::new();
1094    mesh.set_inds(inds.as_slice());
1095    mesh.set_verts(verts.as_slice(), true);
1096
1097    mesh
1098}
1099
1100fn generate_child_indicator(distance: f32, radius: f32) -> Mesh {
1101    let mut verts: Vec<Vertex> = vec![];
1102    let mut inds: Vec<Inds> = vec![];
1103
1104    verts.push(Vertex::new(Vec3::new(distance, radius * 2.0, 0.0), Vec3::FORWARD, None, None));
1105    verts.push(Vertex::new(Vec3::new(distance + radius, 0.0, 0.0), Vec3::FORWARD, None, None));
1106    verts.push(Vertex::new(Vec3::new(distance, -radius * 2.0, 0.0), Vec3::FORWARD, None, None));
1107
1108    inds.push(0);
1109    inds.push(1);
1110    inds.push(2);
1111
1112    let mut mesh = Mesh::new();
1113    mesh.set_inds(inds.as_slice());
1114    mesh.set_verts(verts.as_slice(), true);
1115
1116    mesh
1117}
1118
1119fn generate_img_frame(distance: f32, radius: f32) -> Mesh {
1120    let mut verts: Vec<Vertex> = vec![];
1121    let mut inds: Vec<Inds> = vec![];
1122
1123    verts.push(Vertex::new(
1124        Vec3::new(distance + radius, -radius, 0.0),
1125        Vec3::FORWARD,
1126        Some(Vec2::new(0.0, 0.0)),
1127        None,
1128    ));
1129    verts.push(Vertex::new(
1130        Vec3::new(distance + radius, radius, 0.0),
1131        Vec3::FORWARD,
1132        Some(Vec2::new(1.0, 0.0)),
1133        None,
1134    ));
1135    verts.push(Vertex::new(
1136        Vec3::new(distance - radius, -radius, 0.0),
1137        Vec3::FORWARD,
1138        Some(Vec2::new(0.0, 1.0)),
1139        None,
1140    ));
1141    verts.push(Vertex::new(
1142        Vec3::new(distance - radius, radius, 0.0),
1143        Vec3::FORWARD,
1144        Some(Vec2::new(1.0, 1.0)),
1145        None,
1146    ));
1147
1148    inds.push(0);
1149    inds.push(2);
1150    inds.push(1);
1151    inds.push(1);
1152    inds.push(2);
1153    inds.push(3);
1154
1155    let mut mesh = Mesh::new();
1156    mesh.set_inds(inds.as_slice());
1157    mesh.set_verts(verts.as_slice(), true);
1158
1159    mesh
1160}