macroquad/ui/widgets/
drag.rs

1use crate::{
2    math::{vec2, Rect, Vec2},
3    ui::{widgets::Editbox, ElementState, Id, Layout, Ui, UiContent},
4};
5
6use std::any::Any;
7
8pub trait Num:
9    Copy + std::string::ToString + std::str::FromStr + Into<f64> + 'static + Default + std::fmt::Display
10{
11}
12
13#[derive(Clone, Copy)]
14struct DragState {
15    start_value: f64,
16    start_mouse: f32,
17}
18
19struct State {
20    string_represents: f64,
21    string: String,
22    before: String,
23    drag: Option<DragState>,
24    in_editbox: bool,
25}
26
27impl Default for State {
28    fn default() -> Self {
29        Self {
30            string_represents: 0.0,
31            string: String::new(),
32            before: String::new(),
33            drag: None,
34            in_editbox: false,
35        }
36    }
37}
38
39pub struct Drag<'a> {
40    id: Id,
41    label: &'a str,
42    range: Option<(f64, f64)>,
43    size: Option<Vec2>,
44    step: f32,
45}
46
47impl<'a> Drag<'a> {
48    pub const fn new(id: Id) -> Drag<'a> {
49        Drag {
50            id,
51            size: None,
52            range: None,
53            label: "",
54            step: 0.1,
55        }
56    }
57
58    pub const fn label<'b>(self, label: &'b str) -> Drag<'b> {
59        Drag {
60            label,
61            id: self.id,
62            range: self.range,
63            size: self.size,
64            step: self.step,
65        }
66    }
67
68    pub fn range<T: Num>(self, range: Option<(T, T)>) -> Drag<'a> {
69        Drag {
70            range: range.map(|(start, end)| (start.into(), end.into())),
71            ..self
72        }
73    }
74    // /// Ratio of pixels on the x-axis dragged to how much the value should be changed.
75    // /// Default for floating point numbers is `0.1`, for integers it's `1`.
76    // pub fn step(self, step: f32) -> Self {
77    //     Self { step, ..self }
78    // }
79
80    pub fn ui<T>(self, ui: &mut Ui, data: &mut T)
81    where
82        T: std::any::Any + Num,
83    {
84        let context = ui.get_active_window_context();
85        let state_hash = hash!(self.id, "input_float_state");
86        let mut s: State = std::mem::take(context.storage_any.get_or_default(state_hash));
87
88        let label_size = context.window.painter.content_with_margins_size(
89            &context.style.label_style,
90            &UiContent::Label(self.label.into()),
91        );
92        let size = vec2(
93            context.window.cursor.area.w - context.style.margin * 2. - context.window.cursor.ident,
94            label_size.y.max(22.),
95        );
96
97        let pos = context.window.cursor.fit(size, Layout::Vertical);
98        let editbox_area = Vec2::new(
99            if self.label.is_empty() {
100                size.x
101            } else {
102                size.x / 2.0
103            },
104            size.y,
105        );
106        let hovered = Rect::new(pos.x, pos.y, editbox_area.x, editbox_area.y)
107            .contains(context.input.mouse_position);
108
109        // state transition between editbox and dragbox
110        if s.in_editbox == false {
111            if hovered && context.input.is_mouse_down() && context.input.modifier_ctrl {
112                s.in_editbox = true;
113            }
114        } else {
115            if context.input.escape
116                || context.input.enter
117                || (hovered == false && context.input.is_mouse_down())
118            {
119                s.in_editbox = false;
120            }
121        }
122
123        if s.in_editbox == false {
124            let context = ui.get_active_window_context();
125
126            // context.window.painter.draw_rect(
127            //     Rect::new(pos.x, pos.y, editbox_area.x, editbox_area.y),
128            //     None,
129            //     context.style.drag_background(context.focused),
130            // );
131
132            let label = format!("{:.2}", (*data));
133            let value_size = context.window.painter.content_with_margins_size(
134                &context.style.label_style,
135                &UiContent::Label((&label).into()),
136            );
137
138            context.window.painter.draw_element_label(
139                &context.style.label_style,
140                pos + Vec2::new(size.x / 2. - value_size.x - 15., 0.),
141                &label,
142                ElementState {
143                    focused: context.focused,
144                    hovered: false,
145                    clicked: false,
146                    selected: false,
147                },
148            );
149
150            if let Some(drag) = s.drag {
151                if context.input.is_mouse_down == false {
152                    s.drag = None;
153                    context.input.cursor_grabbed = false;
154                    if !hovered {
155                        *context.input_focus = None;
156                    }
157                } else {
158                    let mouse_delta =
159                        (context.input.mouse_position.x - drag.start_mouse) * self.step;
160
161                    if (data as &mut dyn Any).is::<f32>() {
162                        let data = (data as &mut dyn Any).downcast_mut::<f32>().unwrap();
163                        *data = drag.start_value as f32 + mouse_delta;
164                        if let Some((start, end)) = self.range {
165                            *data = data.max(start as f32).min(end as f32);
166                        }
167                    }
168                    if (data as &mut dyn Any).is::<u32>() {
169                        let data = (data as &mut dyn Any).downcast_mut::<u32>().unwrap();
170                        *data = (drag.start_value as i32 + mouse_delta as i32).max(0) as u32;
171                        if let Some((start, end)) = self.range {
172                            *data = (*data).max(start as u32).min(end as u32);
173                        }
174                    }
175                }
176            } else {
177                if hovered && context.input.is_mouse_down() {
178                    s.drag = Some(DragState {
179                        start_mouse: context.input.mouse_position.x,
180                        start_value: (*data).into(),
181                    });
182                    *context.input_focus = Some(self.id);
183                    context.input.cursor_grabbed = true;
184                }
185            }
186        } else {
187            if s.string_represents != (*data).into() {
188                s.string = data.to_string();
189            }
190
191            Editbox::new(self.id, editbox_area)
192                .position(pos)
193                .multiline(false)
194                .ui(ui, &mut s.string);
195
196            if let Ok(n) = s.string.parse() {
197                *data = n;
198                s.string_represents = n.into();
199                s.before = s.string.clone();
200            } else if s.string.is_empty() {
201                *data = T::default();
202                s.string_represents = 0.0;
203                s.before = s.string.clone();
204            } else {
205                s.string = s.before.clone();
206            }
207        }
208
209        let context = ui.get_active_window_context();
210
211        if self.label.is_empty() == false {
212            context.window.painter.draw_element_label(
213                &context.style.label_style,
214                Vec2::new(pos.x + size.x / 2. + 5., pos.y),
215                self.label,
216                ElementState {
217                    focused: context.focused,
218                    hovered: false,
219                    clicked: false,
220                    selected: false,
221                },
222            );
223        }
224
225        *context.storage_any.get_or_default(state_hash) = s;
226    }
227}
228
229impl Num for u32 {}
230impl Num for f32 {}
231
232impl Ui {
233    pub fn drag<T: Num, T1: Into<Option<(T, T)>>>(
234        &mut self,
235        id: Id,
236        label: &str,
237        range: T1,
238        data: &mut T,
239    ) {
240        let range = range.into();
241
242        Drag::new(id).label(label).range(range).ui(self, data);
243    }
244}