#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EditPhase {
Begin,
Update,
Commit,
Cancel,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Edit {
pub phase: EditPhase,
pub start: f64,
pub value: f64,
}
impl Edit {
pub fn begin(start: f64) -> Self {
Edit { phase: EditPhase::Begin, start, value: start }
}
pub fn update(start: f64, value: f64) -> Self {
Edit { phase: EditPhase::Update, start, value }
}
pub fn commit(start: f64, value: f64) -> Self {
Edit { phase: EditPhase::Commit, start, value }
}
pub fn cancel(start: f64) -> Self {
Edit { phase: EditPhase::Cancel, start, value: start }
}
pub fn changed(&self) -> bool {
match self.phase {
EditPhase::Begin => false,
EditPhase::Update | EditPhase::Commit => true,
EditPhase::Cancel => true,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Bounds {
pub min: f64,
pub max: f64,
}
impl Bounds {
pub fn new(range: &std::ops::RangeInclusive<f64>) -> Self {
let (a, b) = (*range.start(), *range.end());
let lo = if a.is_nan() { f64::MIN } else { a.max(f64::MIN) };
let hi = if b.is_nan() { f64::MAX } else { b.min(f64::MAX) };
if lo <= hi { Bounds { min: lo, max: hi } } else { Bounds { min: hi, max: lo } }
}
pub fn clamp(self, v: f64) -> f64 {
if v.is_nan() { self.min.max(0.0f64.min(self.max)) } else { v.clamp(self.min, self.max) }
}
pub fn fraction(self, v: f64) -> f64 {
let span = self.max - self.min;
if span.is_finite() && span > 0.0 { ((self.clamp(v) - self.min) / span).clamp(0.0, 1.0) } else { 0.0 }
}
pub fn lerp(self, t: f64) -> f64 {
let t = if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 };
self.clamp(self.min + (self.max - self.min) * t)
}
pub fn finite(self) -> bool {
(self.max - self.min).is_finite()
}
}
pub(crate) fn snap(v: f64, step: f64) -> f64 {
if step.is_finite() && step > 0.0 && v.is_finite() {
let snapped = (v / step).round() * step;
if snapped.is_finite() { snapped } else { v }
} else {
v
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Drag {
pub start: f64,
pub travel: f64,
pub target: f64,
pub cancelled: bool,
}
pub(crate) fn speed_multiplier(modifiers: egui::Modifiers) -> f64 {
if modifiers.shift {
10.0
} else if modifiers.alt || modifiers.command {
0.1
} else {
1.0
}
}
pub(crate) enum DragMode<F: FnOnce(egui::Vec2, f64) -> f64> {
Relative(F),
Absolute(Option<f64>),
}
pub(crate) fn drive_drag<F: FnOnce(egui::Vec2, f64) -> f64>(
ui: &egui::Ui,
response: &egui::Response,
value: &mut f64,
bounds: Bounds,
step: f64,
mode: DragMode<F>,
) -> Option<Edit> {
let key = response.id.with("ashlar_drag");
let current = bounds.clamp(*value);
let aim = |drag: &mut Drag| {
let t = match mode {
DragMode::Absolute(Some(abs)) => bounds.clamp(snap(abs, step)),
DragMode::Absolute(None) => drag.target,
DragMode::Relative(delta) => {
drag.travel += delta(response.drag_delta(), speed_multiplier(ui.input(|i| i.modifiers)));
bounds.clamp(snap(drag.start + drag.travel, step))
}
};
if t.is_finite() {
drag.target = t;
}
};
if response.drag_started() {
let mut drag = Drag { start: current, travel: 0.0, target: current, cancelled: false };
aim(&mut drag);
ui.data_mut(|d| d.insert_temp(key, drag));
return Some(Edit::begin(current));
}
let mut drag: Drag = ui.data(|d| d.get_temp(key))?;
if response.drag_stopped() || !response.dragged() {
ui.data_mut(|d| d.remove::<Drag>(key));
if drag.cancelled {
return None;
}
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
*value = drag.start;
return Some(Edit::cancel(drag.start));
}
aim(&mut drag);
*value = drag.target;
return Some(Edit::commit(drag.start, drag.target));
}
if drag.cancelled {
return None;
}
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
drag.cancelled = true;
ui.data_mut(|d| d.insert_temp(key, drag));
*value = drag.start;
return Some(Edit::cancel(drag.start));
}
aim(&mut drag);
ui.data_mut(|d| d.insert_temp(key, drag));
if drag.target != current {
*value = drag.target;
return Some(Edit::update(drag.start, drag.target));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bounds_tolerate_hostile_ranges() {
let b = Bounds::new(&(10.0..=-10.0));
assert_eq!((b.min, b.max), (-10.0, 10.0));
let b = Bounds::new(&(f64::NAN..=f64::INFINITY));
assert_eq!((b.min, b.max), (f64::MIN, f64::MAX));
assert!(!b.finite());
assert_eq!(b.fraction(5.0), 0.0);
assert_eq!(Bounds::new(&(0.0..=1.0)).clamp(f64::NAN), 0.0);
assert_eq!(Bounds::new(&(2.0..=3.0)).clamp(f64::NAN), 2.0);
assert_eq!(Bounds::new(&(0.0..=10.0)).lerp(f64::NAN), 0.0);
assert_eq!(Bounds::new(&(0.0..=10.0)).fraction(2.5), 0.25);
}
#[test]
fn snapping() {
assert_eq!(snap(0.26, 0.25), 0.25);
assert_eq!(snap(7.0, 0.0), 7.0);
assert_eq!(snap(7.0, f64::NAN), 7.0);
assert_eq!(snap(f64::MAX, 1e-300), f64::MAX);
}
}