Hole: each dropdown option shows the hole it makes while hovered (#9577)

* Hole: each dropdown option shows the hole it makes while hovered

Extents, Hole Type, Hole Tap Type and Drill Point list names only, so seeing
what an option makes meant picking it. Hovering an option now shows a small
section of that hole beside the list, drawn by the dialog's own section
painter from a copy of the form with the option applied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhpMr6L9PR8SJXjM6MbQrD

* Hole: give the dropdown previews a gallery entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: danieldimitrov86 <danieldimitrovdev@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
This commit is contained in:
Daniel Dimitrov
2026-10-30 18:54:18 +00:00
committed by GitHub
parent 295bd0e165
commit 0414de7b98
5 changed files with 133 additions and 11 deletions
+18 -6
View File
@@ -193,6 +193,8 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
input_rows(d, ui, &app.session, app.preferences.origin_plane_names);
}
d.sync_material(&app.session);
// The Hole dialog's dropdowns preview each choice on a copy of the form as it stands.
let hole_form = matches!(d.kind, Kind::Hole { .. }).then(|| (d.kind.clone(), to_object_on(&d.inputs)));
match &mut d.kind {
Kind::Material { index, editor } => crate::material_editor::rows(app, ui, index, editor),
Kind::Appearance(k) => enter |= crate::dialogs_appearance::rows(app, ui, k, &mut d.inputs),
@@ -523,10 +525,19 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
}
}
Kind::Hole { kind, opts, .. } => {
let doc = &app.session.doc;
let eval = |e: &str, k| doc.eval(e, k).ok();
// Each option shows the hole it makes while hovered (dialogs_hole::preview).
let preview = |ui: &mut egui::Ui, choice| {
if let Some((form, to_object)) = &hole_form {
crate::dialogs_hole::preview(ui, form, *to_object, choice, &eval);
}
};
use crate::dialogs_hole::Choice;
form_row(ui, "Extents", |ui| {
let cur = if to_object_on(&d.inputs) { 1 } else { usize::from(opts.all) * 2 };
let mut ext = cur;
combo(ui, "hole_ext", &["Distance", "To Object", "All"], &mut ext);
ui_kit::dropdown_previews(ui, "hole_ext", &mut ext, &["Distance", "To Object", "All"], |ui, i| preview(ui, Choice::Extents(i)));
opts.all = ext == 2;
set_to_object(&mut d.inputs, &mut d.active, ext == 1);
});
@@ -542,10 +553,12 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
extra_check(ui, &mut d.extra, "flip");
});
}
choice_row(ui, "Hole Type", "hole_kind", kind, &HOLE_LABELS);
form_row(ui, "Hole Type", |ui| {
ui_kit::dropdown_previews(ui, "hole_kind", kind, &HOLE_LABELS, |ui, i| preview(ui, Choice::Type(i)));
});
form_row(ui, "Hole Tap Type", |ui| {
let mut tap = usize::from(opts.tapped);
combo(ui, "hole_tap", &["Simple", "Tapped"], &mut tap);
ui_kit::dropdown_previews(ui, "hole_tap", &mut tap, &["Simple", "Tapped"], |ui, i| preview(ui, Choice::Tapped(i == 1)));
opts.tapped = tap == 1;
});
if opts.tapped {
@@ -557,14 +570,13 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
if !opts.all {
form_row(ui, "Drill Point", |ui| {
let mut tip = usize::from(opts.angled);
combo(ui, "hole_tip", &["Flat", "Angle"], &mut tip);
ui_kit::dropdown_previews(ui, "hole_tip", &mut tip, &["Flat", "Angle"], |ui, i| preview(ui, Choice::Angled(i == 1)));
opts.angled = tip == 1;
});
}
// The section through the hole, each size beside its dimension (#2171).
let to_object = to_object_on(&d.inputs);
let doc = &app.session.doc;
enter |= crate::dialogs_hole::rows(ui, &mut d.kind, to_object, &|e, k| doc.eval(e, k).ok());
enter |= crate::dialogs_hole::rows(ui, &mut d.kind, to_object, &eval);
}
_ => enter |= additional_rows(app, d, ui),
}
+78 -5
View File
@@ -94,12 +94,45 @@ struct Section {
tip_half: f32,
}
/// The section and its sizes (`ui_kit::figure_rows`): the picture in the label column, the
/// fields beside it. Returns whether Enter was accepted in a field.
pub(crate) fn rows(ui: &mut egui::Ui, k: &mut Kind, to_object: bool, eval: &dyn Fn(&str, ValueKind) -> Option<f64>) -> bool {
let Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } = k else { return false };
/// A choice in one of the dialog's dropdowns, which [`preview`] applies to a copy of the form.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Choice {
/// Distance, To Object or All, by index.
Extents(usize),
/// Simple, Counterbore or Countersink, by index.
Type(usize),
Tapped(bool),
Angled(bool),
}
/// A small section of the hole the form would make with `choice` picked, without its sizes:
/// what a dropdown option shows while hovered, so its name need not be known to pick it.
pub(crate) fn preview(ui: &mut egui::Ui, form: &Kind, to_object: bool, choice: Choice, eval: &dyn Fn(&str, ValueKind) -> Option<f64>) {
let mut k = form.clone();
let mut to_object = to_object;
let Kind::Hole { kind, opts, .. } = &mut k else { return };
match choice {
Choice::Extents(e) => {
opts.all = e == 2;
to_object = e == 1;
}
Choice::Type(t) => *kind = t,
Choice::Tapped(on) => opts.tapped = on,
Choice::Angled(on) => opts.angled = on,
}
let Some(s) = section(&k, to_object, eval) else { return };
let t = Tokens::get();
let (r, _) = ui.allocate_exact_size(vec2(t.form_label_width, t.control_height * 3.5), egui::Sense::hover());
// `paint` keeps the right of its picture for dimensions; with none, the material fills `r`.
let w = r.width() / 0.64;
let pic = Rect::from_min_size(pos2(r.left() - 0.04 * w, r.top() - 6.0), vec2(w, r.height() + 6.0));
paint(ui.painter(), pic, &s, &[]);
}
/// What the form's hole looks like, for its picture.
fn section(k: &Kind, to_object: bool, eval: &dyn Fn(&str, ValueKind) -> Option<f64>) -> Option<Section> {
let Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } = k else { return None };
let kind = *kind;
let dims = dims(kind, opts, to_object);
let len = |e: &str| eval(e, ValueKind::Length).filter(|v| v.is_finite() && *v > 0.0);
let angle = |e: &str, lo: f64, hi: f64, or: f64| eval(e, ValueKind::Angle).filter(|v| v.is_finite()).map_or(or, |v| (v / 2.0).clamp(lo, hi));
let top = if kind == 2 { cs_diameter.as_str() } else { cb_diameter.as_str() };
@@ -137,6 +170,16 @@ pub(crate) fn rows(ui: &mut egui::Ui, k: &mut Kind, to_object: bool, eval: &dyn
cs_half: angle(cs_angle.as_str(), 0.35, 1.3, std::f64::consts::FRAC_PI_4) as f32,
tip_half: angle(opts.tip_angle.as_str(), 0.5, 1.3, 1.03) as f32,
};
Some(section)
}
/// The section and its sizes (`ui_kit::figure_rows`): the picture in the label column, the
/// fields beside it. Returns whether Enter was accepted in a field.
pub(crate) fn rows(ui: &mut egui::Ui, k: &mut Kind, to_object: bool, eval: &dyn Fn(&str, ValueKind) -> Option<f64>) -> bool {
let Some(section) = section(k, to_object, eval) else { return false };
let Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } = k else { return false };
let kind = *kind;
let dims = dims(kind, opts, to_object);
// The fields in `dims`' order, each value taken once.
let mut fields: Vec<(&str, &mut String)> = Vec::with_capacity(dims.len());
match kind {
@@ -480,6 +523,36 @@ mod tests {
}
}
#[test]
fn every_dropdown_option_previews_a_different_hole() {
let app = crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
let form = crate::dialogs::Dialog::for_command(&app, "solid.hole").unwrap().kind;
let ctx = crate::fonts::test_context();
let drawn = |choice: Choice| {
let mut size = egui::Vec2::ZERO;
let output = ctx.run_ui(Default::default(), |ui| {
let before = ui.min_rect();
preview(ui, &form, false, choice, &|_, _| Some(10.0));
size = ui.min_rect().size() - before.size();
});
assert!(size.x > 0.0 && size.y > 0.0, "{choice:?} drew nothing");
output.shapes.into_iter().map(|c| c.shape).collect::<Vec<_>>()
};
for options in [
vec![Choice::Extents(0), Choice::Extents(1), Choice::Extents(2)],
vec![Choice::Type(0), Choice::Type(1), Choice::Type(2)],
vec![Choice::Tapped(false), Choice::Tapped(true)],
vec![Choice::Angled(false), Choice::Angled(true)],
] {
let pictures: Vec<_> = options.iter().map(|c| drawn(*c)).collect();
for (i, a) in pictures.iter().enumerate() {
for (j, b) in pictures.iter().enumerate().skip(i + 1) {
assert_ne!(a, b, "{:?} and {:?} preview the same picture", options[i], options[j]);
}
}
}
}
#[test]
fn partial_tapping_fields_do_not_overlap_on_narrow_forms() {
for width in [220.0, 280.0, 360.0] {
+12
View File
@@ -146,6 +146,18 @@ impl Container {
});
}
/// [`Self::tooltip`] holding what `add` draws (a picture, say), beside `response` rather than
/// at the pointer, so it does not cover the row's neighbours.
pub(crate) fn tooltip_ui(response: &egui::Response, add: impl FnOnce(&mut egui::Ui)) {
let ctx = &response.ctx;
let mut tip = egui::Tooltip::for_enabled(response);
tip.popup = tip.popup.align(egui::RectAlign::RIGHT_START).frame(Self::new(ctx, Surface::Toast).tooltip_frame(ctx));
tip.show(|ui| {
add(ui);
crate::scenario::publish_rect("kit:tooltip", ui.min_rect());
});
}
/// Declare contacts from shell geometry; styling remains entirely in the container.
pub fn joined(mut self, edges: DockEdges) -> Self {
self.joined = Some(edges);
+18
View File
@@ -305,6 +305,24 @@ pub(crate) fn dropdown(ui: &mut Ui, id: &str, selected: &mut usize, options: &[&
})
}
/// [`dropdown`] whose options each show a picture beside them while hovered, painted by `preview`
/// for that option's index, so a choice can be seen before it is picked. Tokens: dropdown tokens,
/// the tooltip frame.
pub(crate) fn dropdown_previews(ui: &mut Ui, id: &str, selected: &mut usize, options: &[&str], preview: impl Fn(&mut Ui, usize)) -> Response {
let text = options.get(*selected).copied().map(crate::i18n::tr_label).unwrap_or("");
let row = label(ui).to_string();
dropdown_menu(ui, id, text, |ui| {
for (index, option) in options.iter().enumerate() {
let response = ui.selectable_value(selected, index, crate::i18n::tr_label(option));
scenario::publish_target(&format!("dialog:{row}:{option}"), response.rect);
Container::tooltip_ui(&response, |ui| {
preview(ui, index);
scenario::publish_rect(&format!("dialog:preview:{row}:{option}"), ui.min_rect());
});
}
})
}
/// [`dropdown`] that also says whether an option was clicked, the one already shown included (a
/// pick of the shown option changes nothing, yet it is a choice).
pub(crate) fn dropdown_picked(ui: &mut Ui, id: &str, selected: &mut usize, options: &[&str]) -> bool {
+7
View File
@@ -1334,6 +1334,13 @@ const ELEMENTS: &[Element] = &[
dropdown(ui, "gallery", &mut s.selected, &["One", "Two"]);
});
}),
element!("dropdown_previews", "dialog:row:Gallery", Some("dialog:Gallery"), |ui, s| {
row(ui, "Gallery", |ui| {
dropdown_previews(ui, "gallery_previews", &mut s.selected, &["One", "Two"], |ui, index| {
ui.label(if index == 0 { "One" } else { "Two" });
});
});
}),
element!("dropdown_picked", "dialog:row:Gallery", Some("dialog:Gallery"), |ui, s| {
row(ui, "Gallery", |ui| {
dropdown_picked(ui, "gallery_picked", &mut s.selected, &["One", "Two"]);