One frame for every window (frame.rs): command dialogs dock right and shrink to their content (260 to 380 px, 560 for table dialogs), floating windows are centred and capped (460 px, 760 for tables); header and footer rows right-align without stretching the dialog, long texts wrap; all windows migrated; tests: no egui::Window outside frame.rs, every command's dialog and the floating windows measured within limits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 20:24:10 +00:00
1 parent 74c7259825
commit 3f44110b59
16 files changed
+684 -483

No files matched your search

+402 -393
View File
@@ -578,6 +578,11 @@ impl Dialog {
})
}
/// Dialogs holding a table get the wide limit.
pub fn is_wide(&self) -> bool {
matches!(self.kind, Kind::Params { .. }) || matches!(&self.kind, Kind::Motion(k) if k.wide())
}
pub fn wants_picks(&self) -> bool {
!self.inputs.is_empty()
}
@@ -1009,8 +1014,7 @@ fn input_rows(d: &mut Dialog, ui: &mut egui::Ui) {
}
/// Width limits of a docked dialog: shrink-wrapped to its content within these.
const DIALOG_MIN_W: f32 = 252.0;
const DIALOG_MAX_W: f32 = 380.0;
use crate::frame::{DIALOG_MAX_W, DIALOG_MIN_W, DIALOG_WIDE_MAX_W};
/// Width of value fields and choice boxes in a dialog.
const FIELD_W: f32 = 124.0;
@@ -1131,7 +1135,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let mut applied = false;
let mut cancel = false;
let mut enter = false;
let wide = matches!(d.kind, Kind::Params { .. }) || matches!(&d.kind, Kind::Motion(k) if k.wide());
let wide = d.is_wide();
let heading = if d.editing.is_some() { format!("EDIT {}", title(&d.kind)) } else { title(&d.kind).to_string() };
let frame = egui::Frame::window(&ctx.global_style())
.fill(t.dialog_bg)
@@ -1141,415 +1145,420 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
// OK is offered once the inputs make a feature.
let has_ok = !matches!(d.kind, Kind::Sketch | Kind::Params { .. } | Kind::Measure { .. } | Kind::Preferences);
let valid = !has_ok || !d.previews() || (apply_commands(app, &d).is_ok() && app.preview.error.is_none());
egui::Window::new(RichText::new(heading.clone()).strong().size(13.0))
.id(egui::Id::new("sc_dialog"))
.title_bar(false)
.frame(frame)
.pivot(egui::Align2::RIGHT_TOP)
.fixed_pos(anchor)
.constrain(false)
.auto_sized()
.scroll([false, true])
.collapsible(false)
.max_height((vp.bottom() - anchor.y - 8.0).max(120.0))
.open(&mut keep)
.show(ctx, |ui| {
ui.spacing_mut().text_edit_width = FIELD_W;
let margin = 2.0 * ui.style().spacing.window_margin.leftf();
ui.set_min_width(if wide { 480.0 } else { DIALOG_MIN_W } - margin);
ui.set_max_width(if wide { 560.0 } else { DIALOG_MAX_W } - margin);
// Header: "−" folds the dialog, the command name, "»" at the right.
ui.horizontal(|ui| {
let fold = if d.collapsed { "+" } else { "−" };
if ui
.add(egui::Button::new(RichText::new(fold).size(14.0)).frame(false))
.on_hover_text(if d.collapsed { "Expand" } else { "Collapse" })
.clicked()
{
d.collapsed = !d.collapsed;
}
ui.label(RichText::new(heading.as_str()).strong().size(12.5));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(RichText::new("»").color(t.text_dim));
});
});
if d.collapsed {
return;
let width = if wide { crate::frame::Width::Wide } else { crate::frame::Width::Normal };
let wkey = egui::Id::new(("sc_dialog_w", wide));
crate::frame::docked(
RichText::new(heading.clone()).strong().size(13.0),
egui::Id::new(("sc_dialog", title(&d.kind))),
anchor,
vp.bottom() - anchor.y - 8.0,
width,
)
.frame(frame)
.scroll([false, true])
.open(&mut keep)
.show(ctx, |ui| {
ui.spacing_mut().text_edit_width = FIELD_W;
// Long texts (errors, notes) wrap instead of widening the dialog.
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
let margin = 2.0 * ui.style().spacing.window_margin.leftf();
let min_w = if wide { 480.0 } else { DIALOG_MIN_W } - margin;
ui.set_min_width(min_w);
ui.set_max_width(if wide { DIALOG_WIDE_MAX_W } else { DIALOG_MAX_W } - margin);
// Rows that end at the right edge line up with last frame's content width.
let total = crate::frame::content_width(ui, wkey, min_w);
// Header: "−" folds the dialog, the command name, "»" at the right.
ui.horizontal(|ui| {
let fold = if d.collapsed { "+" } else { "−" };
if ui
.add(egui::Button::new(RichText::new(fold).size(14.0)).frame(false))
.on_hover_text(if d.collapsed { "Expand" } else { "Collapse" })
.clicked()
{
d.collapsed = !d.collapsed;
}
ui.add_space(2.0);
egui::Grid::new("sc_dialog_grid").num_columns(2).spacing(vec2(10.0, 8.0)).show(ui, |ui| {
placement_row(&mut d, ui);
input_rows(&mut d, ui);
match &mut d.kind {
Kind::Sketch => {}
Kind::Extrude { distance, direction, operation, auto_op, distance2, taper, start, all } => {
row_label(ui, "Start");
let mut st = usize::from(!start.is_empty());
combo(ui, "ex_start", &["Profile Plane", "Offset"], &mut st);
if st == 1 && start.is_empty() {
*start = "5 mm".into();
} else if st == 0 {
start.clear();
}
ui.end_row();
if !start.is_empty() {
row_label(ui, "Offset");
enter |= field(ui, start);
ui.end_row();
}
row_label(ui, "Direction");
// One Side, Two Sides, Symmetric (a flip is a negative distance).
let order = [0usize, TWO_SIDES, 2];
let mut pos = order.iter().position(|o| o == direction).unwrap_or(0);
combo(ui, "ex_dir", &["One Side", "Two Sides", "Symmetric"], &mut pos);
*direction = order.get(pos).copied().unwrap_or(0);
ui.end_row();
row_label(ui, "Extent Type");
let mut ext = usize::from(*all);
combo(ui, "ex_ext", &["Distance", "All"], &mut ext);
*all = ext == 1;
ui.end_row();
if !*all {
row_label(ui, "Distance");
enter |= field(ui, distance);
ui.end_row();
if *direction == TWO_SIDES {
row_label(ui, "Distance 2");
enter |= field(ui, distance2);
ui.end_row();
}
}
if *direction < 2 {
row_label(ui, "Taper Angle");
enter |= field(ui, taper);
ui.end_row();
}
row_label(ui, "Operation");
let before = *operation;
combo(ui, "ex_op", &OP_LABELS, operation);
// A choice made by hand sticks.
if *operation != before {
*auto_op = false;
}
ui.label(RichText::new(heading.as_str()).strong().size(12.5));
crate::frame::right_aligned(ui, total, 8.0, |ui| ui.label(RichText::new("»").color(t.text_dim)));
});
if d.collapsed {
return;
}
ui.add_space(2.0);
egui::Grid::new("sc_dialog_grid").num_columns(2).spacing(vec2(10.0, 8.0)).show(ui, |ui| {
placement_row(&mut d, ui);
input_rows(&mut d, ui);
match &mut d.kind {
Kind::Sketch => {}
Kind::Extrude { distance, direction, operation, auto_op, distance2, taper, start, all } => {
row_label(ui, "Start");
let mut st = usize::from(!start.is_empty());
combo(ui, "ex_start", &["Profile Plane", "Offset"], &mut st);
if st == 1 && start.is_empty() {
*start = "5 mm".into();
} else if st == 0 {
start.clear();
}
ui.end_row();
if !start.is_empty() {
row_label(ui, "Offset");
enter |= field(ui, start);
ui.end_row();
}
Kind::Revolve { angle, operation } => {
row_label(ui, "Angle");
enter |= field(ui, angle);
ui.end_row();
row_label(ui, "Operation");
combo(ui, "rv_op", &OP_LABELS, operation);
ui.end_row();
}
Kind::Fillet { radius, chamfer, chain } => {
row_label(ui, if *chamfer { "Distance" } else { "Radius" });
enter |= field(ui, radius);
ui.end_row();
row_label(ui, "Tangent Chain");
ui.checkbox(chain, "");
ui.end_row();
}
Kind::Shell { thickness } => {
row_label(ui, "Inside Thickness");
enter |= field(ui, thickness);
ui.end_row();
}
Kind::Draft { angle } => {
row_label(ui, "Angle");
enter |= field(ui, angle);
ui.end_row();
}
Kind::Mirror => {
row_label(ui, "Operation");
let mut op = usize::from(d.extra.get("combine").and_then(Value::as_bool).unwrap_or(false));
combo(ui, "mi_op", &["New Body", "Join"], &mut op);
d.extra.insert("combine".into(), json!(op == 1));
ui.end_row();
}
Kind::Measure { result, .. } => measure_rows(ui, result.as_ref()),
Kind::Preferences => preferences_rows(app, ui),
Kind::OffsetPlane { offset } => {
row_label(ui, "Distance");
enter |= field(ui, offset);
ui.end_row();
}
Kind::AnglePlane { angle } => {
row_label(ui, "Angle");
enter |= field(ui, angle);
ui.end_row();
}
Kind::Split | Kind::ReplaceFace | Kind::Remove => {}
Kind::Rib { thickness, depth, flip, .. } => {
row_label(ui, "Thickness");
enter |= field(ui, thickness);
ui.end_row();
row_label(ui, "Depth");
let r = ui.add(egui::TextEdit::singleline(depth).hint_text("to the next face"));
crate::params_dialog::complete(ui, &r, depth);
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
row_label(ui, "Flip");
ui.checkbox(flip, "");
ui.end_row();
}
Kind::Emboss { depth, deboss } => {
row_label(ui, "Depth");
enter |= field(ui, depth);
ui.end_row();
row_label(ui, "Type");
let mut i = usize::from(*deboss);
combo(ui, "emb_mode", &["Emboss", "Deboss"], &mut i);
*deboss = i == 1;
ui.end_row();
}
Kind::Align { flip } => {
row_label(ui, "Flip");
ui.checkbox(flip, "");
ui.end_row();
}
Kind::PathPattern { count, spacing } => {
for (l, v) in [("Quantity", count), ("Spacing", spacing)] {
row_label(ui, l);
enter |= field(ui, v);
ui.end_row();
}
}
Kind::Pipe { diameter, wall } => {
row_label(ui, "Diameter");
enter |= field(ui, diameter);
ui.end_row();
row_label(ui, "Wall");
let r = ui.add(egui::TextEdit::singleline(wall).hint_text("solid"));
crate::params_dialog::complete(ui, &r, wall);
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
}
Kind::Stock { margin } => {
row_label(ui, "Margin");
enter |= field(ui, margin);
ui.end_row();
}
Kind::Scale { factor } => {
row_label(ui, "Scale factor");
enter |= field(ui, factor);
ui.end_row();
}
Kind::OffsetFaces { distance } => {
row_label(ui, "Direction");
// One Side, Two Sides, Symmetric (a flip is a negative distance).
let order = [0usize, TWO_SIDES, 2];
let mut pos = order.iter().position(|o| o == direction).unwrap_or(0);
combo(ui, "ex_dir", &["One Side", "Two Sides", "Symmetric"], &mut pos);
*direction = order.get(pos).copied().unwrap_or(0);
ui.end_row();
row_label(ui, "Extent Type");
let mut ext = usize::from(*all);
combo(ui, "ex_ext", &["Distance", "All"], &mut ext);
*all = ext == 1;
ui.end_row();
if !*all {
row_label(ui, "Distance");
enter |= field(ui, distance);
ui.end_row();
}
Kind::Thread { designation, length } => {
// Fusion's order: Full Length, then the size; a length only when not full.
row_label(ui, "Full Length");
let mut full = length.trim().is_empty();
if ui.checkbox(&mut full, "").changed() {
*length = if full { String::new() } else { "10 mm".into() };
}
ui.end_row();
row_label(ui, "Size");
let r = ui.add(egui::TextEdit::singleline(designation).hint_text("fits the face, e.g. M8"));
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
if !full {
row_label(ui, "Length");
enter |= field(ui, length);
if *direction == TWO_SIDES {
row_label(ui, "Distance 2");
enter |= field(ui, distance2);
ui.end_row();
}
}
Kind::Material { index } => {
let names: Vec<&str> = solvecraft_engine::doc::MATERIALS.iter().map(|(n, _)| *n).collect();
row_label(ui, "Material");
combo(ui, "mat", &names, index);
if *direction < 2 {
row_label(ui, "Taper Angle");
enter |= field(ui, taper);
ui.end_row();
}
Kind::Section { offset, flip } => {
row_label(ui, "Distance");
enter |= field(ui, offset);
ui.end_row();
row_label(ui, "Flip");
ui.checkbox(flip, "");
ui.end_row();
row_label(ui, "Operation");
let before = *operation;
combo(ui, "ex_op", &OP_LABELS, operation);
// A choice made by hand sticks.
if *operation != before {
*auto_op = false;
}
Kind::PatternRect { count, spacing, count2, spacing2 } => {
for (l, v) in [("Quantity", count), ("Spacing", spacing), ("Quantity 2", count2), ("Spacing 2", spacing2)] {
row_label(ui, l);
enter |= field(ui, v);
ui.end_row();
}
}
Kind::PatternCirc { count, angle } => {
for (l, v) in [("Quantity", count), ("Total angle", angle)] {
row_label(ui, l);
enter |= field(ui, v);
ui.end_row();
}
}
Kind::Loft { operation } => {
row_label(ui, "Operation");
combo(ui, "lf_op", &OP_LABELS, operation);
ui.end_row();
}
Kind::Sweep { operation } => {
row_label(ui, "Operation");
combo(ui, "sw_op", &OP_LABELS, operation);
ui.end_row();
}
k @ Kind::Move { .. } => enter |= crate::dialogs_move::rows(ui, k),
Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } => {
row_label(ui, "Extents");
let mut ext = usize::from(opts.all);
combo(ui, "hole_ext", &["Distance", "All"], &mut ext);
opts.all = ext == 1;
ui.end_row();
row_label(ui, "Hole Type");
combo(ui, "hole_kind", &HOLE_LABELS, kind);
ui.end_row();
row_label(ui, "Hole Tap Type");
let mut tap = usize::from(opts.tapped);
combo(ui, "hole_tap", &["Simple", "Tapped"], &mut tap);
opts.tapped = tap == 1;
ui.end_row();
if opts.tapped {
row_label(ui, "Size");
let r = ui.add(egui::TextEdit::singleline(&mut opts.thread).hint_text("M5"));
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
}
if *kind == 0 {
row_label(ui, "Drill Point");
let mut tip = usize::from(opts.angled);
combo(ui, "hole_tip", &["Flat", "Angle"], &mut tip);
opts.angled = tip == 1;
ui.end_row();
}
if !opts.all {
row_label(ui, "Depth");
enter |= field(ui, depth);
ui.end_row();
}
if *kind == 0 && opts.angled {
row_label(ui, "Tip Angle");
enter |= field(ui, &mut opts.tip_angle);
ui.end_row();
}
row_label(ui, "Diameter");
enter |= field(ui, diameter);
ui.end_row();
if *kind == 1 {
row_label(ui, "Counterbore Ø");
enter |= field(ui, cb_diameter);
ui.end_row();
row_label(ui, "Counterbore depth");
enter |= field(ui, cb_depth);
ui.end_row();
}
if *kind == 2 {
row_label(ui, "Countersink Ø");
enter |= field(ui, cs_diameter);
ui.end_row();
row_label(ui, "Countersink angle");
enter |= field(ui, cs_angle);
ui.end_row();
}
}
Kind::Primitive { fields, operation, .. } => {
for (k, v) in fields.iter_mut() {
row_label(ui, k);
enter |= field(ui, v);
ui.end_row();
}
row_label(ui, "Operation");
combo(ui, "pr_op", &OP_LABELS, operation);
ui.end_row();
}
Kind::Combine { operation, keep_tools } => {
row_label(ui, "Operation");
let mut op = operation.saturating_sub(1);
combo(ui, "cb_op", &OP_LABELS[1..], &mut op);
*operation = op + 1;
ui.end_row();
row_label(ui, "Keep Tools");
ui.checkbox(keep_tools, "");
ui.end_row();
}
Kind::Params { new_name, new_expr } => params_table(app, ui, new_name, new_expr),
Kind::EditParam { name, expr } => {
ui.label(name.as_str());
let r = ui.text_edit_singleline(expr);
crate::params_dialog::complete(ui, &r, expr);
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
ok = true;
}
r.request_focus();
ui.end_row();
}
Kind::Rename { name, .. } => {
row_label(ui, "Name");
let r = ui.text_edit_singleline(name);
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
ok = true;
}
r.request_focus();
ui.end_row();
}
Kind::Assembly(k) => enter |= crate::dialogs_assembly::rows(app, ui, k, &mut d.inputs, &mut d.active),
Kind::Sheet(k) => enter |= crate::dialogs_sheet::rows(app, ui, k, &mut d.inputs),
Kind::Plastic(k) => enter |= crate::dialogs_plastic::rows(app, ui, k, &d.inputs),
Kind::Appearance(k) => enter |= crate::dialogs_appearance::rows(app, ui, k, &mut d.inputs),
Kind::Motion(k) => enter |= crate::dialogs_motion::rows(app, ui, k, &mut d.inputs),
Kind::Part(k) => enter |= crate::dialogs_parts::rows(app, ui, k, &d.inputs),
Kind::ConfirmDelete { with, fail, .. } => {
if !with.is_empty() {
row_label(ui, "Also deletes");
ui.label(with.join(", "));
ui.end_row();
}
if !fail.is_empty() {
row_label(ui, "Will fail");
ui.label(RichText::new(fail.join(", ")).color(t.warning));
ui.end_row();
}
}
}
if let Some(e) = d.error.as_ref().or(app.preview.error.as_ref()) {
row_label(ui, "");
ui.add(egui::Label::new(RichText::new(e.as_str()).color(t.error)).wrap());
ui.end_row();
}
});
ui.add_space(6.0);
// Footer: an info glyph at the left, OK and Cancel at the right.
ui.horizontal(|ui| {
let (r, resp) = ui.allocate_exact_size(vec2(16.0, 16.0), egui::Sense::hover());
ui.painter().circle_stroke(r.center(), 7.0, Stroke::new(1.2, t.text_dim));
ui.painter().text(r.center(), egui::Align2::CENTER_CENTER, "i", egui::FontId::proportional(11.0), t.text_dim);
resp.on_hover_text(heading.as_str());
// The spinner's place is always taken: a widget that comes and goes would shift
// the automatic ids of OK and Cancel, and a click pressed in one frame and
// released in the next would be lost (an animating preview toggles it often).
let (sr, sresp) = ui.allocate_exact_size(vec2(14.0, 14.0), egui::Sense::hover());
if app.preview.busy {
egui::Spinner::new().size(14.0).paint_at(ui, sr);
sresp.on_hover_text("Updating the preview");
Kind::Revolve { angle, operation } => {
row_label(ui, "Angle");
enter |= field(ui, angle);
ui.end_row();
row_label(ui, "Operation");
combo(ui, "rv_op", &OP_LABELS, operation);
ui.end_row();
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let close = if matches!(d.kind, Kind::Params { .. } | Kind::Measure { .. } | Kind::Preferences) { "Close" } else { "Cancel" };
if ui.add(egui::Button::new(close).min_size(vec2(64.0, 24.0))).clicked() {
cancel = true;
Kind::Fillet { radius, chamfer, chain } => {
row_label(ui, if *chamfer { "Distance" } else { "Radius" });
enter |= field(ui, radius);
ui.end_row();
row_label(ui, "Tangent Chain");
ui.checkbox(chain, "");
ui.end_row();
}
Kind::Shell { thickness } => {
row_label(ui, "Inside Thickness");
enter |= field(ui, thickness);
ui.end_row();
}
Kind::Draft { angle } => {
row_label(ui, "Angle");
enter |= field(ui, angle);
ui.end_row();
}
Kind::Mirror => {
row_label(ui, "Operation");
let mut op = usize::from(d.extra.get("combine").and_then(Value::as_bool).unwrap_or(false));
combo(ui, "mi_op", &["New Body", "Join"], &mut op);
d.extra.insert("combine".into(), json!(op == 1));
ui.end_row();
}
Kind::Measure { result, .. } => measure_rows(ui, result.as_ref()),
Kind::Preferences => preferences_rows(app, ui),
Kind::OffsetPlane { offset } => {
row_label(ui, "Distance");
enter |= field(ui, offset);
ui.end_row();
}
Kind::AnglePlane { angle } => {
row_label(ui, "Angle");
enter |= field(ui, angle);
ui.end_row();
}
Kind::Split | Kind::ReplaceFace | Kind::Remove => {}
Kind::Rib { thickness, depth, flip, .. } => {
row_label(ui, "Thickness");
enter |= field(ui, thickness);
ui.end_row();
row_label(ui, "Depth");
let r = ui.add(egui::TextEdit::singleline(depth).hint_text("to the next face"));
crate::params_dialog::complete(ui, &r, depth);
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
row_label(ui, "Flip");
ui.checkbox(flip, "");
ui.end_row();
}
Kind::Emboss { depth, deboss } => {
row_label(ui, "Depth");
enter |= field(ui, depth);
ui.end_row();
row_label(ui, "Type");
let mut i = usize::from(*deboss);
combo(ui, "emb_mode", &["Emboss", "Deboss"], &mut i);
*deboss = i == 1;
ui.end_row();
}
Kind::Align { flip } => {
row_label(ui, "Flip");
ui.checkbox(flip, "");
ui.end_row();
}
Kind::PathPattern { count, spacing } => {
for (l, v) in [("Quantity", count), ("Spacing", spacing)] {
row_label(ui, l);
enter |= field(ui, v);
ui.end_row();
}
if has_ok {
let label = if matches!(d.kind, Kind::ConfirmDelete { .. }) { "Delete" } else { "OK" };
let b = egui::Button::new(RichText::new(label).color(if valid { Color32::WHITE } else { t.text_dim }))
.fill(if valid { t.accent } else { Color32::TRANSPARENT })
.min_size(vec2(64.0, 24.0));
if ui.add_enabled(valid, b).clicked() {
ok = true;
}
}
Kind::Pipe { diameter, wall } => {
row_label(ui, "Diameter");
enter |= field(ui, diameter);
ui.end_row();
row_label(ui, "Wall");
let r = ui.add(egui::TextEdit::singleline(wall).hint_text("solid"));
crate::params_dialog::complete(ui, &r, wall);
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
}
Kind::Stock { margin } => {
row_label(ui, "Margin");
enter |= field(ui, margin);
ui.end_row();
}
Kind::Scale { factor } => {
row_label(ui, "Scale factor");
enter |= field(ui, factor);
ui.end_row();
}
Kind::OffsetFaces { distance } => {
row_label(ui, "Distance");
enter |= field(ui, distance);
ui.end_row();
}
Kind::Thread { designation, length } => {
// Fusion's order: Full Length, then the size; a length only when not full.
row_label(ui, "Full Length");
let mut full = length.trim().is_empty();
if ui.checkbox(&mut full, "").changed() {
*length = if full { String::new() } else { "10 mm".into() };
}
});
ui.end_row();
row_label(ui, "Size");
let r = ui.add(egui::TextEdit::singleline(designation).hint_text("fits the face, e.g. M8"));
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
if !full {
row_label(ui, "Length");
enter |= field(ui, length);
ui.end_row();
}
}
Kind::Material { index } => {
let names: Vec<&str> = solvecraft_engine::doc::MATERIALS.iter().map(|(n, _)| *n).collect();
row_label(ui, "Material");
combo(ui, "mat", &names, index);
ui.end_row();
}
Kind::Section { offset, flip } => {
row_label(ui, "Distance");
enter |= field(ui, offset);
ui.end_row();
row_label(ui, "Flip");
ui.checkbox(flip, "");
ui.end_row();
}
Kind::PatternRect { count, spacing, count2, spacing2 } => {
for (l, v) in [("Quantity", count), ("Spacing", spacing), ("Quantity 2", count2), ("Spacing 2", spacing2)] {
row_label(ui, l);
enter |= field(ui, v);
ui.end_row();
}
}
Kind::PatternCirc { count, angle } => {
for (l, v) in [("Quantity", count), ("Total angle", angle)] {
row_label(ui, l);
enter |= field(ui, v);
ui.end_row();
}
}
Kind::Loft { operation } => {
row_label(ui, "Operation");
combo(ui, "lf_op", &OP_LABELS, operation);
ui.end_row();
}
Kind::Sweep { operation } => {
row_label(ui, "Operation");
combo(ui, "sw_op", &OP_LABELS, operation);
ui.end_row();
}
k @ Kind::Move { .. } => enter |= crate::dialogs_move::rows(ui, k),
Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } => {
row_label(ui, "Extents");
let mut ext = usize::from(opts.all);
combo(ui, "hole_ext", &["Distance", "All"], &mut ext);
opts.all = ext == 1;
ui.end_row();
row_label(ui, "Hole Type");
combo(ui, "hole_kind", &HOLE_LABELS, kind);
ui.end_row();
row_label(ui, "Hole Tap Type");
let mut tap = usize::from(opts.tapped);
combo(ui, "hole_tap", &["Simple", "Tapped"], &mut tap);
opts.tapped = tap == 1;
ui.end_row();
if opts.tapped {
row_label(ui, "Size");
let r = ui.add(egui::TextEdit::singleline(&mut opts.thread).hint_text("M5"));
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
ui.end_row();
}
if *kind == 0 {
row_label(ui, "Drill Point");
let mut tip = usize::from(opts.angled);
combo(ui, "hole_tip", &["Flat", "Angle"], &mut tip);
opts.angled = tip == 1;
ui.end_row();
}
if !opts.all {
row_label(ui, "Depth");
enter |= field(ui, depth);
ui.end_row();
}
if *kind == 0 && opts.angled {
row_label(ui, "Tip Angle");
enter |= field(ui, &mut opts.tip_angle);
ui.end_row();
}
row_label(ui, "Diameter");
enter |= field(ui, diameter);
ui.end_row();
if *kind == 1 {
row_label(ui, "Counterbore Ø");
enter |= field(ui, cb_diameter);
ui.end_row();
row_label(ui, "Counterbore depth");
enter |= field(ui, cb_depth);
ui.end_row();
}
if *kind == 2 {
row_label(ui, "Countersink Ø");
enter |= field(ui, cs_diameter);
ui.end_row();
row_label(ui, "Countersink angle");
enter |= field(ui, cs_angle);
ui.end_row();
}
}
Kind::Primitive { fields, operation, .. } => {
for (k, v) in fields.iter_mut() {
row_label(ui, k);
enter |= field(ui, v);
ui.end_row();
}
row_label(ui, "Operation");
combo(ui, "pr_op", &OP_LABELS, operation);
ui.end_row();
}
Kind::Combine { operation, keep_tools } => {
row_label(ui, "Operation");
let mut op = operation.saturating_sub(1);
combo(ui, "cb_op", &OP_LABELS[1..], &mut op);
*operation = op + 1;
ui.end_row();
row_label(ui, "Keep Tools");
ui.checkbox(keep_tools, "");
ui.end_row();
}
Kind::Params { new_name, new_expr } => params_table(app, ui, new_name, new_expr),
Kind::EditParam { name, expr } => {
ui.label(name.as_str());
let r = ui.text_edit_singleline(expr);
crate::params_dialog::complete(ui, &r, expr);
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
ok = true;
}
r.request_focus();
ui.end_row();
}
Kind::Rename { name, .. } => {
row_label(ui, "Name");
let r = ui.text_edit_singleline(name);
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
ok = true;
}
r.request_focus();
ui.end_row();
}
Kind::Assembly(k) => enter |= crate::dialogs_assembly::rows(app, ui, k, &mut d.inputs, &mut d.active),
Kind::Sheet(k) => enter |= crate::dialogs_sheet::rows(app, ui, k, &mut d.inputs),
Kind::Plastic(k) => enter |= crate::dialogs_plastic::rows(app, ui, k, &d.inputs),
Kind::Appearance(k) => enter |= crate::dialogs_appearance::rows(app, ui, k, &mut d.inputs),
Kind::Motion(k) => enter |= crate::dialogs_motion::rows(app, ui, k, &mut d.inputs),
Kind::Part(k) => enter |= crate::dialogs_parts::rows(app, ui, k, &d.inputs),
Kind::ConfirmDelete { with, fail, .. } => {
if !with.is_empty() {
row_label(ui, "Also deletes");
ui.label(with.join(", "));
ui.end_row();
}
if !fail.is_empty() {
row_label(ui, "Will fail");
ui.label(RichText::new(fail.join(", ")).color(t.warning));
ui.end_row();
}
}
}
if let Some(e) = d.error.as_ref().or(app.preview.error.as_ref()) {
row_label(ui, "");
ui.add(egui::Label::new(RichText::new(e.as_str()).color(t.error)).wrap());
ui.end_row();
}
});
ui.add_space(6.0);
// Footer: an info glyph at the left, OK and Cancel at the right.
ui.horizontal(|ui| {
let (r, resp) = ui.allocate_exact_size(vec2(16.0, 16.0), egui::Sense::hover());
ui.painter().circle_stroke(r.center(), 7.0, Stroke::new(1.2, t.text_dim));
ui.painter().text(r.center(), egui::Align2::CENTER_CENTER, "i", egui::FontId::proportional(11.0), t.text_dim);
resp.on_hover_text(heading.as_str());
// The spinner's place is always taken: a widget that comes and goes would shift
// the automatic ids of OK and Cancel, and a click pressed in one frame and
// released in the next would be lost (an animating preview toggles it often).
let (sr, sresp) = ui.allocate_exact_size(vec2(14.0, 14.0), egui::Sense::hover());
if app.preview.busy {
egui::Spinner::new().size(14.0).paint_at(ui, sr);
sresp.on_hover_text("Updating the preview");
}
let buttons = if has_ok { 64.0 * 2.0 + ui.spacing().item_spacing.x } else { 64.0 };
crate::frame::right_aligned(ui, total, buttons, |ui| {
if has_ok {
let label = if matches!(d.kind, Kind::ConfirmDelete { .. }) { "Delete" } else { "OK" };
let b = egui::Button::new(RichText::new(label).color(if valid { Color32::WHITE } else { t.text_dim }))
.fill(if valid { t.accent } else { Color32::TRANSPARENT })
.min_size(vec2(64.0, 24.0));
if ui.add_enabled(valid, b).clicked() {
ok = true;
}
}
let close = if matches!(d.kind, Kind::Params { .. } | Kind::Measure { .. } | Kind::Preferences) { "Close" } else { "Cancel" };
if ui.add(egui::Button::new(close).min_size(vec2(64.0, 24.0))).clicked() {
cancel = true;
}
});
});
crate::frame::remember_width(ui, wkey);
});
// Keyboard: Enter applies (from a value field, or with nothing focused), Esc cancels.
let (enter_free, esc) = ctx.input(|i| (i.key_pressed(egui::Key::Enter), i.key_pressed(egui::Key::Escape)));
let nothing_focused = ctx.memory(|m| m.focused().is_none());