raui_core/widget/component/interactive/
slider_view.rs

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