Merge branch 'stk-2371' into stack/s3

# Conflicts:
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/options.rs
#	docs/gaps.md
#	docs/ui-parity.md
This commit is contained in:
Tanish Manku
2026-11-06 13:33:05 -07:00
7 changed files with 513 additions and 13 deletions
+35 -7
View File
@@ -6,8 +6,12 @@
//! Pan/Zoom, Quick Select, Find, Properties and Options.
//! - **While a command runs** ([`command_entries`]): a quick right-click is Enter; holding the
//! button for [`HOLD_SECS`] opens Enter, Cancel, Recent Input, the prompt's options, Snap
//! Overrides and Pan/Zoom instead (AutoCAD's time-sensitive right-click). With that turned off
//! in OPTIONS (`timeSensitiveRightClick`), every right-click opens the menu.
//! Overrides and Pan/Zoom instead (AutoCAD's time-sensitive right-click; Options ▸ User
//! Preferences ▸ Right-Click Customization sets how long, `rightClickDuration`). With that turned
//! off (`timeSensitiveRightClick`), Command mode says what a right-click does
//! (`rightClickCommand`: the menu, Enter, or the menu only when the prompt has options).
//! `rightClickDefault` and `rightClickEdit` can make a right-click with no command running
//! repeat the last command instead of opening the menu.
//! - **Shift/Ctrl+right-click** ([`snap_entries`]): the object snap menu. Its items type the point
//! modifiers and snap overrides (`_from`, `_endp`, `_non`…) at the current point prompt.
//!
@@ -41,7 +45,8 @@ const SETTINGS: &[&str] = &["options"];
/// How many recent commands and inputs the menus list.
pub const RECENT: usize = 8;
/// How long the right button must be held while a command runs to open the command menu instead
/// of acting as Enter (AutoCAD's time-sensitive right-click; 250 ms there by default).
/// of acting as Enter, by default (AutoCAD's time-sensitive right-click; 250 ms there by
/// default). OPTIONS `rightClickDuration` changes it.
pub const HOLD_SECS: f64 = 0.25;
/// Item ids that are not commands: keys, typed text and snap menu items.
@@ -313,6 +318,19 @@ pub fn groups_ui(ui: &mut egui::Ui, groups: &[Vec<Entry>], clicked: &mut Option<
pub fn show(app: &mut CadApp, resp: &egui::Response) {
let mut clicked = None;
let snap = app.canvas.snap_menu;
// Right-Click Customization: with nothing selected (Default mode) or a selection (Edit mode),
// a right-click may repeat the last command instead of opening the menu.
let o = &app.options;
let mode =
if o.right_click_default == o.right_click_edit || app.session.selection().is_empty() { o.right_click_default } else { o.right_click_edit };
if !snap && mode == crate::options::IdleClick::Repeat {
if resp.secondary_clicked()
&& let Some(last) = app.session.last_command.clone()
{
app.start(&last);
}
return;
}
resp.context_menu(|ui| {
let groups = if snap { snap_entries(app) } else { entries(app) };
groups_ui(ui, &groups, &mut clicked);
@@ -330,8 +348,9 @@ struct Press(f64, egui::Pos2);
/// The canvas's right button while a command runs (Shift/Ctrl+right-click is the snap menu's): a
/// quick click is Enter — returns `true` when the caller should send it — and a click held for
/// [`HOLD_SECS`] opens the command menu ([`command_entries`]) at the pointer (any click does with
/// the time-sensitive right-click turned off). Call it every frame
/// OPTIONS `rightClickDuration` opens the command menu ([`command_entries`]) at the pointer. With
/// the time-sensitive right-click turned off, `rightClickCommand` says whether a click opens the
/// menu or is Enter. Call it every frame
/// with whether the pointer is on the canvas and whether a right-click ended there; it also shows
/// the menu while it is open.
pub fn in_command(app: &mut CadApp, ui: &egui::Ui, inside: bool, right_clicked: bool) -> bool {
@@ -348,14 +367,23 @@ pub fn in_command(app: &mut CadApp, ui: &egui::Ui, inside: bool, right_clicked:
ctx.data_mut(|d| d.insert_temp(press_id, Press(now, p)));
}
let running = app.session.prompting() && !app.canvas.snap_menu;
let o = &app.options;
let timed = o.time_sensitive_right_click;
// Without the time-sensitive right-click, Command mode says whether a click opens the menu.
let menu = match o.right_click_command {
crate::options::CommandClick::Menu => true,
crate::options::CommandClick::Enter => false,
crate::options::CommandClick::MenuWithOptions => app.session.current_prompt().is_some_and(|p| !p.keywords.is_empty()),
};
let hold = f64::from(o.right_click_hold_ms) / 1000.0;
let mut open = false;
if released && let Some(Press(t0, p0)) = ctx.data_mut(|d| d.remove_temp::<Press>(press_id)) {
// A drag isn't a click; a click held long enough asks for the menu.
let still = pos.is_some_and(|p| p.distance(p0) <= 6.0);
let held = now - t0 >= HOLD_SECS || !app.options.time_sensitive_right_click;
let held = if timed { now - t0 >= hold } else { menu };
open = running && still && held;
}
let enter = running && inside && right_clicked && !open && app.options.time_sensitive_right_click;
let enter = running && inside && right_clicked && !open && (timed || !menu);
if !running {
return false;
}
+17
View File
@@ -1381,3 +1381,20 @@ menu Modify Edita
Ctrl+Enter to apply Ctrl+Enter to apply
Edit Text Edit Text
The drawing changed. Close the window and open it again. Il disegno è cambiato. Chiudere la finestra e riaprirla.
Right-Click Customization... Personalizzazione pulsante destro...
Right-Click Customization Personalizzazione pulsante destro
Turn on time-sensitive right-click Attiva clic destro sensibile al tempo
Longer click duration: Durata clic lungo:
milliseconds millisecondi
While a command runs, a quick click is Enter and a longer click opens the shortcut menu. Durante un comando un clic rapido equivale a Invio, un clic più lungo apre il menu di scelta rapida.
Default Mode Modalità predefinita
No objects selected and no command running: Nessun oggetto selezionato e nessun comando in corso:
Edit Mode Modalità modifica
Objects selected and no command running: Oggetti selezionati e nessun comando in corso:
Repeat last command Ripeti ultimo comando
Shortcut Menu Menu di scelta rapida
Command Mode Modalità comando
While a command runs: Durante un comando:
ENTER INVIO
Shortcut Menu: always enabled Menu di scelta rapida: sempre attivo
Shortcut Menu: enabled when command options are present Menu di scelta rapida: attivo se il comando ha opzioni
1 # CADCraft Italian interface catalog — original translations by Emanuele Bottelli (AI-assisted, Claude).
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
+17
View File
@@ -1405,3 +1405,20 @@
Press the keys Натисніть клавіші
Back to CADCraft's shortcut Повернути комбінацію CADCraft
A shortcut you give wins over CADCraft's own. Changes apply at once and are kept for next time. Ваша комбінація клавіш має перевагу над комбінацією CADCraft. Зміни застосовуються одразу й зберігаються.
Right-Click Customization... Налаштування правої кнопки...
Right-Click Customization Налаштування правої кнопки
Turn on time-sensitive right-click Увімкнути залежне від часу клацання правою кнопкою
Longer click duration: Тривалість довгого клацання:
milliseconds мілісекунд
While a command runs, a quick click is Enter and a longer click opens the shortcut menu. Під час команди коротке клацання — Enter, довше відкриває контекстне меню.
Default Mode Режим за замовчуванням
No objects selected and no command running: Нічого не вибрано й команда не виконується:
Edit Mode Режим редагування
Objects selected and no command running: Об'єкти вибрано, команда не виконується:
Repeat last command Повторити останню команду
Shortcut Menu Контекстне меню
Command Mode Режим команди
While a command runs: Під час виконання команди:
ENTER Клавіша Enter
Shortcut Menu: always enabled Контекстне меню: завжди
Shortcut Menu: enabled when command options are present Контекстне меню: коли команда має параметри
Can't render this file because it contains an unexpected character in line 1385 and column 24.
+143 -5
View File
@@ -16,7 +16,10 @@
//! | `SAVETIME` | minutes between automatic saves, 0–600 (0: off) |
//! | `SAVEFILEPATH` | the folder automatic saves go to |
//! | `ISAVEBAK` | 1: saving over a drawing keeps the previous file as `<name>.bak` (default); 0: no backup |
//! | `timeSensitiveRightClick` | while a command runs, a quick right-click is Enter and holding the button opens the shortcut menu (default); `false`: a right-click always opens the menu |
//! | `timeSensitiveRightClick` | while a command runs, a quick right-click is Enter and holding the button opens the shortcut menu (default); `false`: `rightClickCommand` decides |
//! | `rightClickDuration` | how long the right button is held to open the menu while a command runs, 100–2000 ms (250) |
//! | `rightClickDefault`, `rightClickEdit` | a right-click with no command running and nothing (`Default`) or something (`Edit`) selected: `"menu"` (the shortcut menu, default) or `"repeat"` (the last command) |
//! | `rightClickCommand` | a right-click while a command runs, with the time-sensitive right-click off: `"menu"` (default), `"enter"`, or `"menuWithOptions"` (the menu when the prompt has options, else Enter) |
//! | `systemCursor` | keep the system cursor visible over the drawing area |
//! | `historyLines` | command-line history lines, 0–12 |
//! | `modelBackground`, `paperBackground` | `"#rrggbb"`, or `null` for the default |
@@ -65,6 +68,66 @@ pub const SNAP_MARKER_SIZES: std::ops::RangeInclusive<f32> = 5.0..=31.0;
/// Interface scales the JSON form accepts (1: the system's own size).
pub const INTERFACE_SCALES: std::ops::RangeInclusive<f32> = 0.5..=3.0;
/// How long a right-click may be held before it opens the menu, in milliseconds.
pub const RIGHT_CLICK_DURATIONS: std::ops::RangeInclusive<u32> = 100..=2000;
/// What a right-click does with no command running (Right-Click Customization's Default and Edit
/// modes).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum IdleClick {
/// Repeat the last command.
Repeat,
/// The shortcut menu.
#[default]
Menu,
}
impl IdleClick {
pub const ALL: [IdleClick; 2] = [IdleClick::Repeat, IdleClick::Menu];
/// The JSON name.
pub fn as_str(self) -> &'static str {
match self {
IdleClick::Repeat => "repeat",
IdleClick::Menu => "menu",
}
}
pub fn parse(s: &str) -> Option<IdleClick> {
IdleClick::ALL.into_iter().find(|c| c.as_str().eq_ignore_ascii_case(s.trim()))
}
}
/// What a right-click does while a command runs with the time-sensitive right-click off
/// (Right-Click Customization's Command mode).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CommandClick {
/// Enter.
Enter,
/// The shortcut menu, always.
#[default]
Menu,
/// The shortcut menu when the prompt has options, else Enter.
MenuWithOptions,
}
impl CommandClick {
pub const ALL: [CommandClick; 3] = [CommandClick::Enter, CommandClick::Menu, CommandClick::MenuWithOptions];
/// The JSON name.
pub fn as_str(self) -> &'static str {
match self {
CommandClick::Enter => "enter",
CommandClick::Menu => "menu",
CommandClick::MenuWithOptions => "menuWithOptions",
}
}
pub fn parse(s: &str) -> Option<CommandClick> {
CommandClick::ALL.into_iter().find(|c| c.as_str().eq_ignore_ascii_case(s.trim()))
}
}
/// The interface's own preferences (the drafting ones are engine settings).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UiOptions {
@@ -77,8 +140,16 @@ pub struct UiOptions {
/// The selection highlight colour; `None` keeps the theme's.
pub selection_color: Option<Color32>,
/// While a command runs: a quick right-click is Enter and a held one opens the shortcut menu
/// (AutoCAD's time-sensitive right-click). Off: a right-click always opens the menu.
/// (AutoCAD's time-sensitive right-click). Off: [`Self::right_click_command`] decides.
pub time_sensitive_right_click: bool,
/// How long the right button is held to open the menu while a command runs, in milliseconds.
pub right_click_hold_ms: u32,
/// A right-click with no command running and nothing selected (Default mode) or a selection
/// (Edit mode).
pub right_click_default: IdleClick,
pub right_click_edit: IdleClick,
/// A right-click while a command runs, with the time-sensitive right-click off.
pub right_click_command: CommandClick,
/// The size of every menu, palette, dialog and the command line (1: the system's own).
pub interface_scale: f32,
}
@@ -91,6 +162,10 @@ impl Default for UiOptions {
snap_marker_size: theme::SNAP_MARKER,
selection_color: None,
time_sensitive_right_click: true,
right_click_hold_ms: (crate::context_menu::HOLD_SECS * 1000.0) as u32,
right_click_default: IdleClick::Menu,
right_click_edit: IdleClick::Menu,
right_click_command: CommandClick::Menu,
interface_scale: 1.0,
}
}
@@ -158,6 +233,10 @@ struct UiChanges {
savefilepath: Option<String>,
isavebak: Option<bool>,
time_sensitive_right_click: Option<bool>,
right_click_hold_ms: Option<u32>,
right_click_default: Option<IdleClick>,
right_click_edit: Option<IdleClick>,
right_click_command: Option<CommandClick>,
interface_scale: Option<f32>,
}
@@ -201,6 +280,17 @@ fn parse_ui(key: &str, v: &Value, c: &mut UiChanges) -> Result<bool, String> {
c.save_version = Some(crate::SaveVersion::parse(name.trim()).ok_or("saveVersion must be 2000, 2004, 2007, 2010, 2013 or 2018")?);
}
"timeSensitiveRightClick" => c.time_sensitive_right_click = Some(v.as_bool().ok_or("timeSensitiveRightClick must be true or false")?),
"rightClickDuration" => {
let (lo, hi) = (f64::from(*RIGHT_CLICK_DURATIONS.start()), f64::from(*RIGHT_CLICK_DURATIONS.end()));
let n =
v.as_f64().filter(|n| n.is_finite() && (lo..=hi).contains(n)).ok_or("rightClickDuration must be from 100 to 2000 milliseconds")?;
c.right_click_hold_ms = Some(n.round() as u32);
}
"rightClickDefault" => c.right_click_default = Some(IdleClick::parse(text()?).ok_or("rightClickDefault must be menu or repeat")?),
"rightClickEdit" => c.right_click_edit = Some(IdleClick::parse(text()?).ok_or("rightClickEdit must be menu or repeat")?),
"rightClickCommand" => {
c.right_click_command = Some(CommandClick::parse(text()?).ok_or("rightClickCommand must be menu, enter or menuWithOptions")?)
}
k if k.eq_ignore_ascii_case("SAVETIME") => {
let max = cadcraft_engine::autosave::MAX_SAVETIME;
let n = v
@@ -242,6 +332,10 @@ fn ui_json(app: &CadApp) -> Map<String, Value> {
m.insert("SAVEFILEPATH".into(), json!(cadcraft_engine::autosave::savefilepath()));
m.insert("ISAVEBAK".into(), json!(i32::from(cadcraft_engine::autosave::isavebak())));
m.insert("timeSensitiveRightClick".into(), json!(o.time_sensitive_right_click));
m.insert("rightClickDuration".into(), json!(o.right_click_hold_ms));
m.insert("rightClickDefault".into(), json!(o.right_click_default.as_str()));
m.insert("rightClickEdit".into(), json!(o.right_click_edit.as_str()));
m.insert("rightClickCommand".into(), json!(o.right_click_command.as_str()));
m
}
@@ -318,9 +412,26 @@ fn run_json(app: &mut CadApp, params: &Value) -> Result<Value, String> {
if let Some(b) = ui.time_sensitive_right_click {
app.options.time_sensitive_right_click = b;
}
apply_right_click(&mut app.options, &ui);
Ok(report(app))
}
/// Right-Click Customization's settings (the time-sensitive switch aside) from `c`.
fn apply_right_click(o: &mut UiOptions, c: &UiChanges) {
if let Some(ms) = c.right_click_hold_ms {
o.right_click_hold_ms = ms;
}
if let Some(m) = c.right_click_default {
o.right_click_default = m;
}
if let Some(m) = c.right_click_edit {
o.right_click_edit = m;
}
if let Some(m) = c.right_click_command {
o.right_click_command = m;
}
}
/// The interface preferences back to their defaults (the interface language stays).
fn reset_ui(app: &mut CadApp) {
let d = crate::UiState::default();
@@ -349,6 +460,10 @@ impl CadApp {
"selectionColor": color_json(self.options.selection_color),
"snapMarkerSize": self.options.snap_marker_size,
"timeSensitiveRightClick": self.options.time_sensitive_right_click,
"rightClickDuration": self.options.right_click_hold_ms,
"rightClickDefault": self.options.right_click_default.as_str(),
"rightClickEdit": self.options.right_click_edit.as_str(),
"rightClickCommand": self.options.right_click_command.as_str(),
"interfaceScale": self.options.interface_scale,
})
.to_string()
@@ -370,9 +485,19 @@ impl CadApp {
cadcraft_engine::cmd::file::restore_aliases(aliases);
}
let mut c = UiChanges::default();
for key in
["systemCursor", "modelBackground", "paperBackground", "selectionColor", "snapMarkerSize", "timeSensitiveRightClick", "interfaceScale"]
{
for key in [
"systemCursor",
"modelBackground",
"paperBackground",
"selectionColor",
"snapMarkerSize",
"timeSensitiveRightClick",
"rightClickDuration",
"rightClickDefault",
"rightClickEdit",
"rightClickCommand",
"interfaceScale",
] {
if let Some(value) = v.get(key) {
let _ = parse_ui(key, value, &mut c);
}
@@ -396,6 +521,7 @@ impl CadApp {
if let Some(b) = c.time_sensitive_right_click {
self.options.time_sensitive_right_click = b;
}
apply_right_click(&mut self.options, &c);
if let Some(n) = c.interface_scale {
self.options.interface_scale = n;
}
@@ -415,4 +541,16 @@ mod tests {
assert_eq!(parse_hex(bad), None, "{bad}");
}
}
#[test]
fn idle_and_command_clicks_parse_their_names() {
for c in IdleClick::ALL {
assert_eq!(IdleClick::parse(c.as_str()), Some(c));
}
for c in CommandClick::ALL {
assert_eq!(CommandClick::parse(c.as_str()), Some(c));
}
assert_eq!(IdleClick::parse("enter"), None);
assert_eq!(CommandClick::parse("repeat"), None);
}
}
+90 -1
View File
@@ -537,8 +537,9 @@ fn aci_row(ui: &mut egui::Ui, current: u8, key: &str, changes: &mut Map<String,
});
}
/// Preferences tab: interface language, largest-array limit and time-sensitive right-click; edits accumulate in `changes`.
/// Preferences tab: interface language, largest-array limit, time-sensitive right-click and the Right-Click Customization window; edits accumulate in `changes`.
fn preferences(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
let mut right_click = ui.ctx().data(|d| d.get_temp::<bool>(egui::Id::unique(RIGHT_CLICK)).unwrap_or(false));
grid(ui, "options_preferences", |ui| {
let language_caption = ui.label(crate::tl!("Interface language"));
let mut lang = app.ui.interface_language;
@@ -571,6 +572,94 @@ fn preferences(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>
changes.insert("timeSensitiveRightClick".into(), json!(timed));
}
hint(ui, crate::tl!("Turned off, a right-click while a command runs always opens the shortcut menu."));
if ui.button(crate::tl!("Right-Click Customization...")).clicked() {
right_click = true;
}
if right_click {
right_click_window(&app.options, ui.ctx(), &mut right_click, changes);
}
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::unique(RIGHT_CLICK), right_click));
}
/// Whether the Right-Click Customization window is open (egui temp data).
const RIGHT_CLICK: &str = "cc_options_right_click";
/// Right-Click Customization (Options ▸ User Preferences): the time-sensitive right-click and its
/// duration, and what a right-click does in Default, Edit and Command modes. Changes apply at
/// once, as on the Options tabs.
fn right_click_window(o: &super::UiOptions, ctx: &egui::Context, open: &mut bool, changes: &mut Map<String, Value>) {
use super::{CommandClick, IdleClick};
let mut close = false;
egui::Window::new(crate::tl!("Right-Click Customization"))
.id(egui::Id::unique("cc_right_click"))
.open(open)
.resizable(false)
.collapsible(false)
.pivot(egui::Align2::CENTER_CENTER)
.default_pos(ctx.content_rect().center())
.show(ctx, |ui| {
ui.set_width(460.0);
let mut timed = o.time_sensitive_right_click;
if ui.checkbox(&mut timed, crate::tl!("Turn on time-sensitive right-click")).changed() {
changes.insert("timeSensitiveRightClick".into(), json!(timed));
}
ui.add_enabled_ui(o.time_sensitive_right_click, |ui| {
ui.horizontal(|ui| {
ui.add_space(20.0);
let caption = ui.label(crate::tl!("Longer click duration:"));
let mut ms = o.right_click_hold_ms;
let range = super::RIGHT_CLICK_DURATIONS;
if ui.add(egui::DragValue::new(&mut ms).range(range).speed(10.0)).labelled_by(caption.id).changed() {
changes.insert("rightClickDuration".into(), json!(ms));
}
ui.label(crate::tl!("milliseconds"));
});
hint(ui, crate::tl!("While a command runs, a quick click is Enter and a longer click opens the shortcut menu."));
});
ui.add_space(6.0);
for (title, note, key, current) in [
(crate::tl!("Default Mode"), crate::tl!("No objects selected and no command running:"), "rightClickDefault", o.right_click_default),
(crate::tl!("Edit Mode"), crate::tl!("Objects selected and no command running:"), "rightClickEdit", o.right_click_edit),
] {
heading(ui, title);
ui.group(|ui| {
ui.set_width(ui.available_width());
hint(ui, note);
for (choice, label) in [(IdleClick::Repeat, crate::tl!("Repeat last command")), (IdleClick::Menu, crate::tl!("Shortcut Menu"))] {
if ui.radio(current == choice, label).clicked() && current != choice {
changes.insert(key.into(), json!(choice.as_str()));
}
}
});
}
ui.add_enabled_ui(!o.time_sensitive_right_click, |ui| {
heading(ui, crate::tl!("Command Mode"));
ui.group(|ui| {
ui.set_width(ui.available_width());
hint(ui, crate::tl!("While a command runs:"));
for (choice, label) in [
(CommandClick::Enter, crate::tl!("ENTER")),
(CommandClick::Menu, crate::tl!("Shortcut Menu: always enabled")),
(CommandClick::MenuWithOptions, crate::tl!("Shortcut Menu: enabled when command options are present")),
] {
if ui.radio(o.right_click_command == choice, label).clicked() && o.right_click_command != choice {
changes.insert("rightClickCommand".into(), json!(choice.as_str()));
}
}
});
});
ui.separator();
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(crate::tl!("Close")).clicked() {
close = true;
}
});
});
});
if close {
*open = false;
}
}
fn profiles(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
+1
View File
@@ -23,6 +23,7 @@ mod named_viewports_dialog;
mod native_menu_text_keys;
mod native_windows;
mod options_dialog;
mod options_right_click;
mod parameters_rename;
mod pickdrag;
mod properties_units;
@@ -0,0 +1,210 @@
//! Options ▸ User Preferences ▸ Right-Click Customization: the time-sensitive right-click's
//! duration, and what a right-click does with nothing selected (Default mode), with a selection
//! (Edit mode) and while a command runs (Command mode), through the OPTIONS JSON form, the
//! drawing area and the dialog.
use cadcraft_engine::Session;
use cadcraft_ui_egui::options::{CommandClick, IdleClick};
use cadcraft_ui_egui::{CadApp, Services, menus};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
fn app() -> CadApp {
CadApp::new(Session::new(), Services::default())
}
/// One frame at `time` (seconds) with `events`; returns the painted text.
fn frame(app: &mut CadApp, ctx: &egui::Context, events: Vec<egui::Event>, time: f64) -> String {
fn collect(shape: &egui::Shape, text: &mut String) {
match shape {
egui::Shape::Text(s) => {
text.push_str(s.galley.text());
text.push('\n');
}
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect(s, text)),
_ => {}
}
}
let input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0))),
time: Some(time),
events,
..Default::default()
};
app.logic(ctx);
let mut out = ctx.run_ui(input, |ui| app.ui(ui));
out.textures_delta.clear();
let mut text = String::new();
out.shapes.iter().for_each(|s| collect(&s.shape, &mut text));
text
}
const AT: egui::Pos2 = egui::pos2(800.0, 500.0);
fn button(pressed: bool) -> egui::Event {
egui::Event::PointerButton { pos: AT, button: egui::PointerButton::Secondary, pressed, modifiers: egui::Modifiers::NONE }
}
/// A right-click on the drawing area from `t` held for `held` seconds; returns the text painted
/// just after.
fn right_click(app: &mut CadApp, ctx: &egui::Context, t: f64, held: f64) -> String {
frame(app, ctx, vec![egui::Event::PointerMoved(AT)], t);
frame(app, ctx, vec![button(true)], t + 0.01);
frame(app, ctx, vec![button(false)], t + 0.01 + held);
frame(app, ctx, Vec::new(), t + 0.05 + held)
}
#[test]
fn right_click_settings_are_checked_and_survive_a_restart() {
let mut a = app();
let all = a.run("options", json!({})).unwrap();
assert_eq!(
(all["rightClickDuration"].clone(), all["rightClickDefault"].clone(), all["rightClickEdit"].clone(), all["rightClickCommand"].clone()),
(json!(250), json!("menu"), json!("menu"), json!("menu"))
);
let r = a
.run(
"options",
json!({"rightClickDuration": 400, "rightClickDefault": "repeat", "rightClickEdit": "Repeat", "rightClickCommand": "menuWithOptions"}),
)
.unwrap();
assert_eq!((r["rightClickDuration"].clone(), r["rightClickEdit"].clone()), (json!(400), json!("repeat")));
assert_eq!(a.options.right_click_command, CommandClick::MenuWithOptions);
let before = a.run("options", json!({})).unwrap();
for bad in [
json!({"rightClickDuration": 50}),
json!({"rightClickDuration": 2001}),
json!({"rightClickDuration": f64::NAN}),
json!({"rightClickDuration": "long"}),
json!({"rightClickDefault": "enter"}),
json!({"rightClickEdit": 1}),
json!({"rightClickCommand": "repeat", "PICKBOX": 9}),
] {
assert!(a.run("options", bad.clone()).is_err(), "{bad}");
assert_eq!(a.run("options", json!({})).unwrap(), before, "{bad} changed nothing");
}
let mut b = app();
b.load_profile(&a.profile_json());
assert_eq!(b.options, a.options);
let mut c = app();
c.load_profile(r#"{"rightClickDuration": 99999, "rightClickDefault": "fly", "rightClickCommand": null}"#);
assert_eq!(c.options, app().options, "hostile saved values are skipped");
let r = a.run("options", json!({"reset": true})).unwrap();
assert_eq!((r["rightClickDuration"].clone(), r["rightClickDefault"].clone()), (json!(250), json!("menu")));
}
#[test]
fn an_idle_right_click_can_repeat_the_last_command() {
let mut a = app();
a.run("options", json!({"rightClickDefault": "repeat"})).unwrap();
a.cmdline("circle 0,0 5");
assert!(!a.session.prompting());
let ctx = egui::Context::default();
frame(&mut a, &ctx, Vec::new(), 0.0);
right_click(&mut a, &ctx, 0.1, 0.05);
assert!(a.session.prompting(), "CIRCLE runs again: {}", a.session.prompt_text());
a.cmdline("10,10 2");
// With a selection, Edit mode decides: the menu (its default) opens instead.
a.run("selectall", json!({})).unwrap();
assert!(!a.session.selection().is_empty());
let text = right_click(&mut a, &ctx, 1.0, 0.05);
assert!(!a.session.prompting(), "no command started: {}", a.session.prompt_text());
assert!(text.contains("Erase"), "the shortcut menu opens:\n{text}");
}
#[test]
fn command_mode_and_click_duration_decide_enter_or_the_menu() {
let ctx = egui::Context::default();
// A longer duration: a 0.5 s click is still Enter.
let mut a = app();
a.run("options", json!({"rightClickDuration": 1000})).unwrap();
frame(&mut a, &ctx, Vec::new(), 0.0);
a.cmdline("LINE 0,0 10,0");
right_click(&mut a, &ctx, 0.1, 0.5);
assert!(a.session.running.is_none(), "a click shorter than the duration is Enter: {}", a.session.prompt_text());
// Time-sensitive off, Command mode Enter: every right-click is Enter.
let ctx = egui::Context::default();
let mut a = app();
a.run("options", json!({"timeSensitiveRightClick": false, "rightClickCommand": "enter"})).unwrap();
frame(&mut a, &ctx, Vec::new(), 0.0);
a.cmdline("LINE 0,0 10,0");
let text = right_click(&mut a, &ctx, 0.1, 0.6);
assert!(a.session.running.is_none(), "Enter ends LINE: {}", a.session.prompt_text());
assert!(!text.contains("Snap Overrides"), "no menu:\n{text}");
// Command mode "enabled when command options are present": CIRCLE's first prompt has
// options, so the menu opens and the command keeps running.
a.run("options", json!({"rightClickCommand": "menuWithOptions"})).unwrap();
a.cmdline("CIRCLE");
let text = right_click(&mut a, &ctx, 2.0, 0.05);
assert!(text.contains("Cancel"), "the command menu opens:\n{text}");
assert!(a.session.prompting(), "CIRCLE still runs");
}
/// With `CADCRAFT_OPTIONS_SHOTS` set to a folder, the dialog is rendered there for a look.
const SHOTS: &str = "CADCRAFT_OPTIONS_SHOTS";
fn harness(app: CadApp) -> Harness<'static, CadApp> {
let builder = Harness::builder().with_size(egui::vec2(1400.0, 900.0));
let builder = if std::env::var_os(SHOTS).is_some() { builder.wgpu() } else { builder };
let mut h = builder.build_ui_state(
|ui, app: &mut CadApp| {
app.logic(ui.ctx());
app.ui(ui);
},
app,
);
h.run_steps(3);
h
}
fn shot(h: &mut Harness<'static, CadApp>, name: &str) {
if let Some(dir) = std::env::var_os(SHOTS).map(std::path::PathBuf::from) {
std::fs::create_dir_all(&dir).unwrap();
h.render().unwrap().save(dir.join(name)).unwrap();
}
}
#[test]
fn the_customization_window_sets_every_mode() {
let mut a = app();
menus::activate(&mut a, "options");
a.ui.dialog = Some("options:preferences".into());
let mut h = harness(a);
h.get_by_label("Right-Click Customization...").click();
h.run_steps(3);
h.get_by_label("Longer click duration:");
shot(&mut h, "options-right-click.png");
// Default mode's "Repeat last command" comes first, Edit mode's second.
if let Some(n) = h.get_all_by_label("Repeat last command").next() {
n.click();
}
h.run_steps(3);
assert_eq!(h.state().options.right_click_default, IdleClick::Repeat);
assert_eq!(h.state().options.right_click_edit, IdleClick::Menu);
// Command mode applies once the time-sensitive right-click is off.
h.get_by_label("Turn on time-sensitive right-click").click();
h.run_steps(3);
assert!(!h.state().options.time_sensitive_right_click);
h.get_by_label("Shortcut Menu: enabled when command options are present").click();
h.run_steps(3);
assert_eq!(h.state().options.right_click_command, CommandClick::MenuWithOptions);
let unnamed = h.unnamed_widgets();
assert!(unnamed.is_empty(), "unnamed controls: {unnamed:?}");
// The window's Close comes after the Options dialog's in the tree.
if let Some(n) = h.get_all_by_label("Close").last() {
n.click();
}
h.run_steps(3);
assert!(h.query_by_label("Longer click duration:").is_none());
assert_eq!(h.state().ui.dialog.as_deref(), Some("options:preferences"));
}