raui_core/widget/component/interactive/
options_view.rs

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