use eframe::egui;
use egui::{Key, Modifiers, PointerButton, Pos2, Rect, TouchDeviceId, TouchId, TouchPhase};
use egui_kittest::Harness;
use elegance::{Knob, MetricSlider, RangeSlider, ResponseCommitExt, Slider, Theme};
struct Probe {
value: f32,
changed: usize,
committed: usize,
rect: Rect,
}
impl Default for Probe {
fn default() -> Self {
Self {
value: 0.0,
changed: 0,
committed: 0,
rect: Rect::ZERO,
}
}
}
impl Probe {
fn reset_counts(&mut self) {
self.changed = 0;
self.committed = 0;
}
}
const STOPS: [f32; 5] = [0.0, 8.0, 16.0, 24.0, 32.0];
fn slider_harness() -> Harness<'static, Probe> {
let mut harness = Harness::builder()
.with_size(egui::Vec2::new(600.0, 200.0))
.build_ui_state(
|ui, probe: &mut Probe| {
Theme::slate().install(ui.ctx());
let resp = ui.add(
MetricSlider::new(&mut probe.value, 0.0..=32.0)
.stops(STOPS)
.desired_width(400.0),
);
probe.rect = resp.rect;
if resp.changed() {
probe.changed += 1;
}
if resp.committed() {
probe.committed += 1;
}
},
Probe::default(),
);
harness.run();
harness.state_mut().reset_counts();
harness
}
fn track_x(rect: Rect, frac: f32) -> f32 {
rect.min.x + rect.width() * frac
}
fn release_at(harness: &Harness<'_, Probe>, pos: Pos2) {
harness.event(egui::Event::PointerButton {
pos,
button: PointerButton::Primary,
pressed: false,
modifiers: Modifiers::NONE,
});
}
#[test]
fn drag_across_stops_commits_once_on_release() {
let mut harness = slider_harness();
let rect = harness.state().rect;
let y = rect.center().y;
let start = Pos2::new(track_x(rect, 0.0), y);
let end = Pos2::new(track_x(rect, 1.0), y);
harness.hover_at(start);
harness.step();
harness.drag_at(start);
harness.step();
for i in 1..=4 {
harness.hover_at(Pos2::new(track_x(rect, i as f32 / 4.0), y));
harness.step();
}
assert!(
harness.state().changed >= 4,
"expected `changed` on each crossed stop, got {}",
harness.state().changed
);
assert_eq!(
harness.state().committed,
0,
"must not commit mid-drag — that is the whole point"
);
release_at(&harness, end);
harness.step();
assert_eq!(
harness.state().committed,
1,
"release after a drag must commit exactly once"
);
assert_eq!(
harness.state().value,
32.0,
"the committed value must be the settled one"
);
}
#[test]
fn click_to_set_commits_once_on_release_not_on_press() {
let mut harness = slider_harness();
let rect = harness.state().rect;
let y = rect.center().y;
let target = Pos2::new(track_x(rect, 0.5), y);
harness.hover_at(target);
harness.step();
harness.drag_at(target);
harness.step();
assert_eq!(harness.state().changed, 1, "the press frame sets the value");
assert_eq!(
harness.state().committed,
0,
"the press frame must not commit — the interaction has not settled"
);
release_at(&harness, target);
harness.step();
assert_eq!(
harness.state().committed,
1,
"a click must commit exactly once, on release"
);
assert_eq!(harness.state().value, 16.0);
}
#[test]
fn keyboard_nudge_commits_immediately() {
let mut harness = slider_harness();
harness.key_press(Key::Tab);
harness.step();
harness.state_mut().reset_counts();
assert_eq!(harness.state().value, 0.0);
harness.key_press(Key::ArrowRight);
harness.step();
assert_eq!(
harness.state().value,
8.0,
"ArrowRight should advance one stop"
);
assert_eq!(
harness.state().changed,
1,
"a keyboard nudge changes the value once"
);
assert_eq!(
harness.state().committed,
1,
"a keyboard nudge is already atomic, so it commits on the same frame"
);
}
#[test]
fn idle_frames_signal_nothing() {
let mut harness = slider_harness();
for _ in 0..3 {
harness.step();
}
assert_eq!(harness.state().changed, 0);
assert_eq!(harness.state().committed, 0);
}
fn knob_harness() -> Harness<'static, Probe> {
let mut harness = Harness::builder()
.with_size(egui::Vec2::new(300.0, 300.0))
.build_ui_state(
|ui, probe: &mut Probe| {
Theme::slate().install(ui.ctx());
let resp = ui.add(Knob::new(&mut probe.value, 0.0..=100.0));
probe.rect = resp.rect;
if resp.changed() {
probe.changed += 1;
}
if resp.committed() {
probe.committed += 1;
}
},
Probe::default(),
);
harness.run();
harness.state_mut().reset_counts();
harness
}
#[test]
fn knob_drag_commits_once_on_release() {
let mut harness = knob_harness();
let center = harness.state().rect.center();
harness.hover_at(center);
harness.step();
harness.drag_at(center);
harness.step();
for i in 1..=4 {
harness.hover_at(Pos2::new(center.x, center.y - (i as f32) * 12.0));
harness.step();
}
assert!(
harness.state().changed >= 2,
"expected several intermediate changes, got {}",
harness.state().changed
);
assert_eq!(harness.state().committed, 0, "must not commit mid-drag");
harness.event(egui::Event::PointerButton {
pos: Pos2::new(center.x, center.y - 48.0),
button: PointerButton::Primary,
pressed: false,
modifiers: Modifiers::NONE,
});
harness.step();
assert_eq!(harness.state().committed, 1);
assert!(
harness.state().value > 0.0,
"the drag should have raised it"
);
}
#[test]
fn knob_scroll_notch_commits_once_when_it_stops() {
let mut harness = knob_harness();
let center = harness.state().rect.center();
harness.hover_at(center);
harness.step();
harness.state_mut().reset_counts();
harness.event(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Line,
delta: egui::vec2(0.0, 1.0),
modifiers: Modifiers::NONE,
phase: egui::TouchPhase::Move,
});
for _ in 0..30 {
harness.step();
}
assert!(
harness.state().changed >= 1,
"the notch should move the value"
);
assert_eq!(
harness.state().committed,
1,
"one notch must commit exactly once, on the frame the scroll settles"
);
assert!(harness.state().value > 0.0);
}
#[test]
fn every_pointer_button_commits() {
for button in [
PointerButton::Primary,
PointerButton::Secondary,
PointerButton::Middle,
PointerButton::Extra1,
PointerButton::Extra2,
] {
let mut harness = slider_harness();
let rect = harness.state().rect;
let target = Pos2::new(track_x(rect, 0.5), rect.center().y);
harness.hover_at(target);
harness.step();
harness.event(egui::Event::PointerButton {
pos: target,
button,
pressed: true,
modifiers: Modifiers::NONE,
});
harness.step();
harness.event(egui::Event::PointerButton {
pos: target,
button,
pressed: false,
modifiers: Modifiers::NONE,
});
harness.step();
assert_eq!(
harness.state().value,
16.0,
"{button:?} moves the value (the write gate is button-agnostic)"
);
assert_eq!(
harness.state().committed,
1,
"{button:?} must commit exactly once"
);
}
}
#[test]
fn touch_long_press_commits_once_across_the_whole_gesture() {
let mut harness = slider_harness();
let rect = harness.state().rect;
let target = Pos2::new(track_x(rect, 0.5), rect.center().y);
let touch = |phase| egui::Event::Touch {
device_id: TouchDeviceId(0),
id: TouchId(0),
phase,
pos: target,
force: None,
};
harness.event(touch(TouchPhase::Start));
harness.event(egui::Event::PointerButton {
pos: target,
button: PointerButton::Primary,
pressed: true,
modifiers: Modifiers::NONE,
});
harness.step();
assert_eq!(harness.state().value, 16.0, "the press sets the value");
assert_eq!(
harness.state().committed,
0,
"the press frame is not yet a long touch"
);
for _ in 0..6 {
harness.step();
}
assert_eq!(
harness.state().committed,
1,
"holding past the click duration must commit once"
);
harness.event(touch(TouchPhase::End));
release_at(&harness, target);
harness.step();
harness.step();
assert_eq!(
harness.state().committed,
1,
"the lift must not commit a second time for the same gesture"
);
}
#[test]
fn unrelated_key_during_scroll_commits_once() {
let mut harness = knob_harness();
let center = harness.state().rect.center();
harness.hover_at(center);
harness.step();
harness.state_mut().reset_counts();
harness.event(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Line,
delta: egui::vec2(0.0, 1.0),
modifiers: Modifiers::NONE,
phase: egui::TouchPhase::Move,
});
harness.step();
harness.event(egui::Event::Key {
key: Key::A,
physical_key: None,
pressed: true,
repeat: false,
modifiers: Modifiers::NONE,
});
for _ in 0..30 {
harness.step();
}
assert_eq!(
harness.state().committed,
1,
"one notch must commit once regardless of unrelated input"
);
}
#[test]
fn slider_keyboard_commits_once() {
let mut harness = Harness::builder()
.with_size(egui::Vec2::new(600.0, 200.0))
.build_ui_state(
|ui, probe: &mut Probe| {
Theme::slate().install(ui.ctx());
let resp = ui.add(Slider::new(&mut probe.value, 0.0..=100.0));
probe.rect = resp.rect;
if resp.changed() {
probe.changed += 1;
}
if resp.committed() {
probe.committed += 1;
}
},
Probe::default(),
);
harness.run();
harness.state_mut().reset_counts();
harness.key_press(Key::Tab);
harness.step();
harness.state_mut().reset_counts();
harness.key_press(Key::ArrowRight);
harness.step();
assert!(harness.state().value > 0.0, "ArrowRight should raise it");
assert_eq!(harness.state().committed, 1);
}
#[test]
fn range_slider_keyboard_commits_once() {
struct RangeProbe {
low: f32,
high: f32,
committed: usize,
}
let mut harness = Harness::builder()
.with_size(egui::Vec2::new(600.0, 200.0))
.build_ui_state(
|ui, probe: &mut RangeProbe| {
Theme::slate().install(ui.ctx());
let resp = ui.add(RangeSlider::new(
&mut probe.low,
&mut probe.high,
0.0..=100.0,
));
if resp.committed() {
probe.committed += 1;
}
},
RangeProbe {
low: 20.0,
high: 80.0,
committed: 0,
},
);
harness.run();
harness.state_mut().committed = 0;
harness.key_press(Key::Tab);
harness.step();
harness.key_press(Key::Tab);
harness.step();
harness.state_mut().committed = 0;
harness.key_press(Key::ArrowRight);
harness.step();
assert!(
harness.state().low > 20.0,
"ArrowRight should raise the low endpoint, got {}",
harness.state().low
);
assert_eq!(
harness.state().committed,
1,
"a keyboard nudge on a thumb must commit exactly once"
);
}