Files
cadcraft/crates/ui-egui/src/dialogs.rs
T
5183f63bc5 Hatch and Gradient dialog (create and edit) (#753)
* HATCH/GRADIENT: points and objects in one call; GRADIENT name and centered

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

* HATCHEDIT: gradient parameter (make or change a gradient fill)

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

* HATCHPICK: pick hatch boundaries, an origin or hatches for the dialog

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

* Hatch dialog: state, defaults and the OK command call

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

* Hatch dialog: pick points, objects, origin or hatch and come back

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

* Hatch and Gradient dialog window (pattern swatches, gradient, origin, boundaries, options)

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

* HATCH, GRADIENT and HATCHEDIT open the hatch dialog; double-click a hatch to edit; HPDLGMODE

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

* Hatch dialog: keep the window to its content (OK and Cancel stayed off screen); double-click test

A vertical separator between the two columns took the height available, so the window grew
every frame until its buttons left the screen. Found in the running app.

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

* Docs: hatch dialogs in gaps, target-app parity and UI parity

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

* Hatch dialog review fixes

- Double-clicking a hatch at its centre opened the dialog and also started a grip edit.
- Edit mode no longer clamps an out-of-range scale or angle into a change nobody made.
- HPORIGINMODE outside 1-5 means the current origin, as HATCH reads it.
- Picks made in one space are dropped when the dialog comes back in another (layout tab).
- A gradient hatch's Pattern tab offers the current pattern (HPNAME), not SOLID.
- Ukrainian: "Default to boundary extents" said "outside the boundary".

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

---------

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 04:34:09 -04:00

561 lines
25 KiB
Rust

//! Dialogs: Drafting Settings, Drawing Units, About, command reference, blocks; dispatches the
//! Layer Properties Manager ([`crate::layers`]), Quick Select ([`crate::quick`]), Parameters
//! Manager ([`crate::parametric`]) and the style and settings managers ([`crate::managers`]).
use egui::{RichText, vec2};
use serde_json::json;
use crate::CadApp;
use crate::theme::Tokens;
pub fn show(app: &mut CadApp, ctx: &egui::Context) {
mtext_editor(app, ctx);
crate::quick::quick_properties(app, ctx);
let Some(d) = app.ui.dialog.clone() else { return };
let mut open = true;
match d.as_str() {
"spacemouse" => crate::spacemouse::dialog(app, ctx, &mut open),
"language" => language(app, ctx, &mut open),
"layers" => crate::layers::dialog(app, ctx, &mut open),
"qselect" => crate::quick::qselect_dialog(app, ctx, &mut open),
"parameters" => crate::parametric::parameters_dialog(app, ctx, &mut open),
"dsettings" => dsettings(app, ctx, &mut open),
"units" => units(app, ctx, &mut open),
"hatch" => crate::hatchdlg::dialog(app, ctx, &mut open),
"about" => about(ctx, &mut open),
"preview" => crate::exchange::dialog(app, ctx, &mut open),
"commands" => commands(app, ctx, &mut open),
"blocks" => blocks(app, ctx, &mut open),
"stylesmanager" => crate::plotstyles::dialog(app, ctx, &mut open),
o if o == "options" || o.starts_with("options:") => crate::options::dialog(app, ctx, o, &mut open),
other => crate::managers::dialog(app, ctx, other, &mut open),
}
// A dialog that closed by opening another (Plot Preview back to Plot) keeps the new one.
if !open && app.ui.dialog.as_deref() == Some(d.as_str()) {
app.ui.dialog = None;
}
}
fn dsettings(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
let settings = app.session.drafting_settings();
let (mut snapunit, mut gridunit) = (settings.snapunit, settings.gridunit);
egui::Window::new(crate::tl!("Drafting Settings")).id(egui::Id::new("Drafting Settings")).open(open).resizable(false).show(ctx, |ui| {
let s = &mut app.session.settings;
ui.heading(crate::tl!("Snap and Grid"));
ui.checkbox(&mut s.snapmode, crate::tl!("Snap On (F9)"));
// SNAPSTYL: the isometric snap and grid (ISODRAFT).
ui.horizontal(|ui| {
ui.radio_value(&mut s.isodraft, false, crate::tl!("Rectangular snap"));
ui.radio_value(&mut s.isodraft, true, crate::tl!("Isometric snap"));
});
ui.horizontal(|ui| {
ui.label(crate::tl!("Snap X spacing"));
ui.add(egui::DragValue::new(&mut snapunit.x).speed(0.05).range(0.0001..=1e6).clamp_existing_to_range(false));
ui.label(crate::tl!("Y"));
ui.add(egui::DragValue::new(&mut snapunit.y).speed(0.05).range(0.0001..=1e6).clamp_existing_to_range(false));
});
// SNAPTYPE and POLARDIST.
ui.horizontal(|ui| {
ui.label(crate::tl!("Snap type"));
ui.radio_value(&mut s.polarsnap, false, crate::tl!("Grid snap"));
ui.radio_value(&mut s.polarsnap, true, crate::tl!("PolarSnap"));
});
ui.add_enabled_ui(s.polarsnap, |ui| {
ui.horizontal(|ui| {
ui.label(crate::tl!("Polar distance"));
ui.add(egui::DragValue::new(&mut s.polardist).speed(0.05).range(0.0..=1e6));
});
});
ui.checkbox(&mut s.gridmode, crate::tl!("Grid On (F7)"));
ui.horizontal(|ui| {
ui.label(crate::tl!("Grid spacing"));
ui.add(egui::DragValue::new(&mut gridunit.x).speed(0.05).range(0.0..=1e6).clamp_existing_to_range(false));
ui.label(crate::tl!("Major line every"));
ui.add(egui::DragValue::new(&mut s.gridmajor).range(1..=100));
});
ui.separator();
ui.heading(crate::tl!("Polar Tracking"));
ui.checkbox(&mut s.polarmode, crate::tl!("Polar Tracking On (F10)"));
let mut deg = s.polarang.to_degrees();
ui.horizontal(|ui| {
ui.label(crate::tl!("Increment angle"));
egui::ComboBox::from_id_salt("polarang").selected_text(format!("{deg}")).show_ui(ui, |ui| {
for a in [90.0, 45.0, 30.0, 22.5, 18.0, 15.0, 10.0, 5.0] {
ui.selectable_value(&mut deg, a, format!("{a}"));
}
});
});
s.polarang = deg.to_radians();
tracking_options(ui, s);
ui.separator();
ui.heading(crate::tl!("Object Snap"));
let mut on = s.osmode & cadcraft_engine::snap::mode::OFF == 0;
if ui.checkbox(&mut on, crate::tl!("Object Snap On (F3)")).changed() {
if on {
s.osmode &= !cadcraft_engine::snap::mode::OFF;
} else {
s.osmode |= cadcraft_engine::snap::mode::OFF;
}
}
egui::Grid::new("osnap_grid").num_columns(2).show(ui, |ui| {
for (i, (bit, name)) in cadcraft_engine::snap::mode::ALL.iter().enumerate() {
let mut v = s.osmode & bit != 0;
if ui.checkbox(&mut v, crate::i18n::t(name)).changed() {
if v {
s.osmode |= bit;
} else {
s.osmode &= !bit;
}
}
if i % 2 == 1 {
ui.end_row();
}
}
});
ui.separator();
ui.checkbox(&mut s.dynmode, crate::tl!("Enable Dynamic Input (F12)"));
// Pointer input format for second and next points (DYNPIFORMAT, DYNPICOORDS).
ui.indent("dynpi", |ui| {
ui.add_enabled_ui(s.dynmode, |ui| {
ui.label(crate::tl!("Second and next points:"));
ui.horizontal(|ui| {
ui.radio_value(&mut s.dynpi_cartesian, false, crate::tl!("Polar (distance < angle)"));
ui.radio_value(&mut s.dynpi_cartesian, true, crate::tl!("Cartesian (x, y)"));
});
ui.horizontal(|ui| {
ui.radio_value(&mut s.dynpi_absolute, false, crate::tl!("Relative"));
ui.radio_value(&mut s.dynpi_absolute, true, crate::tl!("Absolute"));
});
// DYNPROMPT: the prompt and its options (Down Arrow) beside the cursor.
ui.checkbox(&mut s.dynprompt, crate::tl!("Show command prompting near the crosshairs"));
});
});
ui.checkbox(&mut s.orthomode, crate::tl!("Ortho (F8)"));
});
// Drawing edits go through the same command as the CLI, including dirty state and undo.
for (name, before, after) in [("SNAPUNIT", settings.snapunit, snapunit), ("GRIDUNIT", settings.gridunit, gridunit)] {
if before != after {
let _ = app.run("setvar", json!({ "name": name, "value": [after.x, after.y] }));
}
}
}
/// The polar and object snap tracking options of Drafting Settings: additional angles
/// (POLARMODE bit 4, POLARADDANG), tracking on every polar angle (bit 2) and angles relative to
/// the last segment (bit 1).
fn tracking_options(ui: &mut egui::Ui, s: &mut cadcraft_engine::Settings) {
use cadcraft_engine::snap::tracking::polarmode;
/// A radio button for POLARMODE `bit` being `on`.
fn flag(ui: &mut egui::Ui, flags: &mut u32, bit: u32, on: bool, label: &str) {
if ui.radio((*flags & bit != 0) == on, label).clicked() {
*flags = if on { *flags | bit } else { *flags & !bit };
}
}
let mut additional = s.polar_flags & polarmode::ADDITIONAL != 0;
ui.horizontal(|ui| {
if ui.checkbox(&mut additional, crate::tl!("Additional angles")).changed() {
s.polar_flags ^= polarmode::ADDITIONAL;
}
// The list as typed (degrees separated by `;`), kept while it doesn't parse yet.
let id = ui.id().with("polaraddang");
let shown = cadcraft_engine::sysvars::format_angle_list(&s.polaraddang);
let mut text = ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| shown.clone());
let resp = ui.add_enabled(additional, egui::TextEdit::singleline(&mut text).desired_width(140.0));
if resp.changed()
&& let Some(angles) = cadcraft_engine::sysvars::parse_angle_list(&text)
{
s.polaraddang = angles;
}
if resp.has_focus() {
ui.data_mut(|d| d.insert_temp(id, text));
} else {
ui.data_mut(|d| d.remove::<String>(id));
}
});
ui.label(crate::tl!("Object Snap Tracking Settings"));
ui.horizontal(|ui| {
flag(ui, &mut s.polar_flags, polarmode::TRACK_POLAR, false, crate::tl!("Track orthogonally only"));
flag(ui, &mut s.polar_flags, polarmode::TRACK_POLAR, true, crate::tl!("Track using all polar angle settings"));
});
ui.label(crate::tl!("Polar angle measurement"));
ui.horizontal(|ui| {
flag(ui, &mut s.polar_flags, polarmode::RELATIVE, false, crate::tl!("Absolute"));
flag(ui, &mut s.polar_flags, polarmode::RELATIVE, true, crate::tl!("Relative to last segment"));
});
}
/// LUNITS values in the order the Type list shows them.
const LENGTH_TYPES: [(i64, &str); 5] = [(4, "Architectural"), (2, "Decimal"), (3, "Engineering"), (5, "Fractional"), (1, "Scientific")];
/// AUNITS values.
const ANGLE_TYPES: [(i64, &str); 5] = [(0, "Decimal Degrees"), (1, "Deg/Min/Sec"), (2, "Grads"), (3, "Radians"), (4, "Surveyor's Units")];
/// INSUNITS names, indexed by value (DXF Reference, header group code 70).
const INSERTION_UNITS: [&str; 25] = [
"Unitless",
"Inches",
"Feet",
"Miles",
"Millimeters",
"Centimeters",
"Meters",
"Kilometers",
"Microinches",
"Mils",
"Yards",
"Angstroms",
"Nanometers",
"Microns",
"Decimeters",
"Decameters",
"Hectometers",
"Gigameters",
"Astronomical Units",
"Light Years",
"Parsecs",
"US Survey Feet",
"US Survey Inch",
"US Survey Yard",
"US Survey Mile",
];
/// A combo box over `(value, label)` options.
fn pick(ui: &mut egui::Ui, salt: &str, value: &mut i64, options: &[(i64, String)]) {
let current = options.iter().find(|o| o.0 == *value).map(|o| o.1.clone()).unwrap_or_default();
egui::ComboBox::from_id_salt(salt).selected_text(current).width(180.0).show_ui(ui, |ui| {
for (k, label) in options {
ui.selectable_value(value, *k, label);
}
});
}
/// Drawing Units (UNITS): edits a copy of LUNITS/LUPREC/AUNITS/AUPREC/INSUNITS and applies it
/// through the `units` command on OK.
fn units(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
use cadcraft_engine::units::{format_angle, format_distance};
let id = egui::Id::new("units_dialog_values");
let mut v = ctx.data_mut(|d| d.get_temp::<[i64; 5]>(id)).unwrap_or_else(|| {
app.session
.doc()
.map(|d| {
let h = &d.header;
[h.i64("LUNITS", 2), h.i64("LUPREC", 4), h.i64("AUNITS", 0), h.i64("AUPREC", 0), h.i64("INSUNITS", 1)]
})
.unwrap_or([2, 4, 0, 0, 1])
});
let (mut apply, mut cancel) = (false, false);
egui::Window::new("Drawing Units").open(open).resizable(false).collapsible(false).show(ctx, |ui| {
let [lu, lp, au, ap, ins] = &mut v;
let types = |t: &[(i64, &str)]| t.iter().map(|(k, l)| (*k, l.to_string())).collect::<Vec<_>>();
egui::Grid::new("units_grid").num_columns(2).spacing(vec2(12.0, 6.0)).show(ui, |ui| {
ui.strong("Length");
ui.end_row();
ui.label("Type");
pick(ui, "units_lunits", lu, &types(&LENGTH_TYPES));
ui.end_row();
ui.label("Precision");
pick(ui, "units_luprec", lp, &(0..=8).map(|p| (p, format_distance(1.5, *lu, p))).collect::<Vec<_>>());
ui.end_row();
ui.strong("Angle");
ui.end_row();
ui.label("Type");
pick(ui, "units_aunits", au, &types(&ANGLE_TYPES));
ui.end_row();
ui.label("Precision");
pick(ui, "units_auprec", ap, &(0..=8).map(|p| (p, format_angle(45f64.to_radians(), *au, p))).collect::<Vec<_>>());
ui.end_row();
ui.strong("Insertion scale");
ui.end_row();
ui.label("Units to scale inserted content");
let names = INSERTION_UNITS.iter().enumerate().map(|(k, l)| (k as i64, l.to_string())).collect::<Vec<_>>();
pick(ui, "units_insunits", ins, &names);
ui.end_row();
ui.strong("Sample output");
ui.end_row();
ui.label("");
ui.label(format!("{},{},{}", format_distance(1.5, *lu, *lp), format_distance(2.0039, *lu, *lp), format_distance(0.0, *lu, *lp)));
ui.end_row();
ui.label("");
ui.label(format_angle(45f64.to_radians(), *au, *ap));
ui.end_row();
});
ui.separator();
ui.horizontal(|ui| {
apply = ui.button("OK").clicked();
cancel = ui.button("Cancel").clicked();
});
});
if apply {
let [lu, lp, au, ap, ins] = v;
let _ = app.run("units", json!({ "lunits": lu, "luprec": lp, "aunits": au, "auprec": ap, "insunits": ins }));
}
if apply || cancel || !*open {
ctx.data_mut(|d| d.remove::<[i64; 5]>(id));
*open = false;
} else {
ctx.data_mut(|d| d.insert_temp(id, v));
}
}
fn about(ctx: &egui::Context, open: &mut bool) {
let t = Tokens::get();
let tab_id = egui::Id::new("about_tab");
egui::Window::new(crate::tl!("About CADCraft"))
.id(egui::Id::new("About CADCraft"))
.open(open)
.default_size(vec2(640.0, 420.0))
.collapsible(false)
.show(ctx, |ui| {
let mut tab = ui.data_mut(|d| d.get_temp::<u8>(tab_id)).unwrap_or(0);
ui.horizontal(|ui| {
for (i, l) in ["About", "Contributors", "Models"].iter().enumerate() {
if ui.selectable_label(tab == i as u8, crate::i18n::t(l)).clicked() {
tab = i as u8;
}
}
});
ui.data_mut(|d| d.insert_temp(tab_id, tab));
ui.separator();
match tab {
1 => crate::credits::contributors_ui(ui),
2 => crate::credits::models_ui(ui),
_ => {
ui.heading(crate::tl!("CADCraft"));
ui.label(crate::tf!("Version {version}", version = env!("CARGO_PKG_VERSION")));
ui.label(crate::tl!("Computer-aided design and drafting: an open-source, clean-room CAD application written in pure Rust."));
ui.add_space(6.0);
ui.label(RichText::new(crate::tl!("One of the Crafting Apps by the ArtCraft team and community.")).color(t.text_dim));
ui.hyperlink_to("getartcraft.com/apps/cadcraft", "https://getartcraft.com/apps/cadcraft");
ui.hyperlink_to(crate::tl!("Join us on Discord"), "https://discord.gg/artcraft");
ui.add_space(6.0);
ui.label(RichText::new(crate::tl!("MIT OR Apache-2.0. Not affiliated with Autodesk, Inc.")).small().color(t.text_faint));
}
}
});
}
fn commands(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
let mut start = None;
egui::Window::new(crate::tl!("Command Reference")).id(egui::Id::new("Command Reference")).open(open).default_size(vec2(640.0, 480.0)).show(
ctx,
|ui| {
let id = ui.id().with("cmdfilter");
let mut filter = ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_default();
ui.horizontal(|ui| {
ui.label(crate::tl!("Filter"));
ui.text_edit_singleline(&mut filter);
});
ui.data_mut(|d| d.insert_temp(id, filter.clone()));
let f = filter.to_lowercase();
egui::ScrollArea::vertical().show(ui, |ui| {
egui::Grid::new("cmdref").striped(true).num_columns(3).show(ui, |ui| {
for c in cadcraft_engine::command_specs() {
if !f.is_empty()
&& !c.id.contains(&f)
&& !c.label.to_ascii_lowercase().contains(&f)
&& !crate::i18n::t(c.label).to_lowercase().contains(&f)
{
continue;
}
if ui.link(c.id.to_ascii_uppercase()).clicked() {
start = Some(c.id);
}
ui.label(crate::i18n::t(c.label));
ui.label(RichText::new(if c.aliases.is_empty() { String::new() } else { c.aliases.join(", ").to_ascii_uppercase() }).small());
ui.end_row();
}
});
});
},
);
if let Some(c) = start {
app.ui.dialog = None;
app.start(c);
}
}
fn blocks(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
let mut pick = None;
egui::Window::new(crate::tl!("Blocks")).id(egui::Id::new("Blocks")).open(open).default_size(vec2(320.0, 360.0)).show(ctx, |ui| {
let Ok(d) = app.session.doc() else { return };
let names: Vec<&String> = d.blocks.keys().filter(|k| !k.starts_with('*')).collect();
if names.is_empty() {
ui.label(crate::tl!("No blocks defined in this drawing."));
}
for n in names {
if ui.button(n).on_hover_text(crate::tl!("Insert this block")).clicked() {
pick = Some(n.clone());
}
}
});
if let Some(n) = pick {
insert_block(app, &n);
}
}
/// Start INSERT with `name` already given at its block name prompt, so the next prompt asks for
/// the insertion point.
fn insert_block(app: &mut CadApp, name: &str) {
app.start("insert");
if app.session.running.as_ref().is_none_or(|r| r.id != "insert") {
return;
}
if let Err(e) = app.session.input(cadcraft_engine::Input::Text(name.to_string())) {
app.session.echo(e.to_string());
}
}
/// The editor's lines as a list: a hanging indent of three text heights with a tab stop there
/// (`\pxi-3,l3,t3;`) and each line a bullet or its number and a tab.
fn as_list(buf: &str, numbered: bool) -> String {
let lines: Vec<String> =
buf.split('\n').enumerate().map(|(i, l)| if numbered { format!("{}.\t{l}", i + 1) } else { format!("•\t{l}") }).collect();
format!("\\pxi-3,l3,t3;{}", lines.join("\n"))
}
/// The multiline text editor shown while MTEXT asks for its contents.
fn mtext_editor(app: &mut CadApp, ctx: &egui::Context) {
let active = app.session.running.as_ref().is_some_and(|r| r.id == "mtext")
&& app.session.current_prompt().is_some_and(|p| p.accept.text && !p.accept.point);
let id = egui::Id::new("mtext_editor_buffer");
if !active {
ctx.data_mut(|d| d.remove::<String>(id));
return;
}
let mut buf = ctx.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_default();
let mut submit = None;
let mut cancel = false;
egui::Window::new(crate::tl!("Text Editor"))
.id(egui::Id::new("Text Editor"))
.collapsible(false)
.resizable(true)
.default_size(vec2(460.0, 220.0))
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label(crate::tl!("Style: Standard"));
ui.separator();
let h = app.session.doc().map(|d| d.header.f64("TEXTSIZE", 0.2)).unwrap_or(0.2);
ui.label(crate::tf!("Height: {height}", height = format!("{h:.4}")));
ui.separator();
if ui.button(crate::tl!("B")).on_hover_text(crate::tl!("Bold")).clicked() {
buf.push_str("{\\fArial|b1;}");
}
if ui.button("⅟").on_hover_text(crate::tl!("Stack (type 1/2 then select)")).clicked() {
buf.push_str("\\S1/2;");
}
if ui.button("°").on_hover_text(crate::tl!("Degree")).clicked() {
buf.push_str("%%d");
}
if ui.button("±").on_hover_text(crate::tl!("Plus/minus")).clicked() {
buf.push_str("%%p");
}
if ui.button("⌀").on_hover_text(crate::tl!("Diameter")).clicked() {
buf.push_str("%%c");
}
ui.separator();
if ui.button("⇥").on_hover_text(crate::tl!("Insert tab")).clicked() {
buf.push('\t');
}
if ui.button("•≡").on_hover_text(crate::tl!("Bulleted list")).clicked() {
buf = as_list(&buf, false);
}
if ui.button("1≡").on_hover_text(crate::tl!("Numbered list")).clicked() {
buf = as_list(&buf, true);
}
});
let r = ui.add(
egui::TextEdit::multiline(&mut buf)
.desired_rows(6)
.desired_width(f32::INFINITY)
.hint_text(crate::tl!("Type text; Enter starts a new paragraph")),
);
if !r.has_focus() && buf.is_empty() {
r.request_focus();
}
ui.horizontal(|ui| {
if ui.button(crate::tl!("OK")).clicked() || (r.has_focus() && ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Enter))) {
submit = Some(buf.replace('\n', "\\P"));
}
if ui.button(crate::tl!("Cancel")).clicked() {
cancel = true;
}
let finish = if ui.ctx().os().is_mac() { crate::tl!("⌘↩ to finish") } else { crate::tl!("Ctrl+Enter to finish") };
ui.label(RichText::new(finish).small());
});
});
ctx.data_mut(|d| d.insert_temp(id, buf));
if let Some(t) = submit {
ctx.data_mut(|d| d.remove::<String>(id));
// The editor holds the whole contents: one input, then Enter ends MTEXT's text entry.
if !t.is_empty() {
let _ = app.session.input(cadcraft_engine::Input::Text(t));
}
if app.session.running.as_ref().is_some_and(|r| r.id == "mtext") {
let _ = app.session.input(cadcraft_engine::Input::Enter);
}
} else if cancel {
ctx.data_mut(|d| d.remove::<String>(id));
app.session.cancel();
}
}
fn language(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(crate::tl!("Interface Language")).id(egui::Id::new("interface_language")).open(open).resizable(false).show(ctx, |ui| {
egui::ComboBox::from_id_salt("interface_language_choice").selected_text(app.ui.interface_language.name()).show_ui(ui, |ui| {
for preference in crate::i18n::Preference::ALL {
if ui.selectable_label(app.ui.interface_language == preference, preference.name()).clicked() {
let _ = app.run("ui.language", serde_json::json!({"lang": preference.code()}));
ui.ctx().request_repaint();
}
}
});
});
}
#[cfg(test)]
mod tests {
use cadcraft_engine::doc::EntityKind;
use cadcraft_engine::geom::Vec2;
use cadcraft_engine::{Input, Session};
use serde_json::json;
use crate::{CadApp, Services};
#[test]
fn picking_a_block_starts_insert_with_its_name() {
let mut app = CadApp::new(Session::new(), Services::default());
let c = app.session.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap();
let h = c["handle"].as_str().unwrap().to_string();
app.session.execute("block", &json!({"name": "Door 2", "base": [0, 0], "handles": [h], "keep": "delete"})).unwrap();
// A command already running is replaced, as when a command is typed.
app.start("line");
super::insert_block(&mut app, "Door 2");
assert!(app.session.running.as_ref().is_some_and(|r| r.id == "insert"));
assert_eq!(app.session.current_prompt().map(|p| p.message), Some("Specify insertion point".to_string()));
app.session.input(Input::Point(Vec2::new(5.0, 6.0))).unwrap();
// X scale, Y scale and rotation take their defaults.
for _ in 0..3 {
app.session.input(Input::Enter).unwrap();
}
assert!(app.session.running.is_none());
let d = app.session.doc().unwrap();
let mut ins = Vec::new();
for e in d.model.iter() {
if let EntityKind::Insert(i) = &e.kind {
ins.push(i.clone());
}
}
assert_eq!(ins.len(), 1);
assert_eq!(ins[0].block, "Door 2");
assert!((ins[0].insert.x - 5.0).abs() < 1e-9 && (ins[0].insert.y - 6.0).abs() < 1e-9);
}
#[test]
fn picking_a_missing_block_keeps_asking_for_a_name() {
let mut app = CadApp::new(Session::new(), Services::default());
super::insert_block(&mut app, "Nope");
assert!(app.session.running.as_ref().is_some_and(|r| r.id == "insert"));
assert_eq!(app.session.current_prompt().map(|p| p.message), Some("Enter block name".to_string()));
}
}