UI: support continuous numeric scrubbing across monitor edges
This commit is contained in:
parent
813c7c4650
commit
adbe4dff4e
|
|
@ -23,6 +23,7 @@ pub mod menu_keys;
|
|||
pub mod menus;
|
||||
pub mod native_menu;
|
||||
pub mod panels;
|
||||
mod pointer_wrap;
|
||||
pub mod prefs_live;
|
||||
pub mod state;
|
||||
pub mod theme;
|
||||
|
|
@ -1559,7 +1560,7 @@ impl eframe::App for EffectcraftApp {
|
|||
self.session.end_recovery();
|
||||
}
|
||||
|
||||
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw_input: &mut egui::RawInput) {
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
|
||||
if !self.synthetic.is_empty() {
|
||||
// Pointer events go one per frame so egui sees press → moves → release as a real drag.
|
||||
let pointer = |e: &egui::Event| matches!(e, egui::Event::PointerMoved(_) | egui::Event::PointerButton { .. } | egui::Event::MouseWheel { .. });
|
||||
|
|
@ -1573,6 +1574,9 @@ impl eframe::App for EffectcraftApp {
|
|||
};
|
||||
raw_input.events.extend(self.synthetic.drain(..n));
|
||||
}
|
||||
if pointer_wrap::prepare_input(ctx, raw_input) {
|
||||
self.session.history.merge_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
|
|
@ -1618,6 +1622,8 @@ impl eframe::App for EffectcraftApp {
|
|||
}
|
||||
self.frame(ui);
|
||||
let ctx = ui.ctx().clone();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pointer_wrap::finish_native(&ctx, frame);
|
||||
// wasm32: no frame threads; render queued frames now, between UI frames.
|
||||
if cfg!(target_arch = "wasm32") && self.frames.pump(std::time::Duration::from_millis(if self.playback.playing { 24 } else { 40 })) {
|
||||
ctx.request_repaint();
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
|
|||
ui.label(tr("Width:"));
|
||||
ui.horizontal(|ui| {
|
||||
let (ow, oh) = (d.width, d.height);
|
||||
let r = ui.add(egui::DragValue::new(&mut d.width).range(4..=30000).suffix(" px"));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.width).range(4..=30000).suffix(" px")));
|
||||
auto.add("dialog.comp.width", r.rect, "Width");
|
||||
let lbl = format!("Lock Aspect Ratio to {}", aspect_label(ow as f64, oh as f64));
|
||||
let r = ui.checkbox(&mut d.lock_aspect, lbl);
|
||||
|
|
@ -224,7 +224,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
|
|||
ui.end_row();
|
||||
ui.label(tr("Height:"));
|
||||
let (ow, oh) = (d.width, d.height);
|
||||
let r = ui.add(egui::DragValue::new(&mut d.height).range(4..=30000).suffix(" px"));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.height).range(4..=30000).suffix(" px")));
|
||||
auto.add("dialog.comp.height", r.rect, "Height");
|
||||
if d.lock_aspect && oh != d.height && oh > 0 {
|
||||
d.width = ((d.height as f64) * ow as f64 / oh as f64).round().max(4.0) as u32;
|
||||
|
|
@ -254,7 +254,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
|
|||
ui.label(tr("Frame Rate:"));
|
||||
ui.horizontal(|ui| {
|
||||
// Any rate can be typed; the list holds the common ones.
|
||||
let r = ui.add(egui::DragValue::new(&mut d.fps).range(1.0..=999.0).speed(0.01).max_decimals(3));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.fps).range(1.0..=999.0).speed(0.01).max_decimals(3)));
|
||||
auto.add("dialog.comp.frameRate", r.rect, "Frame Rate");
|
||||
let r = egui::ComboBox::from_id_salt("comp-fps").width(18.0).selected_text("").show_ui(ui, |ui| {
|
||||
for f in FRAME_RATES {
|
||||
|
|
@ -320,19 +320,19 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
|
|||
ui.label(egui::RichText::new(tr("Motion Blur")).strong());
|
||||
ui.end_row();
|
||||
ui.label(tr("Shutter Angle:"));
|
||||
let r = ui.add(egui::DragValue::new(&mut d.shutter_angle).range(0.0..=720.0).suffix("°"));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.shutter_angle).range(0.0..=720.0).suffix("°")));
|
||||
auto.add("dialog.comp.shutterAngle", r.rect, "Shutter Angle");
|
||||
ui.end_row();
|
||||
ui.label(tr("Shutter Phase:"));
|
||||
let r = ui.add(egui::DragValue::new(&mut d.shutter_phase).range(-360.0..=360.0).suffix("°"));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.shutter_phase).range(-360.0..=360.0).suffix("°")));
|
||||
auto.add("dialog.comp.shutterPhase", r.rect, "Shutter Phase");
|
||||
ui.end_row();
|
||||
ui.label(tr("Samples Per Frame:"));
|
||||
let r = ui.add(egui::DragValue::new(&mut d.samples).range(2..=64));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.samples).range(2..=64)));
|
||||
auto.add("dialog.comp.samples", r.rect, "Samples Per Frame");
|
||||
ui.end_row();
|
||||
ui.label(tr("Adaptive Sample Limit:"));
|
||||
let r = ui.add(egui::DragValue::new(&mut d.adaptive_limit).range(16..=256));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.adaptive_limit).range(16..=256)));
|
||||
auto.add("dialog.comp.adaptiveLimit", r.rect, "Adaptive Sample Limit");
|
||||
ui.end_row();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -360,14 +360,14 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
|||
let (ow, oh) = (size[0], size[1]);
|
||||
ui.label(tr("Width:"));
|
||||
ui.horizontal(|ui| {
|
||||
let r = ui.add(egui::DragValue::new(&mut size[0]).range(1..=30000).suffix(" px"));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut size[0]).range(1..=30000).suffix(" px")));
|
||||
auto.add("dialog.solid.width", r.rect, "Width");
|
||||
let r = ui.checkbox(&mut lock, tr_args("Lock Aspect Ratio to {}", &[&(super::comp_settings::aspect_label(ow as f64, oh as f64))]));
|
||||
auto.add("dialog.solid.lockAspect", r.rect, "Lock Aspect Ratio");
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tr("Height:"));
|
||||
let r = ui.add(egui::DragValue::new(&mut size[1]).range(1..=30000).suffix(" px"));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut size[1]).range(1..=30000).suffix(" px")));
|
||||
auto.add("dialog.solid.height", r.rect, "Height");
|
||||
ui.end_row();
|
||||
if lock && ow > 0 && oh > 0 {
|
||||
|
|
|
|||
|
|
@ -188,13 +188,13 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
|||
ui.end_row();
|
||||
ui.label(tr("Zoom"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut d.zoom).range(1.0..=100000.0).speed(1.0).suffix(" px"));
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.zoom).range(1.0..=100000.0).speed(1.0).suffix(" px")));
|
||||
let mut aov = angle_of_view(w, d.zoom);
|
||||
if ui.add(egui::DragValue::new(&mut aov).range(0.1..=179.0).speed(0.1).suffix("°")).changed() {
|
||||
if ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut aov).range(0.1..=179.0).speed(0.1).suffix("°"))).changed() {
|
||||
d.zoom = w * 0.5 / (aov.to_radians() * 0.5).tan();
|
||||
}
|
||||
let mut fl = focal_for_zoom(w, d.zoom);
|
||||
if ui.add(egui::DragValue::new(&mut fl).range(0.1..=10000.0).speed(0.1).suffix(" mm")).changed() {
|
||||
if ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut fl).range(0.1..=10000.0).speed(0.1).suffix(" mm"))).changed() {
|
||||
d.zoom = zoom_for_focal(w, fl);
|
||||
}
|
||||
});
|
||||
|
|
@ -204,15 +204,15 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
|||
ui.end_row();
|
||||
ui.label(tr("Focus Distance"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled(!d.lock_to_zoom, egui::DragValue::new(&mut d.focus).range(0.0..=100000.0).suffix(" px"));
|
||||
ui.add_enabled(!d.lock_to_zoom, crate::widgets::drag_value(egui::DragValue::new(&mut d.focus).range(0.0..=100000.0).suffix(" px")));
|
||||
ui.checkbox(&mut d.lock_to_zoom, tr("Lock to Zoom"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tr("Aperture"));
|
||||
ui.add(egui::DragValue::new(&mut d.aperture).range(0.0..=10000.0).speed(0.1).suffix(" px"));
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.aperture).range(0.0..=10000.0).speed(0.1).suffix(" px")));
|
||||
ui.end_row();
|
||||
ui.label(tr("Blur Level"));
|
||||
ui.add(egui::DragValue::new(&mut d.blur).range(0.0..=10000.0).suffix(" %"));
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.blur).range(0.0..=10000.0).suffix(" %")));
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
|
|
@ -271,15 +271,15 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
|||
ui.color_edit_button_rgb(&mut d.color);
|
||||
ui.end_row();
|
||||
ui.label(tr("Intensity"));
|
||||
ui.add(egui::DragValue::new(&mut d.intensity).range(-10000.0..=10000.0).suffix(" %"));
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.intensity).range(-10000.0..=10000.0).suffix(" %")));
|
||||
ui.end_row();
|
||||
let kind = KINDS[d.kind.min(3)];
|
||||
if kind == LightKind::Spot {
|
||||
ui.label(tr("Cone Angle"));
|
||||
ui.add(egui::DragValue::new(&mut d.cone_angle).range(0.0..=180.0).suffix("°"));
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.cone_angle).range(0.0..=180.0).suffix("°")));
|
||||
ui.end_row();
|
||||
ui.label(tr("Cone Feather"));
|
||||
ui.add(egui::DragValue::new(&mut d.cone_feather).range(0.0..=100.0).suffix(" %"));
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.cone_feather).range(0.0..=100.0).suffix(" %")));
|
||||
ui.end_row();
|
||||
}
|
||||
if kind != LightKind::Ambient {
|
||||
|
|
@ -292,20 +292,20 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
|||
ui.end_row();
|
||||
if d.falloff > 0 {
|
||||
ui.label(tr("Radius"));
|
||||
ui.add(egui::DragValue::new(&mut d.radius).range(0.0..=100000.0).suffix(" px"));
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.radius).range(0.0..=100000.0).suffix(" px")));
|
||||
ui.end_row();
|
||||
ui.label(tr("Falloff Distance"));
|
||||
ui.add(egui::DragValue::new(&mut d.falloff_distance).range(0.0..=100000.0).suffix(" px"));
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.falloff_distance).range(0.0..=100000.0).suffix(" px")));
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label(tr("Shadows"));
|
||||
ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
|
||||
ui.end_row();
|
||||
ui.label(tr("Shadow Darkness"));
|
||||
ui.add_enabled(d.casts_shadows, egui::DragValue::new(&mut d.shadow_darkness).range(0.0..=100.0).suffix(" %"));
|
||||
ui.add_enabled(d.casts_shadows, crate::widgets::drag_value(egui::DragValue::new(&mut d.shadow_darkness).range(0.0..=100.0).suffix(" %")));
|
||||
ui.end_row();
|
||||
ui.label(tr("Shadow Diffusion"));
|
||||
ui.add_enabled(d.casts_shadows, egui::DragValue::new(&mut d.shadow_diffusion).range(0.0..=1000.0).suffix(" px"));
|
||||
ui.add_enabled(d.casts_shadows, crate::widgets::drag_value(egui::DragValue::new(&mut d.shadow_diffusion).range(0.0..=1000.0).suffix(" px")));
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -377,8 +377,8 @@ fn value_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, auto: &str, ty: Opt
|
|||
}
|
||||
ControlType::Point => {
|
||||
let mut a = v.as_vec2();
|
||||
let r1 = ui.add(egui::DragValue::new(&mut a[0]).speed(1.0).max_decimals(1));
|
||||
let r2 = ui.add(egui::DragValue::new(&mut a[1]).speed(1.0).max_decimals(1));
|
||||
let r1 = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut a[0]).speed(1.0).max_decimals(1)));
|
||||
let r2 = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut a[1]).speed(1.0).max_decimals(1)));
|
||||
if r1.changed() || r2.changed() {
|
||||
out = Some(json!(a));
|
||||
}
|
||||
|
|
@ -386,7 +386,7 @@ fn value_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, auto: &str, ty: Opt
|
|||
}
|
||||
ControlType::Angle => {
|
||||
let mut a = v.as_f64();
|
||||
let r = ui.add(egui::DragValue::new(&mut a).speed(1.0).suffix("°").max_decimals(1));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut a).speed(1.0).suffix("°").max_decimals(1)));
|
||||
if r.changed() {
|
||||
out = Some(json!(a));
|
||||
}
|
||||
|
|
@ -415,7 +415,7 @@ fn value_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, auto: &str, ty: Opt
|
|||
}
|
||||
}
|
||||
});
|
||||
let r3 = ui.add(egui::DragValue::new(&mut d.size).speed(0.5).range(1.0..=2000.0).suffix(" px").max_decimals(1));
|
||||
let r3 = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.size).speed(0.5).range(1.0..=2000.0).suffix(" px").max_decimals(1)));
|
||||
app.auto.add(&format!("{auto}.font"), r1.response.rect, "Font");
|
||||
app.auto.add(&format!("{auto}.style"), r2.response.rect, "Style");
|
||||
app.auto.add(&format!("{auto}.size"), r3.rect, "Size");
|
||||
|
|
@ -428,7 +428,7 @@ fn value_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, auto: &str, ty: Opt
|
|||
// One percentage for every axis.
|
||||
let comps = v.components();
|
||||
let mut x = comps.first().copied().unwrap_or(100.0);
|
||||
let r = ui.add(egui::DragValue::new(&mut x).speed(0.5).suffix("%").max_decimals(1));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut x).speed(0.5).suffix("%").max_decimals(1)));
|
||||
if r.changed() {
|
||||
out = Some(json!(vec![x; comps.len().max(1)]));
|
||||
}
|
||||
|
|
@ -438,7 +438,7 @@ fn value_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, auto: &str, ty: Opt
|
|||
let mut x = v.as_f64();
|
||||
let r = match p.ui {
|
||||
ParamUi::Slider { slider_min, slider_max, .. } => ui.add(egui::Slider::new(&mut x, slider_min..=slider_max).max_decimals(2)),
|
||||
_ => ui.add(egui::DragValue::new(&mut x).speed(0.5).max_decimals(2)),
|
||||
_ => ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut x).speed(0.5).max_decimals(2))),
|
||||
};
|
||||
if r.changed() {
|
||||
out = Some(json!(x));
|
||||
|
|
|
|||
|
|
@ -769,7 +769,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
|||
let r = match &mut fl.kind {
|
||||
FieldKind::Number { value, speed } => {
|
||||
let dec = if *speed < 0.01 { 5 } else { 3 };
|
||||
ui.add(egui::DragValue::new(value).speed(*speed).max_decimals(dec)).rect
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(value).speed(*speed).max_decimals(dec))).rect
|
||||
}
|
||||
FieldKind::Text(s) => ui.add(egui::TextEdit::singleline(s).desired_width(220.0)).rect,
|
||||
FieldKind::Path { value, .. } => {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ fn buttons(ui: &mut egui::Ui, app: &mut EffectcraftApp, t: &Tokens, prefix: &str
|
|||
}
|
||||
|
||||
fn drag(ui: &mut egui::Ui, app: &mut EffectcraftApp, id: &str, v: &mut f64, speed: f64, range: std::ops::RangeInclusive<f64>, suffix: &str, enabled: bool) {
|
||||
let r = ui.add_enabled(enabled, egui::DragValue::new(v).speed(speed).range(range).max_decimals(2).suffix(suffix));
|
||||
let r = ui.add_enabled(enabled, crate::widgets::drag_value(egui::DragValue::new(v).speed(speed).range(range).max_decimals(2).suffix(suffix)));
|
||||
app.auto.add(id, r.rect, id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,10 +103,10 @@ fn control(ui: &mut egui::Ui, p: &Property, v: &KV) -> Option<Value> {
|
|||
ParamUi::Slider { slider_min, slider_max, decimals, .. } => {
|
||||
ui.add(egui::Slider::new(&mut y, *slider_min..=*slider_max).clamping(egui::SliderClamping::Never).max_decimals(*decimals as usize))
|
||||
}
|
||||
ParamUi::Percent => ui.add(egui::DragValue::new(&mut y).speed(0.5).suffix("%")),
|
||||
ParamUi::Angle => ui.add(egui::DragValue::new(&mut y).speed(0.5).suffix("°")),
|
||||
ParamUi::Pixels => ui.add(egui::DragValue::new(&mut y).speed(0.25).suffix(" px")),
|
||||
_ => ui.add(egui::DragValue::new(&mut y).speed(0.1)),
|
||||
ParamUi::Percent => ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut y).speed(0.5).suffix("%"))),
|
||||
ParamUi::Angle => ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut y).speed(0.5).suffix("°"))),
|
||||
ParamUi::Pixels => ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut y).speed(0.25).suffix(" px"))),
|
||||
_ => ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut y).speed(0.1))),
|
||||
};
|
||||
(r.changed() && y != *x).then(|| json!(y))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,11 +128,11 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
|||
super::dialogs::modal(ctx, &title, vec2(520.0, 560.0), t, |ui| {
|
||||
egui::Grid::new("marker-grid").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label(tr("Time:"));
|
||||
let r = ui.add(egui::DragValue::new(&mut d.time).speed(0.01).range(0.0..=1e6).suffix(" s").max_decimals(3));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.time).speed(0.01).range(0.0..=1e6).suffix(" s").max_decimals(3)));
|
||||
app.auto.add("dialog.marker.time", r.rect, "Time");
|
||||
ui.end_row();
|
||||
ui.label(tr("Duration:"));
|
||||
let r = ui.add(egui::DragValue::new(&mut d.duration).speed(0.01).range(0.0..=1e6).suffix(" s").max_decimals(3));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.duration).speed(0.01).range(0.0..=1e6).suffix(" s").max_decimals(3)));
|
||||
app.auto.add("dialog.marker.duration", r.rect, "Duration");
|
||||
ui.end_row();
|
||||
ui.label(tr("Comment:"));
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
|
|||
let r = match kind {
|
||||
Kind::Int(lo, hi, unit) => {
|
||||
let mut x = v.as_i64().unwrap_or(0);
|
||||
let r = ui.add(egui::DragValue::new(&mut x).range(lo..=hi).speed(0.25));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut x).range(lo..=hi).speed(0.25)));
|
||||
if !unit.is_empty() {
|
||||
ui.label(RichText::new(unit).color(t.text_dim));
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
|
|||
}
|
||||
Kind::Float(lo, hi, unit) => {
|
||||
let mut x = v.as_f64().unwrap_or(0.0);
|
||||
let r = ui.add(egui::DragValue::new(&mut x).range(lo..=hi).speed(0.1).max_decimals(3));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut x).range(lo..=hi).speed(0.1).max_decimals(3)));
|
||||
if !unit.is_empty() {
|
||||
ui.label(RichText::new(unit).color(t.text_dim));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -508,7 +508,8 @@ pub fn options_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens)
|
|||
ui.horizontal(|ui| {
|
||||
let r = ui.checkbox(&mut d.blur_on, crate::i18n::tr("Blur"));
|
||||
app.auto.add("dialog.trackOptions.blurOn", r.rect, "Blur");
|
||||
let r = ui.add_enabled(d.blur_on, egui::DragValue::new(&mut d.opts.blur).speed(0.1).range(0.5..=50.0).suffix(" pixels"));
|
||||
let r =
|
||||
ui.add_enabled(d.blur_on, crate::widgets::drag_value(egui::DragValue::new(&mut d.opts.blur).speed(0.1).range(0.5..=50.0).suffix(" pixels")));
|
||||
app.auto.add("dialog.trackOptions.blur", r.rect, "Blur pixels");
|
||||
let r = ui.checkbox(&mut d.opts.enhance, crate::i18n::tr("Enhance"));
|
||||
app.auto.add("dialog.trackOptions.enhance", r.rect, "Enhance");
|
||||
|
|
@ -539,7 +540,7 @@ pub fn options_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens)
|
|||
.response;
|
||||
app.auto.add("dialog.trackOptions.action", r.rect, "If Confidence is Below action");
|
||||
ui.label(crate::i18n::tr("If Confidence is Below"));
|
||||
let r = ui.add(egui::DragValue::new(&mut d.opts.threshold).speed(0.5).range(0.0..=100.0).suffix(" %"));
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.opts.threshold).speed(0.5).range(0.0..=100.0).suffix(" %")));
|
||||
app.auto.add("dialog.trackOptions.threshold", r.rect, "Confidence threshold");
|
||||
});
|
||||
(ok, cancel) = ok_cancel(ui, app, t, "dialog.trackOptions");
|
||||
|
|
|
|||
|
|
@ -675,11 +675,13 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect,
|
|||
let mut ev = vo.exposure as f64;
|
||||
let resp = ui.put(
|
||||
er,
|
||||
egui::DragValue::new(&mut ev)
|
||||
.speed(0.05)
|
||||
.range(-40.0..=40.0)
|
||||
.fixed_decimals(1)
|
||||
.custom_formatter(|v, _| if v == 0.0 { "0.0".into() } else { format!("{v:+.1}") }),
|
||||
crate::widgets::drag_value(
|
||||
egui::DragValue::new(&mut ev)
|
||||
.speed(0.05)
|
||||
.range(-40.0..=40.0)
|
||||
.fixed_decimals(1)
|
||||
.custom_formatter(|v, _| if v == 0.0 { "0.0".into() } else { format!("{v:+.1}") }),
|
||||
),
|
||||
);
|
||||
let resp = resp.on_hover_text(crate::i18n::tr("Adjust Exposure (stops)"));
|
||||
app.auto.add("viewer.exposure", er, "Adjust Exposure");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,462 @@
|
|||
//! Continuous numeric scrubbing across native monitor edges.
|
||||
|
||||
use egui::{Context, Event, Id, PointerButton, Pos2, RawInput, Response, Vec2};
|
||||
#[cfg(any(not(target_arch = "wasm32"), test))]
|
||||
use egui::{Rect, pos2, vec2};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct State {
|
||||
numeric: Option<(Id, u64)>,
|
||||
offset: Vec2,
|
||||
pending: Option<(Pos2, Pos2)>,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
fn state_id() -> Id {
|
||||
Id::new("numeric-pointer-wrap")
|
||||
}
|
||||
|
||||
/// Opt in only number scrubs, never object, keyframe, panel or scrollbar drags.
|
||||
pub(crate) fn track(response: &Response) {
|
||||
if response.dragged_by(PointerButton::Primary) {
|
||||
let pass = response.ctx.cumulative_pass_nr();
|
||||
response.ctx.data_mut(|d| {
|
||||
let state = d.get_temp_mut_or_default::<State>(state_id());
|
||||
state.numeric = Some((response.id, pass));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep egui's drag coordinates continuous while the visible native pointer wraps.
|
||||
/// Rebase only when the OS acknowledges a successful warp: queued events from before
|
||||
/// it must not be interpreted as another monitor-width movement. A coalesced event
|
||||
/// may include a little real movement beyond the requested destination.
|
||||
/// Returns whether a numeric gesture ended, so the app can close its Undo group.
|
||||
pub(crate) fn prepare_input(ctx: &Context, raw: &mut RawInput) -> bool {
|
||||
let lost_focus = !raw.focused || raw.events.iter().any(|e| matches!(e, Event::WindowFocused(false)));
|
||||
let (down, last_pos, modifiers) = ctx.input(|i| (i.pointer.primary_down(), i.pointer.latest_pos(), i.modifiers));
|
||||
ctx.data_mut(|d| {
|
||||
let state = d.get_temp_mut_or_default::<State>(state_id());
|
||||
if lost_focus
|
||||
&& down
|
||||
&& state.numeric.is_some()
|
||||
&& let Some(pos) = last_pos
|
||||
{
|
||||
// The OS may deliver the eventual button-up to another app. Explicitly
|
||||
// finish this scrub so focus returning cannot restart it or leave Undo merged.
|
||||
raw.events.push(Event::PointerButton { pos: pos - state.offset, button: PointerButton::Primary, pressed: false, modifiers });
|
||||
}
|
||||
let touch = raw.events.iter().any(|e| matches!(e, Event::Touch { .. }));
|
||||
if lost_focus
|
||||
|| touch
|
||||
|| raw.events.iter().any(|e| matches!(e, Event::WindowFocused(false) | Event::PointerButton { button: PointerButton::Primary, .. }))
|
||||
{
|
||||
let ended = state.numeric.is_some();
|
||||
*state = State { disabled: touch, ..Default::default() };
|
||||
return ended;
|
||||
}
|
||||
if state.numeric.is_none() {
|
||||
return false;
|
||||
}
|
||||
// Mouse capture can report leaving the client area mid-scrub. Keep the
|
||||
// drag's previous coordinate until its next captured movement arrives.
|
||||
raw.events.retain(|e| !matches!(e, Event::PointerGone));
|
||||
for event in &mut raw.events {
|
||||
let p = match event {
|
||||
Event::PointerMoved(p) | Event::PointerButton { pos: p, .. } => p,
|
||||
_ => continue,
|
||||
};
|
||||
if let Some((from, to)) = state.pending
|
||||
&& p.distance_sq(to) < p.distance_sq(from)
|
||||
{
|
||||
state.offset += from - to;
|
||||
state.pending = None;
|
||||
}
|
||||
*p += state.offset;
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(not(target_arch = "wasm32"), test))]
|
||||
fn wrap_target(p: Pos2, bounds: Rect) -> Option<Pos2> {
|
||||
if !p.is_finite() || !bounds.is_finite() || bounds.width() < 32.0 || bounds.height() < 32.0 {
|
||||
return None;
|
||||
}
|
||||
let axis = |v: f32, lo: f32, hi: f32| {
|
||||
if v <= lo + 2.0 {
|
||||
hi - 8.0
|
||||
} else if v >= hi - 2.0 {
|
||||
lo + 8.0
|
||||
} else {
|
||||
v
|
||||
}
|
||||
};
|
||||
let to = pos2(axis(p.x, bounds.min.x, bounds.max.x), axis(p.y, bounds.min.y, bounds.max.y));
|
||||
(to != p).then_some(to)
|
||||
}
|
||||
|
||||
#[cfg(any(not(target_arch = "wasm32"), test))]
|
||||
fn finish_frame(ctx: &Context, bounds: Rect, warp: impl FnOnce(Pos2) -> Option<Pos2>) {
|
||||
let state = ctx.data(|d| d.get_temp::<State>(state_id())).unwrap_or_default();
|
||||
let active = state.numeric.is_some_and(|(id, pass)| pass == ctx.cumulative_pass_nr() && ctx.is_being_dragged(id));
|
||||
let (down, focused, virtual_pos) = ctx.input(|i| (i.pointer.primary_down(), i.focused, i.pointer.latest_pos()));
|
||||
if !active || !down || !focused {
|
||||
ctx.data_mut(|d| d.remove::<State>(state_id()));
|
||||
return;
|
||||
}
|
||||
if state.disabled || state.pending.is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(from) = virtual_pos.map(|p| p - state.offset) else { return };
|
||||
let Some(to) = wrap_target(from, bounds) else { return };
|
||||
let destination = warp(to);
|
||||
ctx.data_mut(|d| {
|
||||
let state = d.get_temp_mut_or_default::<State>(state_id());
|
||||
if let Some(to) = destination {
|
||||
state.pending = Some((from, to));
|
||||
} else {
|
||||
// Unsupported backends keep ordinary dragging; do not retry/log every frame.
|
||||
state.disabled = true;
|
||||
}
|
||||
});
|
||||
if destination.is_some() {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Native monitor coordinates are physical pixels; egui input uses UI points.
|
||||
/// Use the monitor containing the original press, including negative desktop origins.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn finish_native(ctx: &Context, frame: &eframe::Frame) {
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
if !ctx.data(|d| d.get_temp::<State>(state_id()).is_some_and(|s| s.numeric.is_some_and(|(_, p)| p == pass))) {
|
||||
ctx.data_mut(|d| d.remove::<State>(state_id()));
|
||||
return;
|
||||
}
|
||||
let Some(window) = frame.winit_window() else { return };
|
||||
let Ok(origin) = window.inner_position() else { return };
|
||||
let ppp = ctx.pixels_per_point();
|
||||
if !ppp.is_finite() || ppp <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let Some(press) = ctx.input(|i| i.pointer.press_origin()) else { return };
|
||||
let screen_press = press * ppp + vec2(origin.x as f32, origin.y as f32);
|
||||
let monitor = window
|
||||
.available_monitors()
|
||||
.find(|m| {
|
||||
let p = m.position();
|
||||
let s = m.size();
|
||||
Rect::from_min_size(pos2(p.x as f32, p.y as f32), vec2(s.width as f32, s.height as f32)).contains(screen_press)
|
||||
})
|
||||
.or_else(|| window.current_monitor());
|
||||
let Some(monitor) = monitor else { return };
|
||||
let p = monitor.position();
|
||||
let s = monitor.size();
|
||||
let bounds = Rect::from_min_size(
|
||||
pos2((p.x as f64 - origin.x as f64) as f32, (p.y as f64 - origin.y as f64) as f32) / ppp,
|
||||
vec2(s.width as f32, s.height as f32) / ppp,
|
||||
);
|
||||
finish_frame(ctx, bounds, |to| {
|
||||
// Reuse winit's physical position type obtained above, now client-relative.
|
||||
let mut target = origin;
|
||||
target.x = (to.x * ppp).round() as i32;
|
||||
target.y = (to.y * ppp).round() as i32;
|
||||
match window.set_cursor_position(target) {
|
||||
// Remember the actual device pixel to avoid fractional-DPI drift on every wrap.
|
||||
Ok(()) => Some(pos2(target.x as f32 / ppp, target.y as f32 / ppp)),
|
||||
Err(e) => {
|
||||
log::warn!("numeric cursor wrap is unavailable: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use egui::{Event, Id, Modifiers, PointerButton, pos2, vec2};
|
||||
|
||||
struct Scrub {
|
||||
ctx: Context,
|
||||
value: f64,
|
||||
bounds: Rect,
|
||||
time: f64,
|
||||
accept_warp: bool,
|
||||
warps: Vec<Pos2>,
|
||||
standard: bool,
|
||||
ordinary: bool,
|
||||
modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl Scrub {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
ctx: Context::default(),
|
||||
value: 0.0,
|
||||
bounds: Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0)),
|
||||
time: 0.0,
|
||||
accept_warp: true,
|
||||
warps: Vec::new(),
|
||||
standard: false,
|
||||
ordinary: false,
|
||||
modifiers: Modifiers::NONE,
|
||||
}
|
||||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<Event>, focused: bool) {
|
||||
self.time += 0.02;
|
||||
let mut raw = RawInput {
|
||||
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))),
|
||||
time: Some(self.time),
|
||||
events,
|
||||
focused,
|
||||
..Default::default()
|
||||
};
|
||||
raw.events.push(Event::ModifiersChanged(self.modifiers));
|
||||
prepare_input(&self.ctx, &mut raw);
|
||||
let _ = self.ctx.run_ui(raw, |ui| {
|
||||
if self.standard {
|
||||
ui.put(
|
||||
Rect::from_min_size(pos2(100.0, 100.0), vec2(60.0, 20.0)),
|
||||
crate::widgets::drag_value(egui::DragValue::new(&mut self.value).speed(1.0).max_decimals(3)),
|
||||
);
|
||||
} else if self.ordinary {
|
||||
ui.interact(Rect::from_min_size(pos2(100.0, 100.0), vec2(60.0, 20.0)), Id::new("ordinary"), egui::Sense::drag());
|
||||
} else {
|
||||
let (_, value, _) = crate::widgets::hot_number_at(
|
||||
ui,
|
||||
pos2(100.0, 100.0),
|
||||
Id::new("scrub"),
|
||||
self.value,
|
||||
1.0,
|
||||
(-1e9, 1e9),
|
||||
1,
|
||||
"",
|
||||
&crate::theme::Tokens::for_kind(crate::theme::ThemeKind::Dark),
|
||||
);
|
||||
if let Some(v) = value {
|
||||
self.value = v;
|
||||
}
|
||||
}
|
||||
finish_frame(ui.ctx(), self.bounds, |p| {
|
||||
self.warps.push(p);
|
||||
self.accept_warp.then_some(p)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn move_to(&mut self, p: Pos2) {
|
||||
self.frame(vec![Event::PointerMoved(p)], true);
|
||||
}
|
||||
|
||||
fn begin(&mut self) {
|
||||
self.frame(vec![], true);
|
||||
let p = pos2(105.0, 108.0);
|
||||
self.frame(
|
||||
vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }],
|
||||
true,
|
||||
);
|
||||
self.move_to(p + vec2(10.0, 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_drag_wraps_repeatedly_without_counting_the_teleport() {
|
||||
let mut s = Scrub::new();
|
||||
s.begin();
|
||||
for _ in 0..5 {
|
||||
s.move_to(pos2(799.0, 108.0));
|
||||
let target = *s.warps.last().expect("numeric drag must wrap at the monitor edge");
|
||||
assert!(target.x < 16.0);
|
||||
let before = s.value;
|
||||
s.move_to(target);
|
||||
assert_eq!(s.value, before, "the warp itself must not change the value");
|
||||
s.move_to(target + vec2(20.0, 0.0));
|
||||
assert_eq!(s.value, before + 20.0);
|
||||
}
|
||||
assert_eq!(s.warps.len(), 5);
|
||||
s.move_to(pos2(0.0, 108.0));
|
||||
let target = *s.warps.last().unwrap();
|
||||
assert!(target.x > 780.0);
|
||||
let before = s.value;
|
||||
s.move_to(target - vec2(3.0, 0.0));
|
||||
assert_eq!(s.value, before - 3.0, "coalesced movement after a left-edge warp is preserved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_pointer_movement_does_not_warp() {
|
||||
let mut s = Scrub::new();
|
||||
s.move_to(pos2(799.0, 108.0));
|
||||
assert!(s.warps.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_editor_wraps_at_top_bottom_and_corner_without_a_value_jump() {
|
||||
let mut s = Scrub::new();
|
||||
s.standard = true;
|
||||
s.begin();
|
||||
for edge in [pos2(200.0, 0.0), pos2(200.0, 599.0), pos2(799.0, 599.0)] {
|
||||
s.move_to(edge);
|
||||
let target = *s.warps.last().unwrap();
|
||||
assert_ne!(target, edge);
|
||||
let before = s.value;
|
||||
s.move_to(target);
|
||||
assert_eq!(s.value, before);
|
||||
s.move_to(target + vec2(5.0, -5.0));
|
||||
assert_eq!(s.value, before + 10.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_and_focus_loss_end_wrapping_and_restore_normal_hit_testing() {
|
||||
for lose_focus in [false, true] {
|
||||
let mut s = Scrub::new();
|
||||
s.begin();
|
||||
s.move_to(pos2(799.0, 108.0));
|
||||
let target = *s.warps.last().unwrap();
|
||||
s.move_to(target);
|
||||
let value = s.value;
|
||||
let events = if lose_focus {
|
||||
vec![Event::WindowFocused(false)]
|
||||
} else {
|
||||
vec![Event::PointerButton { pos: target, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }]
|
||||
};
|
||||
s.frame(events, !lose_focus);
|
||||
s.move_to(pos2(799.0, 108.0));
|
||||
assert_eq!(s.warps.len(), 1);
|
||||
assert_eq!(s.value, value);
|
||||
assert_eq!(s.ctx.input(|i| i.pointer.latest_pos()), Some(pos2(799.0, 108.0)));
|
||||
s.begin();
|
||||
s.move_to(pos2(799.0, 108.0));
|
||||
assert_eq!(s.warps.len(), 2, "the next independent drag still wraps");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_drag_does_not_wrap_and_failed_warp_is_not_retried() {
|
||||
let mut s = Scrub::new();
|
||||
s.ordinary = true;
|
||||
s.begin();
|
||||
s.move_to(pos2(799.0, 108.0));
|
||||
assert!(s.warps.is_empty());
|
||||
|
||||
let mut s = Scrub::new();
|
||||
s.accept_warp = false;
|
||||
s.begin();
|
||||
s.move_to(pos2(799.0, 108.0));
|
||||
for _ in 0..3 {
|
||||
s.move_to(pos2(799.0, 108.0));
|
||||
}
|
||||
assert_eq!(s.warps.len(), 1);
|
||||
let value = s.value;
|
||||
s.move_to(pos2(779.0, 108.0));
|
||||
assert_eq!(s.value, value - 20.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifiers_keep_their_sensitivity_after_wrapping() {
|
||||
for (modifiers, multiplier) in [(Modifiers::SHIFT, 10.0), (Modifiers::COMMAND, 0.1)] {
|
||||
let mut s = Scrub::new();
|
||||
s.modifiers = modifiers;
|
||||
s.begin();
|
||||
s.move_to(pos2(799.0, 108.0));
|
||||
let target = *s.warps.last().unwrap();
|
||||
let value = s.value;
|
||||
s.move_to(target + vec2(10.0, 0.0));
|
||||
assert!((s.value - value - 10.0 * multiplier).abs() < 1e-8);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monitor_rect_can_extend_beyond_the_app_and_have_a_negative_origin() {
|
||||
let mut s = Scrub::new();
|
||||
s.bounds = Rect::from_min_size(pos2(-200.0, -100.0), vec2(1200.0, 800.0));
|
||||
s.begin();
|
||||
s.move_to(pos2(999.0, 108.0));
|
||||
let target = *s.warps.last().unwrap();
|
||||
assert_eq!(target, pos2(-192.0, 108.0));
|
||||
let value = s.value;
|
||||
s.frame(vec![Event::PointerGone, Event::PointerMoved(target)], true);
|
||||
assert_eq!(s.value, value);
|
||||
s.move_to(target + vec2(10.0, 0.0));
|
||||
assert_eq!(s.value, value + 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_properties_and_textbox_drags_keep_each_gesture_in_one_undo_step() {
|
||||
use crate::{EffectcraftApp, dock::PanelKind};
|
||||
use effectcraft_engine::project::LayerId;
|
||||
use eframe::App;
|
||||
use serde_json::json;
|
||||
|
||||
for textbox in [false, true] {
|
||||
let mut session = effectcraft_host::session();
|
||||
// This patch also works without the optional TextBox plugin.
|
||||
if textbox && effectcraft_engine::effects::plugin::plugin("org.effectcraft.text-box").is_none() {
|
||||
continue;
|
||||
}
|
||||
session.execute("comp.new", json!({"width": 32, "height": 16})).unwrap();
|
||||
let lid = session.execute("layer.newSolid", json!({"width": 8, "height": 8})).unwrap()["layer"].as_u64().unwrap();
|
||||
if textbox {
|
||||
session.execute("effect.apply", json!({"layer": lid, "effect": "TextBox"})).unwrap();
|
||||
}
|
||||
let path = if textbox { "effects/#1/paddingX" } else { "transform/position" };
|
||||
let prop = session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.prop(path).unwrap();
|
||||
let uid = prop.uid;
|
||||
let original = prop.value.clone();
|
||||
let undo = session.history.undo.len();
|
||||
let panel = if textbox { PanelKind::EffectControls } else { PanelKind::Properties };
|
||||
let id = if textbox { format!("effectControls.prop.{uid}.value") } else { format!("properties.prop.{uid}.value.0") };
|
||||
let mut app = EffectcraftApp::new(session);
|
||||
app.show_panel(panel);
|
||||
app.toggle_maximize(panel);
|
||||
let ctx = Context::default();
|
||||
let bounds = Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0));
|
||||
let mut frame = eframe::Frame::_new_kittest();
|
||||
let mut time = 0.0;
|
||||
let mut step = |app: &mut EffectcraftApp, events| {
|
||||
time += 1.0 / 60.0;
|
||||
let mut raw = RawInput { screen_rect: Some(bounds), time: Some(time), events, ..Default::default() };
|
||||
app.raw_input_hook(&ctx, &mut raw);
|
||||
let mut warp = None;
|
||||
let _ = ctx.run_ui(raw, |ui| {
|
||||
app.logic(ui.ctx(), &mut frame);
|
||||
app.ui(ui, &mut frame);
|
||||
finish_frame(ui.ctx(), bounds, |p| {
|
||||
warp = Some(p);
|
||||
Some(p)
|
||||
});
|
||||
});
|
||||
warp
|
||||
};
|
||||
for _ in 0..4 {
|
||||
step(&mut app, vec![]);
|
||||
}
|
||||
for gesture in 1..=2 {
|
||||
let r = app.auto.find(&id).unwrap().rect;
|
||||
let p = pos2(r[0] + r[2] / 2.0, r[1] + r[3] / 2.0);
|
||||
step(
|
||||
&mut app,
|
||||
vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }],
|
||||
);
|
||||
step(&mut app, vec![Event::PointerMoved(p + vec2(10.0, 0.0))]);
|
||||
let mut last = p;
|
||||
for _ in 0..3 {
|
||||
let target = step(&mut app, vec![Event::PointerMoved(pos2(1599.0, p.y))]).expect("real property scrubs wrap");
|
||||
let before = app.session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.find(uid).unwrap().value.clone();
|
||||
step(&mut app, vec![Event::PointerMoved(target)]);
|
||||
assert_eq!(app.session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.find(uid).unwrap().value, before);
|
||||
last = target + vec2(10.0, 0.0);
|
||||
step(&mut app, vec![Event::PointerMoved(last)]);
|
||||
}
|
||||
step(&mut app, vec![Event::PointerButton { pos: last, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }]);
|
||||
assert_eq!(app.session.history.undo.len(), undo + gesture, "each drag is one undo step: {panel:?}");
|
||||
}
|
||||
app.session.execute("edit.undo", json!({})).unwrap();
|
||||
app.session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.find(uid).unwrap().value, original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +104,7 @@ pub fn hot_number_at(
|
|||
return (er, out, out.is_some());
|
||||
}
|
||||
let resp = ui.interact(rect, id, Sense::click_and_drag());
|
||||
crate::pointer_wrap::track(&resp);
|
||||
let col = if resp.hovered() || resp.dragged() { t.accent_hover } else { t.hot_text };
|
||||
ui.painter().galley_with_override_text_color(rect.min + vec2(2.0, 2.0), galley, col);
|
||||
if resp.hovered() || resp.dragged() {
|
||||
|
|
@ -154,6 +155,15 @@ pub fn hot_int_at(ui: &mut Ui, rect_min: egui::Pos2, id: egui::Id, value: i64, s
|
|||
(rect, (step != 0.0).then(|| value.saturating_add(step as i64)), done)
|
||||
}
|
||||
|
||||
/// Standard numeric editors use the same continuous native scrubbing as hot text.
|
||||
pub fn drag_value(value: egui::DragValue<'_>) -> impl egui::Widget {
|
||||
move |ui: &mut Ui| {
|
||||
let response = ui.add(value);
|
||||
crate::pointer_wrap::track(&response);
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the keyboard on an inline editor that is open until it loses the focus. The frame it
|
||||
/// opens (it has no focus yet) its whole `text` is selected, so typing replaces the value.
|
||||
/// (Only then is the focus requested: a request resets what keys the field keeps for itself,
|
||||
|
|
|
|||
Loading…
Reference in New Issue