#1190: enable drag toggling for built-in checkboxes

This commit is contained in:
Kristian Myram
2026-10-13 11:43:31 +00:00
parent 5586b40cf0
commit ab692dce70
25 changed files with 306 additions and 63 deletions
+2 -2
View File
@@ -174,7 +174,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
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::new("snapping")).clicked() {
if widgets::checkbox(ui, snap, app.ui.snapping, &t, egui::Id::new("snapping")).changed() {
let _ = app.session.execute("view.snapping", serde_json::json!({}));
app.ui.snapping = app.session.state.snapping;
}
@@ -591,7 +591,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::new("puppet-show-mesh")).clicked() {
if widgets::checkbox(ui, show, o.show_mesh, &t, egui::Id::new("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
@@ -41,7 +41,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::new(("animtool-cb", auto))).clicked();
let clicked = widgets::checkbox(ui, r, on, &t, egui::Id::new(("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
@@ -321,7 +321,7 @@ pub fn show(
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.width).range(4..=30000).suffix(" px")));
auto.add("dialog.comp.width", r.rect, "Width");
let lbl = format!("Lock Aspect Ratio to {}", aspect_label(ow as f64, oh as f64));
let r = ui.checkbox(&mut d.lock_aspect, lbl);
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;
@@ -416,11 +416,11 @@ pub fn show(
});
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
@@ -480,7 +480,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();
@@ -536,7 +540,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
@@ -201,12 +201,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"));
@@ -301,7 +301,7 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.end_row();
}
ui.label(tr("Shadows"));
ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
crate::widgets::labelled_checkbox(ui, &mut d.casts_shadows, tr("Casts Shadows"));
ui.end_row();
ui.label(tr("Shadow Darkness"));
ui.add_enabled(d.casts_shadows, crate::widgets::drag_value(egui::DragValue::new(&mut d.shadow_darkness).range(0.0..=100.0).suffix(" %")));
+1 -1
View File
@@ -404,7 +404,7 @@ fn prop_row(
}
Value::Bool(b) => {
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::new(("ec-b", uid))).clicked() {
if widgets::checkbox(ui, cr, *b, &t, egui::Id::new(("ec-b", uid))).changed() {
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
@@ -353,7 +353,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
@@ -814,7 +814,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
@@ -944,13 +944,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
@@ -160,7 +160,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();
@@ -86,7 +86,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;
@@ -170,7 +170,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
@@ -240,7 +240,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::new(("pv-check", key))).clicked();
let clicked = widgets::checkbox(ui, r, on, &t, egui::Id::new(("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
@@ -210,7 +210,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::new(key)).clicked() {
if widgets::checkbox(ui, r, on, &t, egui::Id::new(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);
@@ -237,7 +237,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::new("paint.showOverlay")).clicked() {
if widgets::checkbox(ui, r, o.show_overlay, &t, egui::Id::new("paint.showOverlay")).changed() {
set(app, json!({"showOverlay": !o.show_overlay}));
}
app.auto.add("paint.showOverlay", r, "Clone Source Overlay");
@@ -246,7 +246,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::new("paint.overlayDifference")).clicked() {
if widgets::checkbox(ui, dr, o.overlay_difference, &t, egui::Id::new("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::new(("kit-cb", auto))).clicked();
let clicked = widgets::checkbox(ui, r, on, &t, egui::Id::new(("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
@@ -1094,7 +1094,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, label).changed() {
if crate::widgets::labelled_checkbox(ui, &mut on, label).changed() {
set_column(&mut app.ui.project_columns, key, on);
}
}
+1 -1
View File
@@ -402,7 +402,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::new(("props-apply", key))).clicked() {
if widgets::checkbox(ui, cr, on, &t, egui::Id::new(("props-apply", key))).changed() {
let k = if key == "fill" { "applyFill" } else { "applyStroke" };
set(actions, json!({k: !on}));
}
+2 -2
View File
@@ -463,7 +463,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// 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::new("rq-notify")).clicked() {
if widgets::checkbox(ui, nb, notify, &t, egui::Id::new("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);
@@ -589,7 +589,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::new(("rq-cb", it.id))).clicked() && !rendering {
if ui.add_enabled_ui(!rendering, |ui| widgets::checkbox(ui, cb, it.render, &t, egui::Id::new(("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
@@ -188,7 +188,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::new(("char-apply", key))).clicked() && enabled {
if ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, cr, on, &t, egui::Id::new(("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);
@@ -240,7 +240,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::new("char-ligatures")).clicked() && enabled {
if ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, lr, doc.ligatures, &t, egui::Id::new("char-ligatures"))).inner.changed() && enabled {
actions.push(json!({"ligatures": !doc.ligatures}));
}
app.auto.add("character.ligatures", lr, "Ligatures");
@@ -264,7 +264,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::new(("char-vert", key))).clicked() && enabled {
if ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, cr, on, &t, egui::Id::new(("char-vert", key)))).inner.changed() && enabled {
actions.push(json!({key: !on}));
}
app.auto.add(&format!("character.{key}"), cr, label);
@@ -544,7 +544,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::new("para-hanging")).clicked() && enabled {
if ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, hr, doc.hanging_punctuation, &t, egui::Id::new("para-hanging"))).inner.changed() && enabled {
actions.push(json!({"hangingPunctuation": !doc.hanging_punctuation}));
}
app.auto.add("paragraph.hangingPunctuation", hr, "Roman Hanging Punctuation");
@@ -629,17 +629,15 @@ 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.auto.add(&format!("{auto}.variable"), row.rect, "Variable fonts only");
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
@@ -2146,7 +2146,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
actions.push(("layer.setBlendMode".into(), json!({"layers": [layer.id.0], "mode": opts[i]})));
}
let tr = Rect::from_center_size(pos2(cw.mode + 76.0 + 6.0, cy), vec2(14.0, 14.0));
if widgets::checkbox(ui, tr, layer.preserve_transparency, &t, egui::Id::new(("pt", layer.id.0))).clicked() {
if widgets::checkbox(ui, tr, layer.preserve_transparency, &t, egui::Id::new(("pt", layer.id.0))).changed() {
actions.push(("layer.setSwitch".into(), json!({"layers": [layer.id.0], "switch": "preserveTransparency"})));
}
// Track matte: layer picker + kind.
@@ -2476,7 +2476,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
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::new(("minv", uid))).clicked() {
if widgets::checkbox(ui, ir, inverted, &t, egui::Id::new(("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);
@@ -4098,7 +4098,7 @@ fn value_editor(
}
Value::Bool(b) => {
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::new(("cb", uid))).clicked() {
if widgets::checkbox(ui, r, *b, &t, egui::Id::new(("cb", uid))).changed() {
set(actions, json!(!b));
}
app.auto.add(&format!("timeline.prop.{uid}.value"), r, &prop.name);
+7 -7
View File
@@ -246,10 +246,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::new(("tracker-cb", key)));
let resp = ui.add_enabled_ui(transformish, |ui| widgets::checkbox(ui, cr, on && transformish, &t, egui::Id::new(("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;
@@ -506,20 +506,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| {
+189 -2
View File
@@ -382,9 +382,96 @@ 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>,
}
/// 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.
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();
ui.data_mut(|data| {
let drag = data.get_temp_mut_or_default::<CheckboxDrag>(egui::Id::new("checkbox-drag"));
if (!down && !released) || (pressed && drag.press_frame != frame) {
*drag = CheckboxDrag { press_frame: frame, ..Default::default() };
}
let on = value?;
if !response.enabled() {
return None;
}
if pressed && drag.target.is_none() && response.is_pointer_button_down_on() && response.contains_pointer() {
drag.target = Some(!on);
}
if down
&& 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);
@@ -666,6 +753,106 @@ 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::new(index)).changed() {
*on = !*on;
}
} else {
let response = ui.interact(rect, egui::Id::new(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");
}
#[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::new(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));
}
}
/// #488: an inline editor selects its whole text only as it opens. Should it be without the
/// keyboard for a frame later (an input method, another handler), it takes the keyboard back
/// with the caret where the user put it instead of selecting everything again, which made
+54
View File
@@ -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");
}