Cryptomatte eyedropper: arm it from the Selection in Effect Controls (#473)

An eyedropper next to the Cryptomatte effect's Selection arms the viewer pick, and the status
line says how to use it. It stays armed until Esc or the button (Esc in a text field or a menu
only ends that); disarming, New, Open and Revert end it and let go of the session's probe.

The app runs the pick's bookkeeping at the start and end of each pass, the viewer hands the
pointer to the pick while it is armed and opens its context menu for a right-click the pick
forwarded, and a Selection cut off by a narrow Effect Controls panel shows whole on hover.

UI tests: ui_crypto_pick (arming, hover, clicks, modifiers, drags, waiting clicks, status,
highlight) and ui_crypto_pick_undo (one undo step per pick, the probe kept over undo).
This commit is contained in:
Matej Kozlovský committed 2026-11-03 07:37:53 +01:00
1 parent 6b0f72e4be
commit 7cea1625f6
7 files changed
+3252 -11

No files matched your search

+2
View File
@@ -327,7 +327,9 @@ impl EffectcraftApp {
ctx.request_repaint();
return;
}
panels::crypto_pick::pass_start(self, &ctx);
self.frame(ui);
panels::crypto_pick::upkeep(self, &ctx);
#[cfg(not(target_arch = "wasm32"))]
pointer_wrap::finish_native(&ctx, frame);
// wasm32: no frame threads; render queued frames now, between UI frames.
@@ -1,7 +1,7 @@
use super::common::{Actions, DIAL_ROW, PARAM_NAME_X, ROW, TREE_INDENT, gesture_key};
use super::{
Align2, Color32, EffectcraftApp, EvalCtx, FxPick, Icon, Layer, Node, ParamUi, PropGroup, Property, Rect, Sense, Stroke, Tokens, Value, fw, icons, json,
pos2, vec2, widgets,
Align2, Color32, EffectcraftApp, EvalCtx, FxPick, GroupKind, Icon, Layer, Node, ParamUi, PropGroup, Property, Rect, Sense, Stroke, Tokens, Value, fw,
icons, json, pos2, vec2, widgets,
};
fn triangle(p: &egui::Painter, c: egui::Pos2, left: bool, col: Color32) {
@@ -65,6 +65,9 @@ pub(crate) fn toggle_pick(app: &mut EffectcraftApp, kind: &str, layer: &Layer, p
app.ui.text_color_pick = None;
app.ui.status = if kind == "point" {
format!("Click in the Composition panel to set {}", prop.name)
} else if kind == crate::panels::crypto_pick::KIND {
let keys = crate::panels::crypto_pick::remove_keys(egui::os::OperatingSystem::from_target_os());
crate::i18n::tr_args("Click IDs in the Composition panel: Shift adds, {} removes, Esc ends", &[&keys])
} else {
format!("Click in the Composition panel to sample {}", prop.name)
};
@@ -485,11 +488,27 @@ fn value_text(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value: &
let Value::Str(s) = value else {
return;
};
let PropCx { t, uid, cy, vx, r, layer, prop, .. } = *cx;
let fr = Rect::from_min_size(pos2(vx, cy - 9.0), vec2((r.max.x - vx - 12.0).clamp(80.0, 320.0), 18.0));
let PropCx { t, uid, cy, vx, r, layer, group, prop, .. } = *cx;
// Cryptomatte's Selection also has an eyedropper that picks IDs in the viewer (#473).
let crypto = matches!(&group.kind, GroupKind::Effect { effect } if effect == "ec.3d.cryptomatte") && prop.match_id == "selection";
let fr = Rect::from_min_size(pos2(vx, cy - 9.0), vec2((r.max.x - vx - if crypto { 36.0 } else { 12.0 }).clamp(80.0, 320.0), 18.0));
if crypto {
let er = Rect::from_center_size(pos2(fr.max.x + 14.0, cy), vec2(18.0, 18.0));
let active = app.ui.fx_pick.as_ref().is_some_and(|k| k.prop == uid && k.kind == crate::panels::crypto_pick::KIND);
if fw::eyedropper_button(ui, er, active, egui::Id::unique(("ec-eye", uid)), &t)
.on_hover_text(crate::i18n::tr("Eyedropper: click, then click in the Composition panel"))
.clicked()
{
toggle_pick(app, crate::panels::crypto_pick::KIND, layer, prop);
}
app.auto.add(&format!("effectControls.prop.{uid}.eyedropper"), er, &prop.name);
}
let bid = egui::Id::unique(("ec-text", uid));
let mut buf: String = ui.data(|d| d.get_temp(bid)).unwrap_or_else(|| s.to_owned());
let resp = widgets::text_field(ui, fr, &mut buf, "", &t);
// Cut off by a narrow panel (a long Selection): the whole text on hover.
let cut = !resp.has_focus() && ui.painter().layout_no_wrap(buf.clone(), egui::TextStyle::Body.resolve(ui.style()), t.text).size().x > fr.width() - 17.0;
let resp = if cut { resp.on_hover_text(buf.clone()) } else { resp };
// The field has no visible hint: give the screen reader the parameter's name.
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::TextInput, ui.is_enabled(), &prop.name));
app.auto.add(&format!("effectControls.prop.{uid}.value"), fr, &prop.name);
@@ -221,9 +221,19 @@ pub fn viewer_hook(
let ctx = ui.ctx().clone();
crate::color_picker::note_viewer(&ctx, map.area);
let mut actions: Actions = vec![];
let point_preview = match fx_pick_session(app, ui, painter, map, ectx, l2c, &mut actions) {
ControlFlow::Break(()) => return,
ControlFlow::Continue(p) => p,
let point_preview = if let Some(pick) = app.ui.fx_pick.clone().filter(|p| p.kind == crate::panels::crypto_pick::KIND) {
match ectx.comp.layer(effectcraft_engine::project::LayerId(pick.layer)) {
Some(l) => {
crate::panels::crypto_pick::viewer_hook(app, ui, painter, map, ectx, l, &crate::panels::viewer::from_effect_space(ectx, l, l2c(ectx, l)), &pick)
}
None => app.ui.fx_pick = None,
}
None
} else {
match fx_pick_session(app, ui, painter, map, ectx, l2c, &mut actions) {
ControlFlow::Break(()) => return,
ControlFlow::Continue(p) => p,
}
};
text_color_pick(app, ui, painter, map, ectx, &mut actions);
popup_eyedropper(app, ui, painter, map);
+6 -4
View File
@@ -830,9 +830,9 @@ fn phase_menu(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, v: &m
// Interaction.
let resp = ui.interact(area, egui::Id::unique("viewer-interact"), Sense::click_and_drag());
let menu_hits_id = egui::Id::unique(("viewer-menu-hits", cid.0));
if resp.secondary_clicked()
&& let Some(pos) = resp.interact_pointer_pos()
{
// A right-click an overlay took (the Cryptomatte eyedropper's) opens the menu too.
let forwarded = ui.data_mut(|d| d.remove_temp::<egui::Pos2>(egui::Id::unique(FORWARDED_SECONDARY)));
if let Some(pos) = resp.interact_pointer_pos().filter(|_| resp.secondary_clicked()).or(forwarded) {
// Picking an already selected layer preserves the group; an unselected hit
// becomes the target, using the same visible/unlocked hit test as left-click.
pick(app, ectx, map.to_comp(pos), false);
@@ -844,7 +844,9 @@ fn phase_menu(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, v: &m
app.session.state.selected_layers.iter().filter(|id| comp.layer(**id).is_some_and(|l| !l.switches.locked)).map(|id| id.0).collect();
let mut context_actions = Vec::new();
let copy_frame = crate::i18n::label(app, "comp.copyFrame", "Copy Frame to Clipboard");
crate::widgets::context_menu(&resp, |ui| {
let menu = egui::Popup::context_menu(&resp).style(crate::widgets::menu_style);
let open = forwarded.map(|_| egui::containers::SetOpenCommand::Bool(true));
if open.is_some() { menu.open_memory(open) } else { menu }.show(|ui| {
let hits = ui.data(|d| d.get_temp::<Vec<(u64, String)>>(menu_hits_id)).unwrap_or_default();
if !hits.is_empty() {
ui.menu_button(crate::i18n::tr("Select"), |ui| {
+2
View File
@@ -23,6 +23,8 @@ mod ui_color_hex;
mod ui_comp_presets;
mod ui_contrast;
mod ui_control_modifiers;
mod ui_crypto_pick;
mod ui_crypto_pick_undo;
mod ui_dialog_preview;
mod ui_dock_resize;
mod ui_drag_drop;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff