diff --git a/src/app/mod.rs b/src/app/mod.rs index ad912dd81..1b7358b24 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1300,6 +1300,9 @@ pub enum Message { /// User clicked an autocomplete suggestion — fill the input with /// the chosen command name and dispatch it. CommandSuggestionPick(String), + /// User clicked a command-line option button — feed its keyword to the + /// active command as if typed (empty keyword = Enter). (#304) + CommandOptionPick(String), ToggleLayers, LayerToggleVisible(usize), LayerToggleLock(usize), diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 542fbdb51..6bab75c96 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -147,6 +147,13 @@ impl OpenCADStudio { .as_ref() .map(|c| c.prompt()); self.command_line.set_step_prompt(prompt); + // Mirror the step's clickable options so they render as buttons (#304). + let opts = self.tabs[self.active_tab] + .active_cmd + .as_ref() + .map(|c| c.options()) + .unwrap_or_default(); + self.command_line.set_step_options(opts); // Persist UI preferences whenever a toggle changes them (issue #68). self.persist_settings_if_changed(); // OTRACK acquires tracking points only while a command or grip drag is @@ -829,6 +836,19 @@ impl OpenCADStudio { self.dispatch_command(&cmd) } + Message::CommandOptionPick(kw) => { + // Clicking an option button feeds its keyword to the active + // command through the same path as typed text; an empty keyword + // finishes the step like Enter. (#304) + self.command_line.input.clear(); + self.command_line.close_history(); + if kw.is_empty() { + return self.feed_command(crate::command::StepInput::Enter); + } + self.feed_active_cmd(&kw); + Task::none() + } + Message::CommandSubmit => self.on_command_submit(), Message::CommandSpace => { diff --git a/src/command.rs b/src/command.rs index 3587d7e41..b2c6cd001 100644 --- a/src/command.rs +++ b/src/command.rs @@ -499,6 +499,37 @@ pub struct DynSpec { // ── Trait ───────────────────────────────────────────────────────────────── /// An interactive CAD command that collects user input step-by-step. +/// One clickable option a command step offers. Rendered as a button next to +/// the command-line prompt; clicking it feeds `keyword` to the running command +/// exactly as if the user had typed it (routed through `on_text_input`). An +/// empty `keyword` submits the step like pressing Enter (the finish / default +/// action). Lets every bracketed `[A=arc L=line …]` prompt become buttons so +/// the option need not be typed. (#304) +#[derive(Clone, Debug)] +pub struct CmdOption { + /// Text shown on the button, e.g. `"3P"` or `"Close"`. + pub label: String, + /// Token fed to the command when clicked, e.g. `"3P"`. Empty = Enter. + pub keyword: String, +} + +impl CmdOption { + /// Button whose keyword is typed on click, e.g. `("Ttr", "TTR")`. + pub fn new(label: &str, keyword: &str) -> Self { + Self { + label: label.to_string(), + keyword: keyword.to_string(), + } + } + /// A "finish" button that submits the step like Enter. + pub fn enter(label: &str) -> Self { + Self { + label: label.to_string(), + keyword: String::new(), + } + } +} + pub trait CadCommand: Send { /// Short name shown in the command line prompt, e.g. `"LINE"`. #[allow(dead_code)] @@ -506,6 +537,14 @@ pub trait CadCommand: Send { /// Current prompt string to display in the command line. fn prompt(&self) -> String; + /// Clickable keyword options for the current step, rendered as buttons in + /// the command line next to the prompt. Default: none. A command returns + /// different options per step; clicking a button feeds its keyword through + /// the same path as typed command-line text (`on_text_input`). (#304) + fn options(&self) -> Vec { + Vec::new() + } + /// Push the active UCS into the command as a UCS→render(wire)-space affine /// (identity = plain WCS). Commands that build axis-aligned geometry (RECT, /// rectangular ARRAY, …) override this to store it and rotate their implicit diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs index 0190410e7..d4fa47795 100644 --- a/src/modules/draw/draw/arc.rs +++ b/src/modules/draw/draw/arc.rs @@ -254,6 +254,28 @@ impl CadCommand for ArcCommand { ), } } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + // Alternate construction methods, offered only at the first (center) + // step. Each keyword hands off to the dedicated variant command. + if self.step == 0 { + vec![ + CmdOption::new("SCE", "SCE"), + CmdOption::new("SCA", "SCA"), + CmdOption::new("SEA", "SEA"), + CmdOption::new("SER", "SER"), + CmdOption::new("CSA", "CSA"), + CmdOption::new("3P", "3P"), + ] + } else { + vec![] + } + } + + fn point_step_accepts_keywords(&self) -> bool { + self.step == 0 + } fn on_point(&mut self, pt: DVec3) -> CmdResult { match self.step { 0 => { @@ -284,6 +306,22 @@ impl CadCommand for ArcCommand { fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } + fn on_text_input(&mut self, text: &str) -> Option { + // At the center step, keyword options switch construction method by + // handing off to the dedicated variant command. + if self.step == 0 { + return match text.trim().to_uppercase().as_str() { + "SCE" => Some(CmdResult::Dispatch("ARC_SCE".into())), + "SCA" => Some(CmdResult::Dispatch("ARC_SCA".into())), + "SEA" => Some(CmdResult::Dispatch("ARC_SEA".into())), + "SER" => Some(CmdResult::Dispatch("ARC_SER".into())), + "CSA" => Some(CmdResult::Dispatch("ARC_CSA".into())), + "3P" => Some(CmdResult::Dispatch("ARC_3P".into())), + _ => None, + }; + } + None + } fn on_mouse_move(&mut self, pt: DVec3) -> Option { match self.step { 1 => Some(line_wire(self.c, pt)), diff --git a/src/modules/draw/draw/circle.rs b/src/modules/draw/draw/circle.rs index a44765121..47d598ad3 100644 --- a/src/modules/draw/draw/circle.rs +++ b/src/modules/draw/draw/circle.rs @@ -142,6 +142,24 @@ impl CadCommand for CircleCommand { ), } } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.step { + StepCR::Center => vec![ + CmdOption::new("3P", "3P"), + CmdOption::new("2P", "2P"), + CmdOption::new("Ttr", "TTR"), + CmdOption::new("Ttt", "TTT"), + CmdOption::new("Diameter", "D"), + ], + StepCR::Radius(_) => vec![], + } + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!(self.step, StepCR::Center) + } fn on_point(&mut self, pt: DVec3) -> CmdResult { match &self.step { StepCR::Center => { @@ -174,6 +192,18 @@ impl CadCommand for CircleCommand { } } fn on_text_input(&mut self, text: &str) -> Option { + // At the centre step, keyword options switch construction method by + // handing off to the dedicated variant command. (#304) + if matches!(self.step, StepCR::Center) { + return match text.trim().to_uppercase().as_str() { + "3P" => Some(CmdResult::Dispatch("CIRCLE_3P".into())), + "2P" => Some(CmdResult::Dispatch("CIRCLE_2P".into())), + "T" | "TTR" => Some(CmdResult::Dispatch("CIRCLE_TTR".into())), + "TTT" => Some(CmdResult::Dispatch("CIRCLE_TTT".into())), + "D" | "DIAMETER" => Some(CmdResult::Dispatch("CIRCLE_CD".into())), + _ => None, + }; + } if let StepCR::Radius(c) = &self.step { let r: f64 = text.trim().replace(',', ".").parse().ok()?; if r > 0.0 { diff --git a/src/modules/draw/draw/ellipse.rs b/src/modules/draw/draw/ellipse.rs index a974da38f..f782b2289 100644 --- a/src/modules/draw/draw/ellipse.rs +++ b/src/modules/draw/draw/ellipse.rs @@ -135,6 +135,21 @@ impl CadCommand for EllipseCommand { } } + fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.step { + CtrStep::Center => vec![ + CmdOption::new("Arc", "ARC"), + CmdOption::new("Axis", "AXIS"), + ], + _ => vec![], + } + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!(self.step, CtrStep::Center) + } + fn on_point(&mut self, pt: DVec3) -> CmdResult { match &self.step { CtrStep::Center => { @@ -164,6 +179,15 @@ impl CadCommand for EllipseCommand { } fn on_text_input(&mut self, text: &str) -> Option { + // At the centre step, keyword options switch construction method by + // handing off to the dedicated variant command. + if matches!(self.step, CtrStep::Center) { + return match text.trim().to_uppercase().as_str() { + "A" | "ARC" => Some(CmdResult::Dispatch("ELLIPSE_ARC".into())), + "AXIS" => Some(CmdResult::Dispatch("ELLIPSE_AXIS".into())), + _ => None, + }; + } if let CtrStep::MinorRatio { center, major } = &self.step { let r_minor = parse_num(text)?; if r_minor > 0.0 { diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index 0212c6d45..dce2af062 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -139,16 +139,28 @@ impl CadCommand for HatchCommand { } else { "" }; - format!("HATCH Pick internal point [S=Draw manually]:{miss}") + format!("HATCH Pick internal point:{miss}") } Mode::Manual => { if self.manual_pts.is_empty() { "HATCH Boundary point 1:".into() } else { - format!( - "HATCH Point {} [Enter=accept, ≥3 points]:", - self.manual_pts.len() + 1 - ) + format!("HATCH Point {}:", self.manual_pts.len() + 1) + } + } + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + match &self.mode { + Mode::PickInside => vec![CmdOption::new("Draw manually", "S")], + Mode::Manual => { + // Enter accepts the boundary once at least 3 points are picked. + if self.manual_pts.len() >= 3 { + vec![CmdOption::enter("Accept")] + } else { + vec![] } } } @@ -277,16 +289,28 @@ impl CadCommand for GradientCommand { } else { "" }; - format!("GRADIENT Pick internal point [S=Draw manually]:{miss}") + format!("GRADIENT Pick internal point:{miss}") } Mode::Manual => { if self.manual_pts.is_empty() { "GRADIENT Boundary point 1:".into() } else { - format!( - "GRADIENT Point {} [Enter=accept, ≥3 points]:", - self.manual_pts.len() + 1 - ) + format!("GRADIENT Point {}:", self.manual_pts.len() + 1) + } + } + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + match &self.mode { + Mode::PickInside => vec![CmdOption::new("Draw manually", "S")], + Mode::Manual => { + // Enter accepts the boundary once at least 3 points are picked. + if self.manual_pts.len() >= 3 { + vec![CmdOption::enter("Accept")] + } else { + vec![] } } } diff --git a/src/modules/draw/draw/point.rs b/src/modules/draw/draw/point.rs index 8b7de36cf..56fb4978f 100644 --- a/src/modules/draw/draw/point.rs +++ b/src/modules/draw/draw/point.rs @@ -34,7 +34,12 @@ impl CadCommand for PointCommand { "POINT" } fn prompt(&self) -> String { - "POINT Specify point [Enter=done]:".into() + "POINT Specify point:".into() + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + vec![CmdOption::enter("Done")] } fn on_point(&mut self, pt: DVec3) -> CmdResult { diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 5d8ade5bc..009b119e4 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -218,12 +218,28 @@ impl CadCommand for PlineCommand { "PLINE Specify start point:".into() } else { format!( - "PLINE [{mode_tag}] Next pt [{}pts | A=arc L=line C=close Enter=done]:", + "PLINE [{mode_tag}] Next pt [{}pts]:", self.vertices.len() ) } } + fn options(&self) -> Vec { + use crate::command::CmdOption; + // Options appear once at least one vertex is placed: the segment step + // accepts A / L / C, and Enter finishes. + if self.vertices.is_empty() { + Vec::new() + } else { + vec![ + CmdOption::new("Arc", "A"), + CmdOption::new("Line", "L"), + CmdOption::new("Close", "C"), + CmdOption::enter("Done"), + ] + } + } + fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); if !self.vertices.is_empty() { let last = *self.vertices.last().unwrap(); diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index 91d0c0474..3e62cc65d 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -35,7 +35,16 @@ impl CadCommand for RayCommand { if self.base.is_none() { "RAY Specify start point:".into() } else { - "RAY Specify through point [Enter/Esc = done]:".into() + "RAY Specify through point:".into() + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.base.is_some() { + vec![CmdOption::enter("Done")] + } else { + vec![] } } @@ -117,7 +126,16 @@ impl CadCommand for XLineCommand { if self.base.is_none() { "XLINE Specify a point:".into() } else { - "XLINE Specify through point [Enter/Esc = done]:".into() + "XLINE Specify through point:".into() + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.base.is_some() { + vec![CmdOption::enter("Done")] + } else { + vec![] } } diff --git a/src/modules/draw/draw/shapes.rs b/src/modules/draw/draw/shapes.rs index 24e5520a1..733710d36 100644 --- a/src/modules/draw/draw/shapes.rs +++ b/src/modules/draw/draw/shapes.rs @@ -170,6 +170,37 @@ impl CadCommand for RectCommand { "RECT Specify opposite corner:".into() } } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + // First corner also offers the alternate rectangle methods; later steps + // are plain point picks. (#304) + if self.a.is_none() { + vec![ + CmdOption::new("Rotation", "ROTATION"), + CmdOption::new("Center", "CENTER"), + ] + } else { + vec![] + } + } + + fn point_step_accepts_keywords(&self) -> bool { + self.a.is_none() + } + + fn on_text_input(&mut self, text: &str) -> Option { + // At the first corner, keyword options hand off to the dedicated + // variant command. (#304) + if self.a.is_none() { + return match text.trim().to_uppercase().as_str() { + "R" | "ROTATION" => Some(CmdResult::Dispatch("RECT_ROT".into())), + "C" | "CENTER" => Some(CmdResult::Dispatch("RECT_CEN".into())), + _ => None, + }; + } + None + } fn on_point(&mut self, pt: DVec3) -> CmdResult { match self.a { None => { @@ -438,6 +469,24 @@ impl CadCommand for PolyCommand { self.step == 0 } + fn options(&self) -> Vec { + use crate::command::CmdOption; + // The sides step also offers the alternate polygon methods; later steps + // are plain point picks. (#304) + if self.step == 0 { + vec![ + CmdOption::new("Circumscribed", "CIRCUMSCRIBED"), + CmdOption::new("Edge", "EDGE"), + ] + } else { + vec![] + } + } + + fn point_step_accepts_keywords(&self) -> bool { + self.step == 0 + } + fn dyn_field(&self) -> crate::command::DynField { if self.step == 0 { crate::command::DynField::Scalar @@ -461,6 +510,15 @@ impl CadCommand for PolyCommand { } fn on_text_input(&mut self, text: &str) -> Option { + // At the sides step, keyword options hand off to the dedicated variant + // command; numeric input still sets the side count. (#304) + if self.step == 0 { + match text.trim().to_uppercase().as_str() { + "C" | "CIRCUMSCRIBED" => return Some(CmdResult::Dispatch("POLY_C".into())), + "E" | "EDGE" => return Some(CmdResult::Dispatch("POLY_E".into())), + _ => {} + } + } if let Ok(n) = text.trim().parse::() { if (3..=1024).contains(&n) { self.sides = n; diff --git a/src/modules/draw/draw/solid2d.rs b/src/modules/draw/draw/solid2d.rs index 709e8e021..d896a9ef9 100644 --- a/src/modules/draw/draw/solid2d.rs +++ b/src/modules/draw/draw/solid2d.rs @@ -55,7 +55,17 @@ impl CadCommand for Solid2dCommand { 0 => "SOLID Specify first point:".to_string(), 1 => "SOLID Specify second point:".to_string(), 2 => "SOLID Specify third point:".to_string(), - _ => "SOLID Specify fourth point [Enter = triangle]:".to_string(), + _ => "SOLID Specify fourth point:".to_string(), + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + // After three corners Enter commits a triangle; offer a Done button. + if self.points.len() == 3 { + vec![CmdOption::enter("Done")] + } else { + Vec::new() } } diff --git a/src/modules/draw/draw/spline.rs b/src/modules/draw/draw/spline.rs index 2a8b86cd3..4fd3b1a77 100644 --- a/src/modules/draw/draw/spline.rs +++ b/src/modules/draw/draw/spline.rs @@ -80,13 +80,22 @@ impl CadCommand for SplineCommand { if self.pts.is_empty() { "SPLINE Specify first control point:".into() } else { - format!( - "SPLINE Specify next point [{} pts | Close/Undo | Enter=done]:", - self.pts.len() - ) + format!("SPLINE Specify next point [{} pts]:", self.pts.len()) } } + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.pts.is_empty() { + return Vec::new(); + } + let mut opts = vec![CmdOption::new("Close", "C")]; + // Undo only makes sense once a control point exists. + opts.push(CmdOption::new("Undo", "U")); + opts.push(CmdOption::enter("Done")); + opts + } + fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); self.pts.push(pt); CmdResult::NeedPoint diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index 7d09129c4..116c8ae81 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -1249,7 +1249,7 @@ impl CadCommand for FilletCommand { fn prompt(&self) -> String { match &self.step { FilletStep::First => format!( - "FILLET Select first object (Line/Arc/LwPolyline) [R={:.4} | type R to change]:", + "FILLET Select first object (Line/Arc/LwPolyline) [R={:.4}]:", self.radius ), FilletStep::WaitingForRadius => { @@ -1264,6 +1264,16 @@ impl CadCommand for FilletCommand { } } + fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.step { + // Offer the radius keyword while selecting objects; the "R" token is + // already handled by on_text_input. + FilletStep::First | FilletStep::Second { .. } => vec![CmdOption::new("Radius", "R")], + FilletStep::WaitingForRadius => vec![], + } + } + fn wants_text_input(&self) -> bool { matches!(self.step, FilletStep::WaitingForRadius) } @@ -1623,7 +1633,7 @@ impl CadCommand for ChamferCommand { fn prompt(&self) -> String { match &self.step { ChamferStep::First => format!( - "CHAMFER Select first line [D1={:.4} D2={:.4} | type D to change]:", + "CHAMFER Select first line [D1={:.4} D2={:.4}]:", self.dist1, self.dist2 ), ChamferStep::WaitingForDist1 => { @@ -1644,6 +1654,18 @@ impl CadCommand for ChamferCommand { } } + fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.step { + // Offer the distance keyword while selecting objects; the "D" token + // is already handled by on_text_input. + ChamferStep::First + | ChamferStep::Second { .. } + | ChamferStep::SecondPoly { .. } => vec![CmdOption::new("Distance", "D")], + ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => vec![], + } + } + fn wants_text_input(&self) -> bool { matches!( self.step, diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index 3431cc182..17c8ce356 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -16,11 +16,17 @@ use crate::command::{CadCommand, CmdResult}; pub struct PeditCommand { target: Option, + /// Set once the bare `W` keyword (or the Width button) is chosen: the next + /// text input is parsed as the new width instead of an option. (#304) + awaiting_width: bool, } impl PeditCommand { pub fn new() -> Self { - Self { target: None } + Self { + target: None, + awaiting_width: false, + } } } @@ -32,8 +38,24 @@ impl CadCommand for PeditCommand { fn prompt(&self) -> String { if self.target.is_none() { "PEDIT Select polyline:".into() + } else if self.awaiting_width { + "PEDIT Specify new width:".into() } else { - "PEDIT Enter option [C=Close O=Open W=Width X=Exit]:".into() + "PEDIT Enter option:".into() + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.target.is_none() || self.awaiting_width { + vec![] + } else { + vec![ + CmdOption::new("Close", "C"), + CmdOption::new("Open", "O"), + CmdOption::new("Width", "W"), + CmdOption::new("eXit", "X"), + ] } } @@ -57,6 +79,21 @@ impl CadCommand for PeditCommand { let handle = self.target?; let up = text.trim().to_uppercase(); + // Width sub-step: the previous input was a bare `W` (typed or via the + // Width button), so this input is the width value. (#304) + if self.awaiting_width { + let w: f64 = up + .replace(',', ".") + .parse() + .ok() + .filter(|&v: &f64| v >= 0.0)?; + self.awaiting_width = false; + return Some(CmdResult::PeditOp { + handle, + op: PeditOp::SetWidth(w), + }); + } + match up.as_str() { "X" | "EXIT" => return Some(CmdResult::Cancel), "C" | "CLOSE" => { @@ -71,10 +108,17 @@ impl CadCommand for PeditCommand { op: PeditOp::SetClosed(false), }) } + "W" | "WIDTH" => { + // Bare width keyword (the Width button): prompt for the value + // rather than doing nothing. + self.awaiting_width = true; + return Some(CmdResult::NeedPoint); + } _ => {} } - if let Some(rest) = up.strip_prefix("W ").or_else(|| up.strip_prefix("W")) { + // Inline shorthand `W `. + if let Some(rest) = up.strip_prefix("W ") { let w: f64 = rest .trim() .replace(',', ".") diff --git a/src/modules/draw/modify/scale.rs b/src/modules/draw/modify/scale.rs index 15f735625..b212d14a4 100644 --- a/src/modules/draw/modify/scale.rs +++ b/src/modules/draw/modify/scale.rs @@ -82,7 +82,7 @@ impl CadCommand for ScaleCommand { self.handles.len() ), Step::Factor { .. } => format!( - "SCALE Specify scale factor or [Reference] <{:.4}>:", + "SCALE Specify scale factor <{:.4}>:", self.default_factor ), Step::RefLen { .. } => { @@ -95,6 +95,15 @@ impl CadCommand for ScaleCommand { } } + fn options(&self) -> Vec { + use crate::command::CmdOption; + match &self.step { + // The reference-scaling keyword is only offered at the factor step. + Step::Factor { .. } => vec![CmdOption::new("Reference", "R")], + _ => vec![], + } + } + fn on_point(&mut self, pt: DVec3) -> CmdResult { match &self.step { Step::Base => { diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 7005cd70e..aa56a45ec 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -2027,7 +2027,11 @@ impl CadCommand for TrimCommand { } fn prompt(&self) -> String { - "TRIM Click segment to remove [Enter=done]:".into() + "TRIM Click segment to remove:".into() + } + + fn options(&self) -> Vec { + vec![crate::command::CmdOption::enter("Done")] } fn needs_entity_pick(&self) -> bool { @@ -2537,7 +2541,11 @@ impl CadCommand for ExtendCommand { } fn prompt(&self) -> String { - "EXTEND Click near end of object to extend [Enter=done]:".into() + "EXTEND Click near end of object to extend:".into() + } + + fn options(&self) -> Vec { + vec![crate::command::CmdOption::enter("Done")] } fn needs_entity_pick(&self) -> bool { diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 87af7f54d..2bf88d5cc 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -3,6 +3,7 @@ use iced::time::Instant; use crate::app::Message; +use crate::command::CmdOption; use iced::widget::{ button, column, container, opaque, row, text, text_editor, text_input, Space, }; @@ -52,6 +53,10 @@ pub struct CommandLine { /// The active command step's prompt, mirrored here so a step change /// can be detected and the pinned (non-fading) history line updated. step_prompt: Option, + /// The active command step's clickable options, mirrored here so they + /// render as buttons above the input row. Empty when no command is + /// active or the current step offers no options. (#304) + step_options: Vec, } #[derive(Clone, Debug)] @@ -213,6 +218,12 @@ impl CommandLine { self.step_prompt = prompt; } + /// Mirror the active command step's clickable options so `view` can render + /// them as buttons. Called each frame alongside [`Self::set_step_prompt`]. + pub fn set_step_options(&mut self, opts: Vec) { + self.step_options = opts; + } + /// `true` while at least one history entry is still within the /// visible window — the host app uses this to drive a low-frequency /// tick subscription so the overlay re-renders and fades the entry @@ -318,7 +329,48 @@ impl CommandLine { EntryKind::Error => ERR_COLOR, EntryKind::Info => INFO_COLOR, }; - col.push(container(text(&entry.text).size(11).color(color)).padding([1, 8])) + // The current step's prompt is the single pinned line. When the + // step offers options, render them as clickable buttons inline + // beside the prompt text — so options need never be typed and + // don't clutter the prompt string itself. (#304) + if entry.pinned && !self.step_options.is_empty() { + let mut r = row![text(&entry.text).size(11).color(color)] + .spacing(6) + .align_y(iced::Center); + for opt in &self.step_options { + let btn = button( + text(opt.label.to_uppercase()).size(11).color(CMD_COLOR), + ) + .on_press(Message::CommandOptionPick(opt.keyword.clone())) + .padding([1, 6]) + .style(|_: &Theme, status| { + let bg = if matches!(status, button::Status::Hovered) { + Color { + r: 0.28, + g: 0.40, + b: 0.56, + a: 1.0, + } + } else { + INPUT_ROW_BG + }; + button::Style { + background: Some(Background::Color(bg)), + text_color: Color::WHITE, + border: Border { + color: BORDER_COLOR, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + } + }); + r = r.push(btn); + } + col.push(container(r).padding([1, 8])) + } else { + col.push(container(text(&entry.text).size(11).color(color)).padding([1, 8])) + } }); let prompt = container(text("Command:").size(11).color(PROMPT_COLOR)).padding([5, 8]); // While dynamic input is capturing keystrokes, the command-line