mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 05:58:19 -06:00
Merge pull request #1226 from nanox333/fix/1190-checkbox-drag
Fix/1190 checkbox drag
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}));
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user