raui_core/widget/component/interactive/
slider_view.rs

1use crate::{
2    PropsData, Scalar, pre_hooks, unpack_named_slots,
3    view_model::ViewModelValue,
4    widget::{
5        component::interactive::{
6            button::{ButtonNotifyMessage, ButtonNotifyProps, ButtonProps, use_button},
7            navigation::{
8                NavSignal, NavTrackingNotifyMessage, NavTrackingNotifyProps, use_nav_item,
9                use_nav_tracking_self,
10            },
11        },
12        context::WidgetContext,
13        node::WidgetNode,
14        unit::area::AreaBoxNode,
15    },
16};
17use intuicio_data::managed::ManagedLazy;
18use serde::{Deserialize, Serialize};
19use std::ops::{Deref, DerefMut};
20
21fn is_zero(value: &Scalar) -> bool {
22    value.abs() < 1.0e-6
23}
24
25pub trait SliderViewProxy: Send + Sync {
26    fn get(&self) -> Scalar;
27    fn set(&mut self, value: Scalar);
28}
29
30macro_rules! impl_proxy {
31    ($type:ty) => {
32        impl SliderViewProxy for $type {
33            fn get(&self) -> Scalar {
34                *self as _
35            }
36
37            fn set(&mut self, value: Scalar) {
38                *self = value as _;
39            }
40        }
41    };
42    (@round $type:ty) => {
43        impl SliderViewProxy for $type {
44            fn get(&self) -> Scalar {
45                *self as _
46            }
47
48            fn set(&mut self, value: Scalar) {
49                *self = value.round() as _;
50            }
51        }
52    };
53}
54
55impl_proxy!(@round u8);
56impl_proxy!(@round u16);
57impl_proxy!(@round u32);
58impl_proxy!(@round u64);
59impl_proxy!(@round u128);
60impl_proxy!(@round usize);
61impl_proxy!(@round i8);
62impl_proxy!(@round i16);
63impl_proxy!(@round i32);
64impl_proxy!(@round i64);
65impl_proxy!(@round i128);
66impl_proxy!(@round isize);
67impl_proxy!(f32);
68impl_proxy!(f64);
69
70impl<T> SliderViewProxy for ViewModelValue<T>
71where
72    T: SliderViewProxy,
73{
74    fn get(&self) -> Scalar {
75        self.deref().get()
76    }
77
78    fn set(&mut self, value: Scalar) {
79        self.deref_mut().set(value);
80    }
81}
82
83#[derive(Clone)]
84pub struct SliderInput(ManagedLazy<dyn SliderViewProxy>);
85
86impl SliderInput {
87    pub fn new(data: ManagedLazy<impl SliderViewProxy + 'static>) -> Self {
88        let (lifetime, data) = data.into_inner();
89        let data = data as *mut dyn SliderViewProxy;
90        unsafe { Self(ManagedLazy::<dyn SliderViewProxy>::new_raw(data, lifetime).unwrap()) }
91    }
92
93    pub fn into_inner(self) -> ManagedLazy<dyn SliderViewProxy> {
94        self.0
95    }
96
97    pub fn get<T: TryFrom<Scalar> + Default>(&self) -> T {
98        self.0
99            .read()
100            .map(|data| data.get())
101            .and_then(|value| T::try_from(value).ok())
102            .unwrap_or_default()
103    }
104
105    pub fn set<T: TryInto<Scalar>>(&mut self, value: T) {
106        if let Some(mut data) = self.0.write()
107            && let Ok(value) = value.try_into()
108        {
109            data.set(value);
110        }
111    }
112}
113
114impl std::fmt::Debug for SliderInput {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_tuple("SliderInput")
117            .field(&self.0.read().map(|data| data.get()).unwrap_or_default())
118            .finish()
119    }
120}
121
122impl<T: SliderViewProxy + 'static> From<ManagedLazy<T>> for SliderInput {
123    fn from(value: ManagedLazy<T>) -> Self {
124        Self::new(value)
125    }
126}
127
128#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub enum SliderViewDirection {
130    #[default]
131    LeftToRight,
132    RightToLeft,
133    TopToBottom,
134    BottomToTop,
135}
136
137#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
138#[props_data(crate::props::PropsData)]
139#[prefab(crate::Prefab)]
140pub struct SliderViewProps {
141    #[serde(default)]
142    #[serde(skip)]
143    pub input: Option<SliderInput>,
144    #[serde(default)]
145    #[serde(skip_serializing_if = "is_zero")]
146    pub from: Scalar,
147    #[serde(default)]
148    #[serde(skip_serializing_if = "is_zero")]
149    pub to: Scalar,
150    #[serde(default)]
151    pub direction: SliderViewDirection,
152}
153
154impl SliderViewProps {
155    pub fn get_value(&self) -> Scalar {
156        self.input
157            .as_ref()
158            .map(|input| input.get::<Scalar>())
159            .unwrap_or_default()
160    }
161
162    pub fn set_value(&mut self, value: Scalar) {
163        if let Some(input) = self.input.as_mut() {
164            input.set(value);
165        }
166    }
167
168    pub fn get_percentage(&self) -> Scalar {
169        (self.get_value() - self.from) / (self.to - self.from)
170    }
171
172    pub fn set_percentage(&mut self, value: Scalar) {
173        self.set_value(value * (self.to - self.from) + self.from)
174    }
175}
176
177#[pre_hooks(use_button, use_nav_tracking_self)]
178pub fn use_slider_view(context: &mut WidgetContext) {
179    context
180        .props
181        .write(ButtonNotifyProps(context.id.to_owned().into()));
182    context
183        .props
184        .write(NavTrackingNotifyProps(context.id.to_owned().into()));
185
186    context.life_cycle.unmount(|context| {
187        context.signals.write(NavSignal::Unlock);
188    });
189
190    context.life_cycle.change(|context| {
191        for msg in context.messenger.messages {
192            if let Some(msg) = msg.as_any().downcast_ref::<ButtonNotifyMessage>() {
193                if msg.trigger_start() {
194                    context.signals.write(NavSignal::Lock);
195                }
196                if msg.trigger_stop() {
197                    context.signals.write(NavSignal::Unlock);
198                }
199            } else if let Some(msg) = msg.as_any().downcast_ref::<NavTrackingNotifyMessage>() {
200                let button = context.state.read_cloned_or_default::<ButtonProps>();
201                if button.selected && button.trigger {
202                    let mut props = context.props.read_cloned_or_default::<SliderViewProps>();
203                    let value = match props.direction {
204                        SliderViewDirection::LeftToRight => msg.state.factor.x,
205                        SliderViewDirection::RightToLeft => 1.0 - msg.state.factor.x,
206                        SliderViewDirection::TopToBottom => msg.state.factor.y,
207                        SliderViewDirection::BottomToTop => 1.0 - msg.state.factor.y,
208                    }
209                    .clamp(0.0, 1.0);
210                    let value = value * (props.to - props.from) + props.from;
211                    if let Some(input) = props.input.as_mut() {
212                        input.set(value);
213                    }
214                }
215            }
216        }
217    });
218}
219
220#[pre_hooks(use_nav_item, use_slider_view)]
221pub fn slider_view(mut context: WidgetContext) -> WidgetNode {
222    let WidgetContext {
223        id,
224        props,
225        state,
226        named_slots,
227        ..
228    } = context;
229    unpack_named_slots!(named_slots => content);
230
231    if let Some(p) = content.props_mut() {
232        p.write(state.read_cloned_or_default::<ButtonProps>());
233        p.write(props.read_cloned_or_default::<SliderViewProps>());
234    }
235
236    AreaBoxNode {
237        id: id.to_owned(),
238        slot: Box::new(content),
239    }
240    .into()
241}