Merge pull request #1226 from nanox333/fix/1190-checkbox-drag

Fix/1190 checkbox drag
This commit is contained in:
BFlat
2026-10-30 11:03:54 +07:00
committed by GitHub
26 changed files with 475 additions and 63 deletions
+2 -2
View File
@@ -226,7 +226,7 @@ fn snapping_toggle(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte
let snap = Rect::from_min_size(pos2(x, cy - 10.0), vec2(20.0, 20.0));
// The engine owns snapping (View ▸ Snapping); the checkbox mirrors it.
app.ui.snapping = app.session.state.snapping;
if widgets::checkbox(ui, snap, app.ui.snapping, &t, egui::Id::unique("snapping")).clicked() {
if widgets::checkbox(ui, snap, app.ui.snapping, &t, egui::Id::unique("snapping")).changed() {
let _ = app.session.execute("view.snapping", serde_json::json!({}));
app.ui.snapping = app.session.state.snapping;
}
@@ -745,7 +745,7 @@ fn puppet_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter
p.text(pos2(x, cy), Align2::LEFT_CENTER, tr("Mesh:"), Tokens::ui(12.0), t.text_dim);
x += 40.0;
let show = Rect::from_min_size(pos2(x, cy - 10.0), vec2(20.0, 20.0));
if widgets::checkbox(ui, show, o.show_mesh, &t, egui::Id::unique("puppet-show-mesh")).clicked() {
if widgets::checkbox(ui, show, o.show_mesh, &t, egui::Id::unique("puppet-show-mesh")).changed() {
let _ = app.session.execute("puppet.mesh", json!({"showMesh": !o.show_mesh}));
}
app.auto.add("header.puppet.showMesh", show, "Show mesh");
+1 -1
View File
@@ -45,7 +45,7 @@ fn number(app: &mut EffectcraftApp, ui: &mut egui::Ui, at: egui::Pos2, auto: &st
fn checkbox(app: &mut EffectcraftApp, ui: &mut egui::Ui, at: egui::Pos2, auto: &str, text: &str, on: bool) -> bool {
let t = app.tokens;
let r = Rect::from_min_size(at, vec2(16.0, 16.0));
let clicked = widgets::checkbox(ui, r, on, &t, egui::Id::unique(("animtool-cb", auto))).clicked();
let clicked = widgets::checkbox(ui, r, on, &t, egui::Id::unique(("animtool-cb", auto))).changed();
ui.painter().text(pos2(r.max.x + 6.0, r.center().y), Align2::LEFT_CENTER, text, Tokens::ui(12.0), t.text);
app.auto.add(auto, r, text);
if clicked { !on } else { on }
+3 -3
View File
@@ -310,7 +310,7 @@ fn tab_body(
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 = tr_args("Lock Aspect Ratio to {}", &[&(aspect_label(ow as f64, oh as f64))]);
let r = ui.checkbox(&mut d.lock_aspect, lbl);
let r = crate::widgets::labelled_checkbox(ui, &mut d.lock_aspect, lbl);
auto.add("dialog.comp.lockAspect", r.rect, "Lock Aspect Ratio");
if d.lock_aspect && ow != d.width && ow > 0 {
d.height = ((d.width as f64) * oh as f64 / ow as f64).round().max(4.0) as u32;
@@ -426,11 +426,11 @@ fn tab_body(
});
ui.end_row();
ui.label("");
let r = ui.checkbox(&mut d.preserve_frame_rate, tr("Preserve frame rate when nested or in render queue"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.preserve_frame_rate, tr("Preserve frame rate when nested or in render queue"));
auto.add("dialog.comp.preserveFrameRate", r.rect, "Preserve frame rate when nested or in render queue");
ui.end_row();
ui.label("");
let r = ui.checkbox(&mut d.preserve_resolution, tr("Preserve resolution when nested"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.preserve_resolution, tr("Preserve resolution when nested"));
auto.add("dialog.comp.preserveResolution", r.rect, "Preserve resolution when nested");
ui.end_row();
ui.label(egui::RichText::new(tr("Motion Blur")).strong());
+6 -2
View File
@@ -584,7 +584,11 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.horizontal(|ui| {
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))]));
let r = crate::widgets::labelled_checkbox(
ui,
&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();
@@ -640,7 +644,7 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
if editing.is_some() {
ui.add_space(8.0);
let r = ui
.add_enabled(shared, egui::Checkbox::new(&mut affect_all, tr("Affect all layers that use this solid")))
.add_enabled(shared, crate::widgets::DragCheckbox::new(&mut affect_all, tr("Affect all layers that use this solid")))
.on_disabled_hover_text(tr("Only this layer uses this solid."));
auto.add("dialog.solid.affectAll", r.rect, "Affect all layers that use this solid");
if shared && !affect_all {
+3 -3
View File
@@ -231,12 +231,12 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.end_row();
ui.label(tr("Depth of Field"));
ui.checkbox(&mut d.dof, tr("Enable Depth of Field"));
crate::widgets::labelled_checkbox(ui, &mut d.dof, tr("Enable Depth of Field"));
ui.end_row();
ui.label(tr("Focus Distance"));
ui.horizontal(|ui| {
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"));
crate::widgets::labelled_checkbox(ui, &mut d.lock_to_zoom, tr("Lock to Zoom"));
});
ui.end_row();
ui.label(tr("Aperture"));
@@ -354,7 +354,7 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
}
if casts_shadows(d.kind) {
ui.label(tr("Shadows"));
let cb = ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
let cb = crate::widgets::labelled_checkbox(ui, &mut d.casts_shadows, tr("Casts Shadows"));
app.auto.add_response("dialog.light.casts", &cb, "Casts Shadows");
ui.end_row();
ui.label(tr("Shadow Darkness"));
@@ -367,7 +367,7 @@ fn value_toggle(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value:
};
let PropCx { t, uid, cy, vx, p, prop, .. } = *cx;
let cr = Rect::from_min_size(pos2(vx, cy - 8.0), vec2(16.0, 16.0));
if widgets::checkbox(ui, cr, *b, &t, egui::Id::unique(("ec-b", uid))).clicked() {
if widgets::checkbox(ui, cr, *b, &t, egui::Id::unique(("ec-b", uid))).changed() {
cx.set(actions, json!(!b));
}
p.text(pos2(cr.max.x + 6.0, cy), Align2::LEFT_CENTER, &prop.name, Tokens::ui(12.0), t.text_dim);
+1 -1
View File
@@ -362,7 +362,7 @@ fn value_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, auto: &str, ty: Opt
}
ControlType::Checkbox => {
let mut on = v.as_bool();
let r = ui.checkbox(&mut on, "");
let r = crate::widgets::labelled_checkbox(ui, &mut on, "");
if r.changed() {
out = Some(json!(on));
}
+8 -8
View File
@@ -888,7 +888,7 @@ fn form_grid(ui: &mut egui::Ui, f: &mut Form, regs: &mut Vec<(String, egui::Rect
.inner
.rect
}
FieldKind::Bool(b) => ui.checkbox(b, "").rect,
FieldKind::Bool(b) => crate::widgets::labelled_checkbox(ui, b, "").rect,
FieldKind::Choice { options, sel } => {
let cur = options.get(*sel).map(|o| o.0.clone()).unwrap_or_default();
// The column keeps its width: a long choice (a GPU adapter's name) ends in an
@@ -1018,13 +1018,13 @@ pub fn show_view_options(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Toke
let mut close = false;
super::dialogs::modal(ctx, "View Options", vec2(360.0, 330.0), t, |ui| {
let v = &mut app.ui.viewer;
ui.checkbox(&mut v.show_layer_controls, tr("Layer controls"));
ui.checkbox(&mut v.show_masks, tr("Masks"));
ui.checkbox(&mut v.rulers, tr("Rulers"));
ui.checkbox(&mut v.guides, tr("Guides"));
ui.checkbox(&mut v.grid, tr("Grid"));
ui.checkbox(&mut v.safe_margins, tr("Title/action safe"));
ui.checkbox(&mut v.transparency_grid, tr("Transparency grid"));
crate::widgets::labelled_checkbox(ui, &mut v.show_layer_controls, tr("Layer controls"));
crate::widgets::labelled_checkbox(ui, &mut v.show_masks, tr("Masks"));
crate::widgets::labelled_checkbox(ui, &mut v.rulers, tr("Rulers"));
crate::widgets::labelled_checkbox(ui, &mut v.guides, tr("Guides"));
crate::widgets::labelled_checkbox(ui, &mut v.grid, tr("Grid"));
crate::widgets::labelled_checkbox(ui, &mut v.safe_margins, tr("Title/action safe"));
crate::widgets::labelled_checkbox(ui, &mut v.transparency_grid, tr("Transparency grid"));
ui.add_space(12.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)).clicked() {
+1 -1
View File
@@ -168,7 +168,7 @@ pub fn velocity(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.add_space(8.0);
}
let r = ui.checkbox(&mut d.continuous, tr("Continuous (Lock Outgoing to Incoming)"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.continuous, tr("Continuous (Lock Outgoing to Incoming)"));
app.auto.add("dialog.velocity.continuous", r.rect, "Continuous");
if d.continuous {
d.out_speed = d.in_speed.clone();
@@ -89,7 +89,7 @@ fn control(ui: &mut egui::Ui, p: &Property, v: &KV) -> Option<Value> {
match (v, &p.ui) {
(KV::Bool(b), _) => {
let mut on = *b;
ui.checkbox(&mut on, "").changed().then(|| json!(on))
crate::widgets::labelled_checkbox(ui, &mut on, "").changed().then(|| json!(on))
}
(KV::Enum(i), ParamUi::Popup { options }) => {
let mut sel = *i as usize;
@@ -177,7 +177,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
if id != BLENDING {
let g = styles.as_ref().and_then(|s| s.sub(id));
let mut on = g.is_some_and(|g| g.enabled);
if ui.checkbox(&mut on, "").changed() {
if crate::widgets::labelled_checkbox(ui, &mut on, "").changed() {
if g.is_none() {
actions.push(("layer.style.add".into(), json!({"layers": [lid], "style": id})));
} else {
+2 -2
View File
@@ -155,7 +155,7 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.end_row();
});
ui.add_space(8.0);
let r = ui.checkbox(&mut d.cue, tr("Cue Point"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.cue, tr("Cue Point"));
app.auto.add("dialog.marker.cue", r.rect, "Cue Point");
ui.add_enabled_ui(d.cue, |ui| {
ui.horizontal(|ui| {
@@ -191,7 +191,7 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.add_space(8.0);
ui.horizontal(|ui| {
let r = ui.checkbox(&mut d.protected, tr("Protected Region"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.protected, tr("Protected Region"));
app.auto.add("dialog.marker.protected", r.rect, "Protected Region");
ui.add_space(20.0);
ui.label(tr("Label:"));
+1 -1
View File
@@ -264,7 +264,7 @@ fn fmt_rate(r: f64) -> String {
fn check_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, at: egui::Pos2, key: &str, text: &str, on: bool) -> bool {
let t = app.tokens;
let r = Rect::from_min_size(pos2(at.x, at.y - 8.0), vec2(16.0, 16.0));
let clicked = widgets::checkbox(ui, r, on, &t, egui::Id::unique(("pv-check", key))).clicked();
let clicked = widgets::checkbox(ui, r, on, &t, egui::Id::unique(("pv-check", key))).changed();
p.text(pos2(r.max.x + 5.0, at.y), Align2::LEFT_CENTER, text, Tokens::ui(12.0), t.text);
app.auto.add(&format!("preview.{key}"), r, text);
clicked
+3 -3
View File
@@ -239,7 +239,7 @@ pub fn paint(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
.enumerate()
{
let r = Rect::from_min_size(pos2(x0 + i as f32 * (w / 2.0), y), vec2(18.0, 20.0));
if widgets::checkbox(ui, r, on, &t, egui::Id::unique(key)).clicked() {
if widgets::checkbox(ui, r, on, &t, egui::Id::unique(key)).changed() {
set(app, json!({k: !on}));
}
p.text(pos2(r.max.x + 4.0, r.center().y), Align2::LEFT_CENTER, label, Tokens::ui(11.5), t.text_dim);
@@ -273,7 +273,7 @@ pub fn paint(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
y += 26.0;
let r = Rect::from_min_size(pos2(x0, y), vec2(18.0, 20.0));
if widgets::checkbox(ui, r, o.show_overlay, &t, egui::Id::unique("paint.showOverlay")).clicked() {
if widgets::checkbox(ui, r, o.show_overlay, &t, egui::Id::unique("paint.showOverlay")).changed() {
set(app, json!({"showOverlay": !o.show_overlay}));
}
app.auto.add("paint.showOverlay", r, "Clone Source Overlay");
@@ -282,7 +282,7 @@ pub fn paint(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
set(app, json!({"overlayOpacity": nv}));
}
let dr = Rect::from_min_size(pos2(x0 + w - 18.0, y), vec2(18.0, 20.0));
if widgets::checkbox(ui, dr, o.overlay_difference, &t, egui::Id::unique("paint.overlayDifference")).clicked() {
if widgets::checkbox(ui, dr, o.overlay_difference, &t, egui::Id::unique("paint.overlayDifference")).changed() {
set(app, json!({"overlayDifference": !o.overlay_difference}));
}
app.auto.add("paint.overlayDifference", dr, "Difference mode overlay");
+1 -1
View File
@@ -23,7 +23,7 @@ pub fn dropdown(app: &mut EffectcraftApp, ui: &mut egui::Ui, r: Rect, auto: &str
pub fn checkbox(app: &mut EffectcraftApp, ui: &mut egui::Ui, at: egui::Pos2, auto: &str, text: &str, on: bool) -> bool {
let t = app.tokens;
let r = Rect::from_min_size(at, vec2(16.0, 16.0));
let clicked = widgets::checkbox(ui, r, on, &t, egui::Id::unique(("kit-cb", auto))).clicked();
let clicked = widgets::checkbox(ui, r, on, &t, egui::Id::unique(("kit-cb", auto))).changed();
ui.painter().text(pos2(r.max.x + 6.0, r.center().y), Align2::LEFT_CENTER, text, Tokens::ui(12.0), t.text);
let full = Rect::from_min_size(at, vec2(22.0 + text.len() as f32 * 6.5, 16.0));
app.auto.add(auto, full, text);
+1 -1
View File
@@ -54,7 +54,7 @@ pub fn create_nulls(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
acts.push((id, if id == "paths.tracePath" { json!({"loop": looping}) } else { json!({}) }));
}
}
let r = ui.checkbox(&mut looping, tr("Loop (Trace Path)"));
let r = crate::widgets::labelled_checkbox(&mut ui, &mut looping, tr("Loop (Trace Path)"));
app.auto.add("createNulls.loop", r.rect, "Loop");
ui.data_mut(|d| d.insert_temp(loop_id, looping));
run(app, &ctx, acts);
+2 -2
View File
@@ -141,11 +141,11 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
);
ui.add_space(4.0);
ui.add_enabled_ui(!d.leave, |ui| {
let r = ui.checkbox(&mut d.adjust, tr("Adjust composition duration to the time span of the selected layers"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.adjust, tr("Adjust composition duration to the time span of the selected layers"));
app.auto.add("dialog.precompose.adjust", r.rect, "Adjust composition duration");
});
ui.add_space(8.0);
let r = ui.checkbox(&mut d.open, tr("Open New Composition"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.open, tr("Open New Composition"));
app.auto.add("dialog.precompose.open", r.rect, "Open New Composition");
ui.add_space(14.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+1 -1
View File
@@ -1341,7 +1341,7 @@ fn column_menu(app: &mut EffectcraftApp, resp: &egui::Response) {
ui.label(egui::RichText::new(crate::i18n::tr("Columns")).weak());
for (key, label, _) in COLUMNS {
let mut on = app.ui.project_columns.iter().any(|c| c == key);
if ui.checkbox(&mut on, crate::i18n::tr(label)).changed() {
if crate::widgets::labelled_checkbox(ui, &mut on, crate::i18n::tr(label)).changed() {
set_column(&mut app.ui.project_columns, key, on);
}
}
+1 -1
View File
@@ -456,7 +456,7 @@ fn text_section(
for (i, (key, label, on, c)) in [("fill", "Fill", doc.apply_fill, doc.fill), ("stroke", "Stroke", doc.apply_stroke, doc.stroke)].into_iter().enumerate() {
let ry = y + i as f32 * 24.0;
let cr = Rect::from_min_size(pos2(x0, ry + 2.0), vec2(14.0, 14.0));
if widgets::checkbox(ui, cr, on, &t, egui::Id::unique(("props-apply", key))).clicked() {
if widgets::checkbox(ui, cr, on, &t, egui::Id::unique(("props-apply", key))).changed() {
let k = if key == "fill" { "applyFill" } else { "applyStroke" };
set(actions, json!({k: !on}));
}
+2 -2
View File
@@ -501,7 +501,7 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
// Notify (sound + notification when the queue finishes).
let notify = app.session.project.render_prefs.notify;
let nb = Rect::from_min_size(pos2(render_b.min.x - 8.0 - 2.0 * bw - 8.0, render_b.min.y + 4.0), vec2(18.0, 18.0));
if widgets::checkbox(ui, nb, notify, &t, egui::Id::unique("rq-notify")).clicked() {
if widgets::checkbox(ui, nb, notify, &t, egui::Id::unique("rq-notify")).changed() {
actions.push(("renderQueue.setNotify", json!({"notify": !notify})));
}
p.text(pos2(nb.max.x + 5.0, nb.center().y), Align2::LEFT_CENTER, tr("Notify"), Tokens::ui(11.5), t.text_dim);
@@ -797,7 +797,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
app.auto.add(&aid("twirl"), tw, "Twirl");
let cb = Rect::from_center_size(pos2(cx_render + 20.0, row.center().y), vec2(18.0, 18.0));
if widgets::checkbox(ui, cb, it.render, &t, egui::Id::unique(("rq-cb", it.id))).clicked() && !rendering {
if ui.add_enabled_ui(!rendering, |ui| widgets::checkbox(ui, cb, it.render, &t, egui::Id::unique(("rq-cb", it.id)))).inner.changed() && !rendering {
actions.push(("renderQueue.setRender", json!({"item": it.id, "render": !it.render})));
}
app.auto.add(&aid("render"), cb, if it.render { "Render: on" } else { "Render: off" });
+1 -1
View File
@@ -106,7 +106,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
match kind {
Kind::Bool => {
let mut b = v.as_bool().unwrap_or(false);
let r = ui.checkbox(&mut b, tr(label));
let r = crate::widgets::labelled_checkbox(ui, &mut b, tr(label));
if r.changed() {
acts.push(Act::Set(key.into(), json!(b)));
}
+10 -12
View File
@@ -297,7 +297,7 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
for (i, (key, label, on, c)) in [("fill", "Fill", doc.apply_fill, doc.fill), ("stroke", "Stroke", doc.apply_stroke, doc.stroke)].into_iter().enumerate() {
let fx = x0 + i as f32 * (w / 2.0);
let cr = Rect::from_min_size(pos2(fx, y + 3.0), vec2(14.0, 14.0));
if widgets::checkbox(ui, cr, on, &t, egui::Id::unique(("char-apply", key))).clicked() && enabled {
if ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, cr, on, &t, egui::Id::unique(("char-apply", key)))).inner.changed() && enabled {
actions.push(json!({if key == "fill" { "applyFill" } else { "applyStroke" }: !on}));
}
app.auto.add(&format!("character.{key}.enabled"), cr, label);
@@ -350,7 +350,7 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
y += 32.0;
// Ligatures.
let lr = Rect::from_min_size(pos2(x0, y), vec2(14.0, 14.0));
if widgets::checkbox(ui, lr, doc.ligatures, &t, egui::Id::unique("char-ligatures")).clicked() && enabled {
if ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, lr, doc.ligatures, &t, egui::Id::unique("char-ligatures"))).inner.changed() && enabled {
actions.push(json!({"ligatures": !doc.ligatures}));
}
app.auto.add("character.ligatures", lr, "Ligatures");
@@ -374,7 +374,7 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
.enumerate()
{
let cr = Rect::from_min_size(pos2(x0 + i as f32 * 120.0, y), vec2(14.0, 14.0));
if widgets::checkbox(ui, cr, on, &t, egui::Id::unique(("char-vert", key))).clicked() && enabled {
if ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, cr, on, &t, egui::Id::unique(("char-vert", key)))).inner.changed() && enabled {
actions.push(json!({key: !on}));
}
app.auto.add(&format!("character.{key}"), cr, label);
@@ -677,7 +677,7 @@ pub fn paragraph(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
y += 30.0;
let hr = Rect::from_min_size(pos2(x0, y), vec2(14.0, 14.0));
if widgets::checkbox(ui, hr, doc.hanging_punctuation, &t, egui::Id::unique("para-hanging")).clicked() && enabled {
if ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, hr, doc.hanging_punctuation, &t, egui::Id::unique("para-hanging"))).inner.changed() && enabled {
actions.push(json!({"hangingPunctuation": !doc.hanging_punctuation}));
}
app.auto.add("paragraph.hangingPunctuation", hr, "Roman Hanging Punctuation");
@@ -777,11 +777,7 @@ pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui:
ui.set_width(rect.width().max(180.0));
// Filter row: a checkbox that keeps only the variable fonts.
let filter_h = 22.0;
let row = ui.add_sized([ui.available_width(), filter_h], egui::Button::selectable(false, ""));
let box_rect = Rect::from_min_size(row.rect.min, vec2(18.0, filter_h));
let label_rect = Rect::from_min_max(pos2(box_rect.max.x + 2.0, row.rect.min.y), row.rect.max);
let boxed = widgets::checkbox(ui, box_rect, only_variable, &t, id.with("variable-check"));
ui.painter().text(label_rect.left_center(), Align2::LEFT_CENTER, tr("Variable fonts only"), Tokens::ui(11.5), t.text);
let row = ui.add_sized([ui.available_width(), filter_h], widgets::DragCheckbox::new(&mut only_variable, tr("Variable fonts only")));
app_ref.auto.add(&format!("{auto}.variable"), row.rect, "Variable fonts only");
// #1089: fonts installed after launch appear without restarting. The button is its own
// row: the popup is only 180 px wide, so a control pinned to the filter row covers
@@ -792,11 +788,13 @@ pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui:
app_ref.ui.status = format!("{}…", tr("Rescan Fonts"));
}
app_ref.auto.add(&format!("{auto}.rescan"), resc, "Rescan Fonts");
let toggled = row.clicked() || boxed.clicked();
let toggled = row.changed();
if toggled {
only_variable = !only_variable;
ui.data_mut(|d| d.insert_temp(variable_id, only_variable));
// Typing goes on into the search field.
}
// The native checkbox can take focus on release after changing on press.
// Typing goes on into the search field after either event.
if toggled || row.clicked() {
ui.memory_mut(|m| m.request_focus(search_id));
}
let query = state.query.trim().to_lowercase();
+3 -3
View File
@@ -3058,7 +3058,7 @@ fn layer_mode_cell(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc:
}
let tr = Rect::from_center_size(pos2(cw.mode + 76.0 + 6.0, cy), vec2(14.0, 14.0));
app.auto.add(&format!("timeline.layer.{}.preserveTransparency", layer.id.0), tr, "Preserve Transparency");
if widgets::checkbox(ui, tr, layer.preserve_transparency, &t, egui::Id::unique(("pt", layer.id.0))).clicked() {
if widgets::checkbox(ui, tr, layer.preserve_transparency, &t, egui::Id::unique(("pt", layer.id.0))).changed() {
// The click gives the selection the state the clicked row toggles to,
// whatever the first selected layer's value (as the switches do).
actions.push((
@@ -3565,7 +3565,7 @@ fn group_mask_mode(ui: &mut egui::Ui, g: &mut Rw, rc: &RowCtx<'_>) {
actions.push(("layer.setMask".into(), json!({"layer": layer.id.0, "mask": uid, "mode": opts[i]})));
}
let ir = Rect::from_min_size(pos2(mr.max.x + 8.0, cy - 8.0), vec2(16.0, 16.0));
if widgets::checkbox(ui, ir, inverted, &t, egui::Id::unique(("minv", uid))).clicked() {
if widgets::checkbox(ui, ir, inverted, &t, egui::Id::unique(("minv", uid))).changed() {
actions.push(("layer.setMask".into(), json!({"layer": layer.id.0, "mask": uid, "inverted": !inverted})));
}
lp.text(pos2(ir.max.x + 4.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Inverted"), Tokens::ui(11.0), t.text_dim);
@@ -5773,7 +5773,7 @@ fn bool_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>,
let t = app.tokens;
let x = at.x;
let r = Rect::from_min_size(pos2(x, at.y - 8.0), vec2(16.0, 16.0));
if widgets::checkbox(ui, r, *b, &t, egui::Id::unique(("cb", uid))).clicked() {
if widgets::checkbox(ui, r, *b, &t, egui::Id::unique(("cb", uid))).changed() {
ve.set(actions, json!(!b));
}
app.auto.add(&format!("timeline.prop.{uid}.value"), r, &prop.name);
+7 -7
View File
@@ -257,10 +257,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
("scale", "Scale", settings.as_ref().is_some_and(|s| s.scale)),
] {
let cr = Rect::from_min_size(pos2(xx, y + 2.0), vec2(16.0, 16.0));
let resp = widgets::checkbox(ui, cr, on && transformish, &t, egui::Id::unique(("tracker-cb", key)));
let resp = ui.add_enabled_ui(transformish, |ui| widgets::checkbox(ui, cr, on && transformish, &t, egui::Id::unique(("tracker-cb", key)))).inner;
app.auto.add(&format!("tracker.{key}"), cr, text);
p.text(pos2(cr.max.x + 4.0, cr.center().y), Align2::LEFT_CENTER, text, Tokens::ui(12.0), if transformish { t.text } else { t.text_faint });
if resp.clicked() && transformish {
if resp.changed() && transformish {
run(app, &ctx, "track.setType", with(json!({ key: !on })));
}
xx += 84.0;
@@ -517,20 +517,20 @@ pub fn options_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens)
ui.add_space(8.0);
ui.label(egui::RichText::new(crate::i18n::tr("Process Before Match")).font(Tokens::medium(12.5)));
ui.horizontal(|ui| {
let r = ui.checkbox(&mut d.blur_on, crate::i18n::tr("Blur"));
let r = crate::widgets::labelled_checkbox(ui, &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, 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"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.opts.enhance, crate::i18n::tr("Enhance"));
app.auto.add("dialog.trackOptions.enhance", r.rect, "Enhance");
});
ui.add_space(8.0);
let r = ui.checkbox(&mut d.opts.subpixel, crate::i18n::tr("Subpixel Positioning"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.opts.subpixel, crate::i18n::tr("Subpixel Positioning"));
app.auto.add("dialog.trackOptions.subpixel", r.rect, "Subpixel Positioning");
let r = ui.checkbox(&mut d.opts.adapt_every_frame, crate::i18n::tr("Adapt Feature On Every Frame"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.opts.adapt_every_frame, crate::i18n::tr("Adapt Feature On Every Frame"));
app.auto.add("dialog.trackOptions.adaptEveryFrame", r.rect, "Adapt Feature On Every Frame");
let r = ui.checkbox(&mut d.opts.track_shape, crate::i18n::tr("Follow Feature Rotation and Scale"));
let r = crate::widgets::labelled_checkbox(ui, &mut d.opts.track_shape, crate::i18n::tr("Follow Feature Rotation and Scale"));
app.auto.add("dialog.trackOptions.trackShape", r.rect, "Follow Feature Rotation and Scale");
ui.add_space(8.0);
ui.horizontal(|ui| {
+357 -2
View File
@@ -415,9 +415,173 @@ pub fn text_button(ui: &mut Ui, rect: Rect, text: &str, primary: bool, t: &Token
resp
}
/// Checkbox in the AE look (small square).
/// A primary-button gesture shared by custom and labelled checkboxes in this context.
#[derive(Clone, Default)]
struct CheckboxDrag {
press_frame: u64,
target: Option<bool>,
visited: std::collections::HashSet<egui::Id>,
/// Checkboxes the pointer crossed in the frame of the press, before the checkbox it pressed on
/// had been drawn (so the value to set was not known): they take it in the next frame.
pending: std::collections::HashSet<egui::Id>,
}
/// A primary-button event of the frame, in the order it happened.
#[derive(Clone, Copy, PartialEq)]
enum Step {
Press,
Move,
Release,
}
/// The frame's primary-button events in order, each with its position. egui hit-tests the whole
/// frame at the last pointer position, so a press followed by a move in one frame would otherwise
/// be taken for a press where the pointer ended.
fn pointer_steps(ui: &Ui) -> Vec<(Step, egui::Pos2)> {
ui.input(|i| {
i.events
.iter()
.filter_map(|e| match e {
egui::Event::PointerMoved(pos) => Some((Step::Move, *pos)),
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, .. } => {
Some((if *pressed { Step::Press } else { Step::Release }, *pos))
}
_ => None,
})
.collect()
})
}
/// Returns the requested value. Mixed checkboxes (`None`) and disabled controls are
/// skipped. Each checkbox is visited once, so crossing it again cannot toggle it back. The
/// checkbox a press lands on, and the ones the pointer passes over after it, are found from the
/// frame's ordered pointer events, not from where the pointer ended.
fn drag_toggle(ui: &Ui, response: &Response, value: Option<bool>) -> Option<bool> {
let (pressed, down, released) = ui.input(|i| (i.pointer.primary_pressed(), i.pointer.primary_down(), i.pointer.primary_released()));
let frame = ui.ctx().cumulative_frame_nr();
// clicked() reads Context input; do not call it while Context data is locked.
let clicked = response.clicked();
// Whether the pointer at `pos` is over this checkbox itself (not a window above it).
let over = |pos: egui::Pos2| response.rect.contains(pos) && ui.clip_rect().contains(pos) && ui.ctx().layer_id_at(pos) == Some(response.layer_id);
// Resolved before the data lock below: layer_id_at reads the Context.
let steps: Vec<(Step, bool)> = pointer_steps(ui).into_iter().map(|(step, pos)| (step, over(pos))).collect();
ui.data_mut(|data| {
let drag = data.get_temp_mut_or_default::<CheckboxDrag>(egui::Id::unique("checkbox-drag"));
// A release in the press frame leaves crossings pending: keep them for the next frame.
let flushing = !drag.pending.is_empty() && drag.target.is_some() && frame <= drag.press_frame.saturating_add(1);
if (!down && !released && !flushing) || (pressed && drag.press_frame != frame) {
*drag = CheckboxDrag { press_frame: frame, ..Default::default() };
}
let on = value?;
if !response.enabled() {
return None;
}
// Crossed in the frame of the press before the value was known: take it now.
if drag.pending.remove(&response.id)
&& let Some(target) = drag.target
&& drag.visited.insert(response.id)
&& on != target
{
return Some(target);
}
// Whether the button is held, tracked in event order: a move after a release in the same frame is no drag.
// Before the first button event it is held exactly when that event is a release (or, with none, when it is down).
let mut held = match steps.iter().find(|(step, _)| *step != Step::Move) {
Some((step, _)) => *step == Step::Release,
None => down,
};
let mut press_seen = false;
for (step, over) in steps {
match step {
Step::Press => {
press_seen = true;
held = true;
if drag.target.is_none() && over {
drag.target = Some(!on);
drag.visited.insert(response.id);
return Some(!on);
}
}
Step::Release => held = false,
Step::Move if held => match drag.target {
Some(target) if over => {
if drag.visited.insert(response.id) && on != target {
return Some(target);
}
}
None if press_seen && over => {
drag.pending.insert(response.id);
}
_ => {}
},
_ => {}
}
}
// The pointer held still over this checkbox in a later frame of the gesture.
if down
&& !pressed
&& response.contains_pointer()
&& let Some(target) = drag.target
{
if drag.visited.insert(response.id) && on != target {
return Some(target);
}
return None;
}
// The source already changed on press. Suppress its ordinary release click.
if released && drag.target.is_some() {
return None;
}
clicked.then_some(!on)
})
}
/// A labelled checkbox with the same drag behavior as the custom panel checkbox.
/// Use through `ui.add`, `ui.put`, or `ui.add_enabled` like `egui::Checkbox`.
pub struct DragCheckbox<'a> {
value: &'a mut bool,
text: egui::WidgetText,
}
impl<'a> DragCheckbox<'a> {
pub fn new(value: &'a mut bool, text: impl Into<egui::WidgetText>) -> Self {
Self { value, text: text.into() }
}
}
impl egui::Widget for DragCheckbox<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let before = *self.value;
let mut response = ui.add(egui::Checkbox::new(self.value, self.text));
// egui changes on release; our shared gesture changes on press and entry.
*self.value = before;
response.flags.remove(egui::response::Flags::CHANGED);
if response.clicked() {
ui.ctx().request_repaint();
}
if let Some(value) = drag_toggle(ui, &response, Some(before)) {
*self.value = value;
response.mark_changed();
ui.ctx().request_repaint();
}
response
}
}
/// A labelled checkbox that allocates its own rectangle.
pub fn labelled_checkbox(ui: &mut Ui, value: &mut bool, text: impl Into<egui::WidgetText>) -> Response {
ui.add(DragCheckbox::new(value, text))
}
/// Checkbox in the AE look (small square). `changed()` requests a toggle, including
/// keyboard activation and entry during a drag begun on another checkbox.
pub fn checkbox(ui: &mut Ui, rect: Rect, on: bool, t: &Tokens, id: egui::Id) -> Response {
let resp = ui.interact(rect, id, Sense::click());
let mut resp = ui.interact(rect, id, Sense::click());
let next = drag_toggle(ui, &resp, Some(on));
if next.is_some() {
resp.mark_changed();
}
let on = next.unwrap_or(on);
let b = Rect::from_center_size(rect.center(), vec2(12.0, 12.0));
ui.painter().rect_filled(b, 2.0, if on { t.accent } else { t.field_bg });
ui.painter().rect_stroke(b, 2.0, Stroke::new(1.0, if on { t.accent } else { t.field_border }), StrokeKind::Inside);
@@ -824,6 +988,197 @@ pub(crate) fn constrained(c: &[f64], d: usize, v: f64) -> Vec<f64> {
mod tests {
use super::*;
fn checkbox_frame(ctx: &egui::Context, values: &mut [Option<bool>; 5], events: Vec<egui::Event>) {
let t = Tokens::for_kind(Default::default());
let input = egui::RawInput { events, ..Default::default() };
let mut output = ctx.run_ui(input, |ui| {
for (index, value) in values.iter_mut().enumerate() {
let rect = Rect::from_min_size(pos2(20.0, 20.0 + index as f32 * 30.0), vec2(120.0, 20.0));
ui.add_enabled_ui(index != 3, |ui| {
if index == 4 {
if let Some(on) = value {
ui.put(rect, DragCheckbox::new(on, crate::i18n::tr("Grid")));
}
} else if let Some(on) = value {
if checkbox(ui, rect, *on, &t, egui::Id::unique(index)).changed() {
*on = !*on;
}
} else {
let response = ui.interact(rect, egui::Id::unique(index), Sense::click());
assert_eq!(drag_toggle(ui, &response, None), None);
}
});
}
});
output.textures_delta.clear();
}
fn checkbox_pointer(y: f32, pressed: Option<bool>) -> Vec<egui::Event> {
let pos = pos2(28.0, y);
let mut events = vec![egui::Event::PointerMoved(pos)];
if let Some(pressed) = pressed {
events.push(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() });
}
events
}
#[test]
fn checkbox_drag_sets_one_state_skips_mixed_and_disabled_and_ends_on_release() {
let ctx = egui::Context::default();
let mut values = [Some(false), Some(true), None, Some(false), Some(false)];
checkbox_frame(&ctx, &mut values, vec![]);
checkbox_frame(&ctx, &mut values, checkbox_pointer(28.0, Some(true)));
assert_eq!(values[0], Some(true));
// An already-matching neighbour must not invert, even after revisiting.
for y in [58.0, 88.0, 118.0, 148.0, 58.0, 28.0] {
checkbox_frame(&ctx, &mut values, checkbox_pointer(y, None));
}
assert_eq!(values, [Some(true), Some(true), None, Some(false), Some(true)]);
checkbox_frame(&ctx, &mut values, checkbox_pointer(28.0, Some(false)));
assert_eq!(values[0], Some(true), "release must not toggle the source again");
// Begin on the labelled wrapper and drag back into custom checkboxes.
checkbox_frame(&ctx, &mut values, checkbox_pointer(148.0, Some(true)));
assert_eq!(values[4], Some(false));
for y in [58.0, 28.0] {
checkbox_frame(&ctx, &mut values, checkbox_pointer(y, None));
}
checkbox_frame(&ctx, &mut values, checkbox_pointer(28.0, Some(false)));
assert_eq!(values, [Some(false), Some(false), None, Some(false), Some(false)]);
checkbox_frame(&ctx, &mut values, checkbox_pointer(58.0, None));
assert_eq!(values[1], Some(false), "hover after release must not paint");
}
fn button(y: f32, pressed: bool) -> Vec<egui::Event> {
let pos = pos2(28.0, y);
vec![egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }]
}
fn moved(y: f32) -> egui::Event {
egui::Event::PointerMoved(pos2(28.0, y))
}
/// A press and the moves after it can arrive in one frame (a quick flick): the press belongs to
/// the checkbox it landed on, not to the one the pointer ended over, and the ones passed over
/// follow the pressed one's new state, whether they are drawn before it or after it.
#[test]
fn a_press_and_a_move_in_one_frame_start_on_the_checkbox_pressed() {
// Rows are 30 apart, from y = 28; index 3 is disabled.
let (a, b) = (28.0, 58.0);
// Pressed on the first, moved to the second (drawn after it).
let ctx = egui::Context::default();
let mut values = [Some(false); 5];
values[2] = None;
checkbox_frame(&ctx, &mut values, vec![]);
let mut events = vec![moved(a)];
events.extend(button(a, true));
events.push(moved(b));
checkbox_frame(&ctx, &mut values, events);
assert_eq!((values[0], values[1]), (Some(true), Some(true)), "the pressed one is the source, the one passed over follows");
let mut events = vec![moved(b)];
events.extend(button(b, false));
checkbox_frame(&ctx, &mut values, events);
assert_eq!((values[0], values[1]), (Some(true), Some(true)), "release changes nothing");
// Pressed on the second, moved to the first (drawn before it): the first takes the state in
// the next frame, while the button is still down.
let ctx = egui::Context::default();
let mut values = [Some(false); 5];
values[2] = None;
checkbox_frame(&ctx, &mut values, vec![]);
let mut events = vec![moved(b)];
events.extend(button(b, true));
events.push(moved(a));
checkbox_frame(&ctx, &mut values, events);
assert_eq!(values[1], Some(true), "the pressed one is the source");
checkbox_frame(&ctx, &mut values, vec![]);
assert_eq!(values[0], Some(true), "the one passed over takes the state");
let mut events = vec![moved(a)];
events.extend(button(a, false));
checkbox_frame(&ctx, &mut values, events);
assert_eq!((values[0], values[1]), (Some(true), Some(true)));
// Pressed outside every checkbox, then moved onto one in the same frame: nothing starts.
let ctx = egui::Context::default();
let mut values = [Some(false); 5];
values[2] = None;
checkbox_frame(&ctx, &mut values, vec![]);
let mut events = vec![moved(200.0)];
events.extend(button(200.0, true));
events.push(moved(a));
checkbox_frame(&ctx, &mut values, events);
checkbox_frame(&ctx, &mut values, vec![]);
assert_eq!(values, [Some(false), Some(false), None, Some(false), Some(false)], "a press outside starts no gesture");
// Pressed on the second, moved to the first (drawn before it) and released, all in one
// frame: the first still takes the state in the next frame.
let ctx = egui::Context::default();
let mut values = [Some(false); 5];
values[2] = None;
checkbox_frame(&ctx, &mut values, vec![]);
let mut events = vec![moved(b)];
events.extend(button(b, true));
events.push(moved(a));
events.extend(button(a, false));
checkbox_frame(&ctx, &mut values, events);
checkbox_frame(&ctx, &mut values, vec![]);
assert_eq!((values[0], values[1]), (Some(true), Some(true)), "a crossing before the release still takes the state");
}
/// A move that arrives after the release, in the frame of the click, is the pointer travelling without the button:
/// it must not carry the gesture onto the checkbox it ends over.
#[test]
fn a_move_after_the_release_in_the_same_frame_paints_nothing() {
let (a, b) = (28.0, 58.0);
let ctx = egui::Context::default();
let mut values = [Some(false); 5];
values[2] = None;
checkbox_frame(&ctx, &mut values, vec![]);
let mut events = vec![moved(a)];
events.extend(button(a, true));
events.extend(button(a, false));
events.push(moved(b));
checkbox_frame(&ctx, &mut values, events);
checkbox_frame(&ctx, &mut values, vec![]);
assert_eq!((values[0], values[1]), (Some(true), Some(false)), "the clicked one changes, the one moved onto afterwards does not");
}
#[test]
fn checkbox_click_and_keyboard_toggle_once_and_drag_must_start_on_a_checkbox() {
let ctx = egui::Context::default();
let mut values = [Some(false), Some(false), None, Some(false), Some(false)];
checkbox_frame(&ctx, &mut values, vec![]);
// Pressing outside the checkboxes must not start a gesture on entry.
checkbox_frame(&ctx, &mut values, checkbox_pointer(200.0, Some(true)));
checkbox_frame(&ctx, &mut values, checkbox_pointer(28.0, None));
checkbox_frame(&ctx, &mut values, checkbox_pointer(28.0, Some(false)));
assert_eq!(values[0], Some(false));
for source_y in [88.0, 118.0] {
checkbox_frame(&ctx, &mut values, checkbox_pointer(source_y, Some(true)));
checkbox_frame(&ctx, &mut values, checkbox_pointer(28.0, None));
checkbox_frame(&ctx, &mut values, checkbox_pointer(28.0, Some(false)));
assert_eq!(values[0], Some(false), "mixed and disabled sources cannot begin a drag");
}
for y in [28.0, 148.0] {
checkbox_frame(&ctx, &mut values, checkbox_pointer(y, Some(true)));
checkbox_frame(&ctx, &mut values, checkbox_pointer(y, Some(false)));
}
assert_eq!(values[0], Some(true));
assert_eq!(values[4], Some(true));
ctx.memory_mut(|m| m.request_focus(egui::Id::unique(0usize)));
checkbox_frame(
&ctx,
&mut values,
vec![egui::Event::Key { key: egui::Key::Space, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }],
);
assert_eq!(values[0], Some(false), "keyboard activation still works");
// Fast clicks may deliver press and release together in one input frame.
// This also exercises clicked() without an active drag (outside the data lock).
for (index, y) in [(0, 28.0), (4, 148.0)] {
let before = values[index];
let mut events = checkbox_pointer(y, Some(true));
events.extend(checkbox_pointer(y, Some(false)));
checkbox_frame(&ctx, &mut values, events);
assert_eq!(values[index], before.map(|on| !on));
}
}
/// #2097: the drag speed comes from the slider range, not a much wider hard range.
#[test]
fn slider_scrub_speed_uses_the_slider_range() {
+1
View File
@@ -15,6 +15,7 @@ mod ui_blank_context_menus;
mod ui_camera_tracker;
mod ui_character_eyedropper;
mod ui_character_stroke_order;
mod ui_checkbox_drag;
mod ui_color_hex;
mod ui_comp_presets;
mod ui_control_modifiers;
@@ -0,0 +1,54 @@
//! #1190: the shared drag gesture also reaches real dialog controls.
use effectcraft_engine::Session;
use effectcraft_ui_egui::{EffectcraftApp, menus};
use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
use egui_kittest::{Harness, kittest::Queryable as _};
use serde_json::json;
fn pointer(h: &mut Harness<'_, EffectcraftApp>, at: Pos2, pressed: Option<bool>) {
h.event(Event::PointerMoved(at));
h.step();
if let Some(pressed) = pressed {
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
h.step();
}
h.run_steps(2);
}
#[test]
fn view_options_drag_checks_and_unchecks_neighbours() {
let mut session = Session::default();
session.prefs.disk.disk_cache_enabled = false;
session.execute("comp.new", json!({"width":32,"height":32,"duration":1})).unwrap();
let mut h = Harness::builder().with_size(vec2(1200.0, 900.0)).build_eframe(|_| EffectcraftApp::new(session));
h.state_mut().ui.viewer.rulers = false;
h.state_mut().ui.viewer.guides = true;
h.state_mut().ui.viewer.grid = false;
h.run_steps(3);
let ctx = h.ctx.clone();
menus::invoke(h.state_mut(), &ctx, "view.options", json!({})).unwrap();
h.run_steps(3);
let rulers = h.get_by_label("Rulers").rect().center();
let guides = h.get_by_label("Guides").rect().center();
let grid = h.get_by_label("Grid").rect().center();
pointer(&mut h, rulers, Some(true));
assert!(h.state().ui.viewer.rulers);
pointer(&mut h, guides, None);
assert!(h.state().ui.viewer.guides, "an already-checked neighbour stays checked");
pointer(&mut h, grid, None);
assert!(h.state().ui.viewer.grid);
pointer(&mut h, rulers, None);
pointer(&mut h, rulers, Some(false));
assert!(h.state().ui.viewer.rulers, "revisiting and releasing does not undo the change");
if let Ok(path) = std::env::var("CHECKBOX_REVIEW_PNG") {
h.render().unwrap().save(path).unwrap();
}
pointer(&mut h, grid, Some(true));
pointer(&mut h, guides, None);
pointer(&mut h, rulers, None);
pointer(&mut h, rulers, Some(false));
let viewer = &h.state().ui.viewer;
assert!(!viewer.rulers && !viewer.guides && !viewer.grid);
pointer(&mut h, guides, None);
assert!(!h.state().ui.viewer.guides, "hover after release does not change a checkbox");
}