1#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum EditPhase {
20 Begin,
21 Update,
22 Commit,
23 Cancel,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct Edit {
29 pub phase: EditPhase,
30 pub start: f64,
32 pub value: f64,
34}
35
36impl Edit {
37 pub fn begin(start: f64) -> Self {
38 Edit { phase: EditPhase::Begin, start, value: start }
39 }
40 pub fn update(start: f64, value: f64) -> Self {
41 Edit { phase: EditPhase::Update, start, value }
42 }
43 pub fn commit(start: f64, value: f64) -> Self {
44 Edit { phase: EditPhase::Commit, start, value }
45 }
46 pub fn cancel(start: f64) -> Self {
47 Edit { phase: EditPhase::Cancel, start, value: start }
48 }
49 pub fn changed(&self) -> bool {
51 match self.phase {
52 EditPhase::Begin => false,
53 EditPhase::Update | EditPhase::Commit => true,
54 EditPhase::Cancel => true,
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq)]
61pub(crate) struct Bounds {
62 pub min: f64,
63 pub max: f64,
64}
65
66impl Bounds {
67 pub fn new(range: &std::ops::RangeInclusive<f64>) -> Self {
68 let (a, b) = (*range.start(), *range.end());
69 let lo = if a.is_nan() { f64::MIN } else { a.max(f64::MIN) };
70 let hi = if b.is_nan() { f64::MAX } else { b.min(f64::MAX) };
71 if lo <= hi { Bounds { min: lo, max: hi } } else { Bounds { min: hi, max: lo } }
72 }
73
74 pub fn clamp(self, v: f64) -> f64 {
75 if v.is_nan() { self.min.max(0.0f64.min(self.max)) } else { v.clamp(self.min, self.max) }
76 }
77
78 pub fn fraction(self, v: f64) -> f64 {
80 let span = self.max - self.min;
81 if span.is_finite() && span > 0.0 { ((self.clamp(v) - self.min) / span).clamp(0.0, 1.0) } else { 0.0 }
82 }
83
84 pub fn lerp(self, t: f64) -> f64 {
85 let t = if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 };
86 self.clamp(self.min + (self.max - self.min) * t)
87 }
88
89 pub fn finite(self) -> bool {
90 (self.max - self.min).is_finite()
91 }
92}
93
94pub(crate) fn snap(v: f64, step: f64) -> f64 {
96 if step.is_finite() && step > 0.0 && v.is_finite() {
97 let snapped = (v / step).round() * step;
98 if snapped.is_finite() { snapped } else { v }
99 } else {
100 v
101 }
102}
103
104#[derive(Clone, Copy, Debug, Default)]
106pub(crate) struct Drag {
107 pub start: f64,
108 pub travel: f64,
110 pub target: f64,
112 pub cancelled: bool,
114}
115
116pub(crate) fn speed_multiplier(modifiers: egui::Modifiers) -> f64 {
118 if modifiers.shift {
119 10.0
120 } else if modifiers.alt || modifiers.command {
121 0.1
122 } else {
123 1.0
124 }
125}
126
127pub(crate) enum DragMode<F: FnOnce(egui::Vec2, f64) -> f64> {
129 Relative(F),
131 Absolute(Option<f64>),
133}
134
135pub(crate) fn drive_drag<F: FnOnce(egui::Vec2, f64) -> f64>(
139 ui: &egui::Ui,
140 response: &egui::Response,
141 value: &mut f64,
142 bounds: Bounds,
143 step: f64,
144 mode: DragMode<F>,
145) -> Option<Edit> {
146 let key = response.id.with("ashlar_drag");
147 let current = bounds.clamp(*value);
148 let aim = |drag: &mut Drag| {
149 let t = match mode {
150 DragMode::Absolute(Some(abs)) => bounds.clamp(snap(abs, step)),
151 DragMode::Absolute(None) => drag.target,
152 DragMode::Relative(delta) => {
153 drag.travel += delta(response.drag_delta(), speed_multiplier(ui.input(|i| i.modifiers)));
154 bounds.clamp(snap(drag.start + drag.travel, step))
155 }
156 };
157 if t.is_finite() {
158 drag.target = t;
159 }
160 };
161 if response.drag_started() {
162 let mut drag = Drag { start: current, travel: 0.0, target: current, cancelled: false };
163 aim(&mut drag);
165 ui.data_mut(|d| d.insert_temp(key, drag));
166 return Some(Edit::begin(current));
167 }
168 let mut drag: Drag = ui.data(|d| d.get_temp(key))?;
169 if response.drag_stopped() || !response.dragged() {
170 ui.data_mut(|d| d.remove::<Drag>(key));
172 if drag.cancelled {
173 return None;
174 }
175 if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
176 *value = drag.start;
177 return Some(Edit::cancel(drag.start));
178 }
179 aim(&mut drag);
180 *value = drag.target;
181 return Some(Edit::commit(drag.start, drag.target));
182 }
183 if drag.cancelled {
184 return None;
185 }
186 if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
187 drag.cancelled = true;
188 ui.data_mut(|d| d.insert_temp(key, drag));
189 *value = drag.start;
190 return Some(Edit::cancel(drag.start));
191 }
192 aim(&mut drag);
193 ui.data_mut(|d| d.insert_temp(key, drag));
194 if drag.target != current {
195 *value = drag.target;
196 return Some(Edit::update(drag.start, drag.target));
197 }
198 None
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn bounds_tolerate_hostile_ranges() {
207 let b = Bounds::new(&(10.0..=-10.0));
208 assert_eq!((b.min, b.max), (-10.0, 10.0));
209 let b = Bounds::new(&(f64::NAN..=f64::INFINITY));
210 assert_eq!((b.min, b.max), (f64::MIN, f64::MAX));
211 assert!(!b.finite());
212 assert_eq!(b.fraction(5.0), 0.0);
213 assert_eq!(Bounds::new(&(0.0..=1.0)).clamp(f64::NAN), 0.0);
214 assert_eq!(Bounds::new(&(2.0..=3.0)).clamp(f64::NAN), 2.0);
215 assert_eq!(Bounds::new(&(0.0..=10.0)).lerp(f64::NAN), 0.0);
216 assert_eq!(Bounds::new(&(0.0..=10.0)).fraction(2.5), 0.25);
217 }
218
219 #[test]
220 fn snapping() {
221 assert_eq!(snap(0.26, 0.25), 0.25);
222 assert_eq!(snap(7.0, 0.0), 7.0);
223 assert_eq!(snap(7.0, f64::NAN), 7.0);
224 assert_eq!(snap(f64::MAX, 1e-300), f64::MAX);
225 }
226}