raui_core/widget/component/interactive/
options_view.rs1use 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 && let Ok(value) = value.try_into()
97 {
98 data.set(value);
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 && msg.trigger_stop()
155 {
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 && let Ok(value) = value.parse::<usize>()
167 && let Ok(mut options) = context.props.read_cloned::<OptionsViewProps>()
168 {
169 options.set_index(value);
170 }
171 }
172 }
173 }
174 });
175}
176
177#[pre_hooks(use_options_view)]
178pub fn options_view(mut context: WidgetContext) -> WidgetNode {
179 let WidgetContext {
180 id,
181 idref,
182 key,
183 props,
184 state,
185 named_slots,
186 listed_slots,
187 ..
188 } = context;
189 unpack_named_slots!(named_slots => content);
190
191 let state = state.read_cloned_or_default::<ContextBoxProps>();
192 let active = props.read_cloned::<NavItemActive>().ok();
193 let options = props.read_cloned_or_default::<OptionsViewProps>();
194 let selected = listed_slots
195 .get(options.get_index())
196 .cloned()
197 .map(|mut node| {
198 node.remap_props(|props| props.with(OptionsViewMode::Selected));
199 node
200 })
201 .unwrap_or_default();
202 let content = if state.show {
203 let content = match content {
204 WidgetNode::Component(node) => {
205 WidgetNode::Component(node.listed_slots(listed_slots.into_iter().enumerate().map(
206 |(index, mut slot)| {
207 slot.remap_props(|props| props.with(OptionsViewMode::Option));
208 make_widget!(button)
209 .key(format!("button-item?index={index}"))
210 .merge_props(slot.props().cloned().unwrap_or_default())
211 .with_props(ButtonNotifyProps(id.to_owned().into()))
212 .named_slot("content", slot)
213 },
214 )))
215 }
216 node => node,
217 };
218 Some(
219 make_widget!(size_box)
220 .key("context")
221 .merge_props(content.props().cloned().unwrap_or_default())
222 .with_props(props.read_cloned_or_default::<SizeBoxProps>())
223 .named_slot("content", content),
224 )
225 } else {
226 None
227 };
228
229 make_widget!(portals_context_box)
230 .key(key)
231 .maybe_idref(idref.cloned())
232 .with_props(props.read_cloned_or_default::<PivotBoxProps>())
233 .with_props(state)
234 .named_slot(
235 "content",
236 make_widget!(button)
237 .key("button-selected")
238 .maybe_with_props(active)
239 .with_props(ButtonNotifyProps(id.to_owned().into()))
240 .named_slot("content", selected),
241 )
242 .maybe_named_slot("context", content)
243 .into()
244}