raui_core/widget/component/interactive/
options_view.rs

1use crate::{
2    PropsData, make_widget, pre_hooks, unpack_named_slots,
3    view_model::ViewModelValue,
4    widget::{
5        WidgetIdMetaParams,
6        component::{
7            containers::{
8                anchor_box::PivotBoxProps,
9                context_box::{ContextBoxProps, portals_context_box},
10                size_box::{SizeBoxProps, size_box},
11            },
12            interactive::{
13                button::{ButtonNotifyMessage, ButtonNotifyProps, button},
14                navigation::NavItemActive,
15            },
16        },
17        context::WidgetContext,
18        node::WidgetNode,
19    },
20};
21use intuicio_data::managed::ManagedLazy;
22use serde::{Deserialize, Serialize};
23use std::ops::{Deref, DerefMut};
24
25pub trait OptionsViewProxy: Send + Sync {
26    fn get(&self) -> usize;
27    fn set(&mut self, value: usize);
28}
29
30macro_rules! impl_proxy {
31    ($type:ty) => {
32        impl OptionsViewProxy for $type {
33            fn get(&self) -> usize {
34                *self as _
35            }
36
37            fn set(&mut self, value: usize) {
38                *self = value as _;
39            }
40        }
41    };
42}
43
44impl_proxy!(u8);
45impl_proxy!(u16);
46impl_proxy!(u32);
47impl_proxy!(u64);
48impl_proxy!(u128);
49impl_proxy!(usize);
50impl_proxy!(i8);
51impl_proxy!(i16);
52impl_proxy!(i32);
53impl_proxy!(i64);
54impl_proxy!(i128);
55impl_proxy!(isize);
56impl_proxy!(f32);
57impl_proxy!(f64);
58
59impl<T> OptionsViewProxy for ViewModelValue<T>
60where
61    T: OptionsViewProxy,
62{
63    fn get(&self) -> usize {
64        self.deref().get()
65    }
66
67    fn set(&mut self, value: usize) {
68        self.deref_mut().set(value);
69    }
70}
71
72#[derive(Clone)]
73pub struct OptionsInput(ManagedLazy<dyn OptionsViewProxy>);
74
75impl OptionsInput {
76    pub fn new(data: ManagedLazy<impl OptionsViewProxy + 'static>) -> Self {
77        let (lifetime, data) = data.into_inner();
78        let data = data as *mut dyn OptionsViewProxy;
79        unsafe { Self(ManagedLazy::<dyn OptionsViewProxy>::new_raw(data, lifetime).unwrap()) }
80    }
81
82    pub fn into_inner(self) -> ManagedLazy<dyn OptionsViewProxy> {
83        self.0
84    }
85
86    pub fn get<T: TryFrom<usize> + Default>(&self) -> T {
87        self.0
88            .read()
89            .map(|data| data.get())
90            .and_then(|value| T::try_from(value).ok())
91            .unwrap_or_default()
92    }
93
94    pub fn set<T: TryInto<usize>>(&mut self, value: T) {
95        if let Some(mut data) = self.0.write() {
96            if let Ok(value) = value.try_into() {
97                data.set(value);
98            }
99        }
100    }
101}
102
103impl std::fmt::Debug for OptionsInput {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.debug_tuple("OptionsInput")
106            .field(&self.0.read().map(|data| data.get()).unwrap_or_default())
107            .finish()
108    }
109}
110
111impl<T: OptionsViewProxy + 'static> From<ManagedLazy<T>> for OptionsInput {
112    fn from(value: ManagedLazy<T>) -> Self {
113        Self::new(value)
114    }
115}
116
117#[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[props_data(crate::props::PropsData)]
119#[prefab(crate::Prefab)]
120pub enum OptionsViewMode {
121    Selected,
122    #[default]
123    Option,
124}
125
126#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
127#[props_data(crate::props::PropsData)]
128#[prefab(crate::Prefab)]
129pub struct OptionsViewProps {
130    #[serde(default)]
131    #[serde(skip)]
132    pub input: Option<OptionsInput>,
133}
134
135impl OptionsViewProps {
136    pub fn get_index(&self) -> usize {
137        self.input
138            .as_ref()
139            .map(|input| input.get::<usize>())
140            .unwrap_or_default()
141    }
142
143    pub fn set_index(&mut self, value: usize) {
144        if let Some(input) = self.input.as_mut() {
145            input.set(value);
146        }
147    }
148}
149
150fn use_options_view(context: &mut WidgetContext) {
151    context.life_cycle.change(|context| {
152        for msg in context.messenger.messages {
153            if let Some(msg) = msg.as_any().downcast_ref::<ButtonNotifyMessage>() {
154                if msg.trigger_stop() {
155                    if msg.sender.key() == "button-selected" {
156                        let mut state = context.state.read_cloned_or_default::<ContextBoxProps>();
157                        state.show = !state.show;
158                        let _ = context.state.write_with(state);
159                    } else if msg.sender.key() == "button-item" {
160                        let mut state = context.state.read_cloned_or_default::<ContextBoxProps>();
161                        state.show = !state.show;
162                        let _ = context.state.write_with(state);
163                        let params = WidgetIdMetaParams::new(msg.sender.meta());
164                        if let Some(value) = params.find_value("index") {
165                            if let Ok(value) = value.parse::<usize>() {
166                                if let Ok(mut options) =
167                                    context.props.read_cloned::<OptionsViewProps>()
168                                {
169                                    options.set_index(value);
170                                }
171                            }
172                        }
173                    }
174                }
175            }
176        }
177    });
178}
179
180#[pre_hooks(use_options_view)]
181pub fn options_view(mut context: WidgetContext) -> WidgetNode {
182    let WidgetContext {
183        id,
184        idref,
185        key,
186        props,
187        state,
188        named_slots,
189        listed_slots,
190        ..
191    } = context;
192    unpack_named_slots!(named_slots => content);
193
194    let state = state.read_cloned_or_default::<ContextBoxProps>();
195    let active = props.read_cloned::<NavItemActive>().ok();
196    let options = props.read_cloned_or_default::<OptionsViewProps>();
197    let selected = listed_slots
198        .get(options.get_index())
199        .cloned()
200        .map(|mut node| {
201            node.remap_props(|props| props.with(OptionsViewMode::Selected));
202            node
203        })
204        .unwrap_or_default();
205    let content = if state.show {
206        let content = match content {
207            WidgetNode::Component(node) => {
208                WidgetNode::Component(node.listed_slots(listed_slots.into_iter().enumerate().map(
209                    |(index, mut slot)| {
210                        slot.remap_props(|props| props.with(OptionsViewMode::Option));
211                        make_widget!(button)
212                            .key(format!("button-item?index={index}"))
213                            .merge_props(slot.props().cloned().unwrap_or_default())
214                            .with_props(ButtonNotifyProps(id.to_owned().into()))
215                            .named_slot("content", slot)
216                    },
217                )))
218            }
219            node => node,
220        };
221        Some(
222            make_widget!(size_box)
223                .key("context")
224                .merge_props(content.props().cloned().unwrap_or_default())
225                .with_props(props.read_cloned_or_default::<SizeBoxProps>())
226                .named_slot("content", content),
227        )
228    } else {
229        None
230    };
231
232    make_widget!(portals_context_box)
233        .key(key)
234        .maybe_idref(idref.cloned())
235        .with_props(props.read_cloned_or_default::<PivotBoxProps>())
236        .with_props(state)
237        .named_slot(
238            "content",
239            make_widget!(button)
240                .key("button-selected")
241                .maybe_with_props(active)
242                .with_props(ButtonNotifyProps(id.to_owned().into()))
243                .named_slot("content", selected),
244        )
245        .maybe_named_slot("context", content)
246        .into()
247}