feat(input): scalar entry via dynamic input
Text-input steps (OFFSET distance, POLYGON sides, radii, counts) were swallowed by the dynamic-input overlay: typed digits landed in the field buffer and Enter committed a point pick, so the value never reached on_text_input (issue #61). Now each text step shows one scalar box whose value routes to on_text_input on commit; non-numeric prompts (names, keywords) keep going to the command line so mixed text like "Bolt12" isn't split. OFFSET reworked: object first, then through-mode by default — the offset follows the cursor at the perpendicular distance to the object. Typing a number locks the magnitude (cursor then only picks the side); a value typed and clicked without Enter is applied before the point pick. The dynamic-input overlay and the command line now show the active step's prompt and hold it until the step completes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c9c6e45793
commit
4b878b88c4
15 changed files with 405 additions and 70 deletions
|
|
@ -1734,10 +1734,7 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].scene.document.get_entity(h).cloned()
|
self.tabs[i].scene.document.get_entity(h).cloned()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let new_cmd = OffsetCommand::new(
|
let new_cmd = OffsetCommand::new(all_entities);
|
||||||
crate::modules::home::defaults::get_offset_dist(),
|
|
||||||
all_entities,
|
|
||||||
);
|
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ pub(super) enum DynComponent {
|
||||||
Distance,
|
Distance,
|
||||||
/// Angle from the last point, in degrees.
|
/// Angle from the last point, in degrees.
|
||||||
Angle,
|
Angle,
|
||||||
|
/// A scalar the command reads from the command line (a count, a radius,
|
||||||
|
/// a delta). Typed-only — it has no geometric live value derived from
|
||||||
|
/// the cursor unless the command supplies one via `dyn_live_value`.
|
||||||
|
Scalar,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single editable dynamic-input field. `buffer == None` means the box
|
/// A single editable dynamic-input field. `buffer == None` means the box
|
||||||
|
|
|
||||||
|
|
@ -2850,6 +2850,31 @@ impl OpenCADStudio {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// A scalar typed into the dynamic-input box but not
|
||||||
|
// yet confirmed with Enter is applied before the
|
||||||
|
// point pick — e.g. an OFFSET distance typed and then
|
||||||
|
// clicked takes effect rather than being discarded.
|
||||||
|
let wants_text = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.wants_text_input())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if wants_text {
|
||||||
|
if let Some(text) = self.tabs[i]
|
||||||
|
.dyn_fields
|
||||||
|
.iter()
|
||||||
|
.find_map(|f| f.buffer.clone())
|
||||||
|
{
|
||||||
|
let text = super::expr_eval::eval_to_string(text.trim());
|
||||||
|
if let Some(c) = self.tabs[i].active_cmd.as_mut() {
|
||||||
|
c.on_text_input(&text);
|
||||||
|
}
|
||||||
|
for f in self.tabs[i].dyn_fields.iter_mut() {
|
||||||
|
f.buffer = None;
|
||||||
|
}
|
||||||
|
self.tabs[i].dyn_active = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
self.last_point = Some(world_pt);
|
self.last_point = Some(world_pt);
|
||||||
self.dyn_user_reshaped = false;
|
self.dyn_user_reshaped = false;
|
||||||
self.sync_dyn_fields();
|
self.sync_dyn_fields();
|
||||||
|
|
@ -7106,10 +7131,28 @@ impl OpenCADStudio {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.dyn_field())
|
.map(|c| c.dyn_field())
|
||||||
.unwrap_or(crate::command::DynField::Point);
|
.unwrap_or(crate::command::DynField::Point);
|
||||||
|
// A text-input step reads a single scalar. The overlay shows one box
|
||||||
|
// the user types into (or, when the command supplies a live value,
|
||||||
|
// sets by moving the cursor); on commit the host routes it to
|
||||||
|
// `on_text_input` instead of `on_point`. A distance prompt keeps the
|
||||||
|
// `Distance` box (so a perpendicular-distance live value reads
|
||||||
|
// naturally); everything else uses the typed-only `Scalar` box.
|
||||||
|
let wants_text = self
|
||||||
|
.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.wants_text_input())
|
||||||
|
.unwrap_or(false);
|
||||||
let has_base = self.last_point.is_some();
|
let has_base = self.last_point.is_some();
|
||||||
let default: Vec<DynComponent> = match field {
|
let default: Vec<DynComponent> = match field {
|
||||||
crate::command::DynField::Distance => vec![DynComponent::Distance],
|
crate::command::DynField::Distance => vec![DynComponent::Distance],
|
||||||
crate::command::DynField::Angle => vec![DynComponent::Angle],
|
crate::command::DynField::Angle => vec![DynComponent::Angle],
|
||||||
|
crate::command::DynField::Scalar => vec![DynComponent::Scalar],
|
||||||
|
// A text prompt with the default `Point` field reads free text /
|
||||||
|
// a name / a keyword (which may itself contain digits) from the
|
||||||
|
// command line — show no scalar box, so digits reach the command
|
||||||
|
// line instead of being captured into a dyn buffer.
|
||||||
|
crate::command::DynField::Point if wants_text => vec![],
|
||||||
crate::command::DynField::Point if has_base => {
|
crate::command::DynField::Point if has_base => {
|
||||||
vec![DynComponent::Distance, DynComponent::Angle]
|
vec![DynComponent::Distance, DynComponent::Angle]
|
||||||
}
|
}
|
||||||
|
|
@ -7128,7 +7171,7 @@ impl OpenCADStudio {
|
||||||
// the command's default so e.g. clicking the first point of LINE
|
// the command's default so e.g. clicking the first point of LINE
|
||||||
// flips a stale `[X, Y]` (from before there was a base) over to
|
// flips a stale `[X, Y]` (from before there was a base) over to
|
||||||
// the polar `[Distance, Angle]` the prompt actually wants.
|
// the polar `[Distance, Angle]` the prompt actually wants.
|
||||||
let current_is_acceptable = if self.dyn_user_reshaped {
|
let current_is_acceptable = if self.dyn_user_reshaped && !wants_text {
|
||||||
match field {
|
match field {
|
||||||
crate::command::DynField::Distance => {
|
crate::command::DynField::Distance => {
|
||||||
matches!(current.as_slice(), [DynComponent::Distance])
|
matches!(current.as_slice(), [DynComponent::Distance])
|
||||||
|
|
@ -7136,6 +7179,9 @@ impl OpenCADStudio {
|
||||||
crate::command::DynField::Angle => {
|
crate::command::DynField::Angle => {
|
||||||
matches!(current.as_slice(), [DynComponent::Angle])
|
matches!(current.as_slice(), [DynComponent::Angle])
|
||||||
}
|
}
|
||||||
|
crate::command::DynField::Scalar => {
|
||||||
|
matches!(current.as_slice(), [DynComponent::Scalar])
|
||||||
|
}
|
||||||
crate::command::DynField::Point => matches!(
|
crate::command::DynField::Point => matches!(
|
||||||
current.as_slice(),
|
current.as_slice(),
|
||||||
[DynComponent::Distance, DynComponent::Angle]
|
[DynComponent::Distance, DynComponent::Angle]
|
||||||
|
|
@ -7447,6 +7493,42 @@ impl OpenCADStudio {
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
// A text-input step reads its single box as a string and commits via
|
||||||
|
// `on_text_input` (a count, radius, distance) rather than resolving a
|
||||||
|
// point. Only the typed buffer matters here — a mouse-driven live
|
||||||
|
// value commits through the viewport click, not Enter.
|
||||||
|
let wants_text = self
|
||||||
|
.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.wants_text_input())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if wants_text {
|
||||||
|
let text = self.tabs[i]
|
||||||
|
.dyn_fields
|
||||||
|
.iter()
|
||||||
|
.find_map(|f| f.buffer.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let text = super::expr_eval::eval_to_string(text.trim());
|
||||||
|
let result = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|c| c.on_text_input(&text));
|
||||||
|
for f in self.tabs[i].dyn_fields.iter_mut() {
|
||||||
|
f.buffer = None;
|
||||||
|
}
|
||||||
|
self.tabs[i].dyn_active = 0;
|
||||||
|
self.sync_dyn_fields();
|
||||||
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||||
|
if let Some(p) = prompt {
|
||||||
|
self.command_line.push_info(&p);
|
||||||
|
}
|
||||||
|
self.refresh_active_cmd_preview(i);
|
||||||
|
return Some(match result {
|
||||||
|
Some(r) => self.apply_cmd_result(r),
|
||||||
|
None => self.focus_cmd_input(),
|
||||||
|
});
|
||||||
|
}
|
||||||
let pt = self.dyn_resolve_point()?;
|
let pt = self.dyn_resolve_point()?;
|
||||||
self.last_point = Some(pt);
|
self.last_point = Some(pt);
|
||||||
self.dyn_user_reshaped = false;
|
self.dyn_user_reshaped = false;
|
||||||
|
|
|
||||||
|
|
@ -676,18 +676,45 @@ impl OpenCADStudio {
|
||||||
if self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty() {
|
if self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty() {
|
||||||
let w = tab.last_cursor_world;
|
let w = tab.last_cursor_world;
|
||||||
let base = self.last_point;
|
let base = self.last_point;
|
||||||
|
// A command may drive a typed scalar by mouse (e.g. a
|
||||||
|
// perpendicular distance to a picked object); show that live
|
||||||
|
// value in the box until the user types over it.
|
||||||
|
let live = tab.active_cmd.as_ref().and_then(|c| c.dyn_live_value(w));
|
||||||
let boxes: Vec<overlay::DynBox> = tab
|
let boxes: Vec<overlay::DynBox> = tab
|
||||||
.dyn_fields
|
.dyn_fields
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(idx, f)| overlay::DynBox {
|
.map(|(idx, f)| {
|
||||||
label: dyn_component_label(f.component),
|
let value = match (&f.buffer, live) {
|
||||||
value: dyn_component_value(f, w, base),
|
(Some(b), _) => b.clone(),
|
||||||
active: idx == tab.dyn_active,
|
(None, Some(lv))
|
||||||
locked: f.locked(),
|
if matches!(
|
||||||
|
f.component,
|
||||||
|
DynComponent::Scalar | DynComponent::Distance
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
format!("{lv:.4}")
|
||||||
|
}
|
||||||
|
_ => dyn_component_value(f, w, base),
|
||||||
|
};
|
||||||
|
overlay::DynBox {
|
||||||
|
label: dyn_component_label(f.component),
|
||||||
|
value,
|
||||||
|
active: idx == tab.dyn_active,
|
||||||
|
locked: f.locked(),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Some(overlay::dynamic_input_overlay(tab.last_cursor_screen, boxes))
|
let prompt = tab
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.prompt())
|
||||||
|
.unwrap_or_default();
|
||||||
|
Some(overlay::dynamic_input_overlay(
|
||||||
|
tab.last_cursor_screen,
|
||||||
|
boxes,
|
||||||
|
prompt,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -1150,8 +1177,13 @@ impl OpenCADStudio {
|
||||||
&& !tab.dyn_fields.is_empty())
|
&& !tab.dyn_fields.is_empty())
|
||||||
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
||||||
|| self.text_inline.is_some();
|
|| self.text_inline.is_some();
|
||||||
|
let active_prompt = tab.active_cmd.as_ref().map(|c| c.prompt());
|
||||||
let command_line_overlay =
|
let command_line_overlay =
|
||||||
iced::widget::container(self.command_line.view(allow_autocomplete, dyn_capturing))
|
iced::widget::container(self.command_line.view(
|
||||||
|
allow_autocomplete,
|
||||||
|
dyn_capturing,
|
||||||
|
active_prompt,
|
||||||
|
))
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.align_x(iced::alignment::Horizontal::Center)
|
.align_x(iced::alignment::Horizontal::Center)
|
||||||
|
|
@ -3496,6 +3528,7 @@ fn dyn_component_label(c: DynComponent) -> String {
|
||||||
DynComponent::Z => "Z".into(),
|
DynComponent::Z => "Z".into(),
|
||||||
DynComponent::Distance => "d".into(),
|
DynComponent::Distance => "d".into(),
|
||||||
DynComponent::Angle => "<".into(),
|
DynComponent::Angle => "<".into(),
|
||||||
|
DynComponent::Scalar => "".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3527,5 +3560,7 @@ fn dyn_component_value(
|
||||||
DynComponent::Z => format!("{:.4}", b.z),
|
DynComponent::Z => format!("{:.4}", b.z),
|
||||||
DynComponent::Distance => format!("{:.4}", (dx * dx + dy * dy).sqrt()),
|
DynComponent::Distance => format!("{:.4}", (dx * dx + dy * dy).sqrt()),
|
||||||
DynComponent::Angle => format!("{:.1}", dy.atan2(dx).to_degrees().rem_euclid(360.0)),
|
DynComponent::Angle => format!("{:.1}", dy.atan2(dx).to_degrees().rem_euclid(360.0)),
|
||||||
|
// Typed-only scalar — no geometric value to track when empty.
|
||||||
|
DynComponent::Scalar => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,10 @@ pub enum DynField {
|
||||||
Distance,
|
Distance,
|
||||||
/// An angle, shown in degrees, measured from the last point.
|
/// An angle, shown in degrees, measured from the last point.
|
||||||
Angle,
|
Angle,
|
||||||
|
/// A typed scalar with no geometric meaning at the cursor — a count
|
||||||
|
/// (number of sides / segments) or any value the command reads purely
|
||||||
|
/// from the keyboard. Shown as a single typed box.
|
||||||
|
Scalar,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Trait ─────────────────────────────────────────────────────────────────
|
// ── Trait ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -379,6 +383,17 @@ pub trait CadCommand: Send {
|
||||||
fn dyn_field(&self) -> DynField {
|
fn dyn_field(&self) -> DynField {
|
||||||
DynField::Point
|
DynField::Point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Live value for the dynamic-input scalar box, derived from the cursor
|
||||||
|
/// world position. Lets a command drive a typed prompt by mouse — e.g.
|
||||||
|
/// OFFSET returns the perpendicular distance from the cursor to the
|
||||||
|
/// object being offset, so moving the cursor fills in the distance.
|
||||||
|
/// Returns `None` when the value can only be typed (a count, or a
|
||||||
|
/// distance with no reference yet). The string the host commits is this
|
||||||
|
/// value formatted; the command's own `on_text_input` parses it back.
|
||||||
|
fn dyn_live_value(&self, _cursor: Vec3) -> Option<f64> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -781,6 +781,14 @@ impl CadCommand for CircleTTRCommand {
|
||||||
matches!(self.step, StepTTR::Radius { .. })
|
matches!(self.step, StepTTR::Radius { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if matches!(self.step, StepTTR::Radius { .. }) {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match &self.step {
|
||||||
StepTTR::First => "CIRCLE TTR Select first tangent object:".into(),
|
StepTTR::First => "CIRCLE TTR Select first tangent object:".into(),
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,14 @@ impl CadCommand for DonutCommand {
|
||||||
matches!(self.state, DonutState::AskInner | DonutState::AskOuter)
|
matches!(self.state, DonutState::AskInner | DonutState::AskOuter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if matches!(self.state, DonutState::AskInner | DonutState::AskOuter) {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let val: f64 = text
|
let val: f64 = text
|
||||||
.trim()
|
.trim()
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,14 @@ impl CadCommand for PolyCommand {
|
||||||
self.step == 0
|
self.step == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if self.step == 0 {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if let Ok(n) = text.trim().parse::<u32>() {
|
if let Ok(n) = text.trim().parse::<u32>() {
|
||||||
if (3..=1024).contains(&n) {
|
if (3..=1024).contains(&n) {
|
||||||
|
|
@ -443,6 +451,14 @@ impl CadCommand for PolyCCommand {
|
||||||
self.step == 0
|
self.step == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if self.step == 0 {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if let Ok(n) = text.trim().parse::<u32>() {
|
if let Ok(n) = text.trim().parse::<u32>() {
|
||||||
if (3..=1024).contains(&n) {
|
if (3..=1024).contains(&n) {
|
||||||
|
|
@ -544,6 +560,14 @@ impl CadCommand for PolyECommand {
|
||||||
self.step == 0
|
self.step == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if self.step == 0 {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if let Ok(n) = text.trim().parse::<u32>() {
|
if let Ok(n) = text.trim().parse::<u32>() {
|
||||||
if (3..=1024).contains(&n) {
|
if (3..=1024).contains(&n) {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,14 @@ impl CadCommand for DivideCommand {
|
||||||
self.waiting_for_n
|
self.waiting_for_n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if self.waiting_for_n {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let n: usize = text.trim().parse().ok().filter(|&n| n >= 2)?;
|
let n: usize = text.trim().parse().ok().filter(|&n| n >= 2)?;
|
||||||
let handle = self.target?;
|
let handle = self.target?;
|
||||||
|
|
@ -117,6 +125,14 @@ impl CadCommand for MeasureCommand {
|
||||||
self.waiting_for_dist
|
self.waiting_for_dist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if self.waiting_for_dist {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let dist: f64 = text
|
let dist: f64 = text
|
||||||
.trim()
|
.trim()
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,10 @@ impl CadCommand for ArrayRectCommand {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let t = text.trim().replace(',', ".");
|
let t = text.trim().replace(',', ".");
|
||||||
let t = t.as_str();
|
let t = t.as_str();
|
||||||
|
|
@ -279,6 +283,14 @@ impl CadCommand for ArrayPolarCommand {
|
||||||
matches!(self.step, PolarStep::Count { .. } | PolarStep::Angle { .. })
|
matches!(self.step, PolarStep::Count { .. } | PolarStep::Angle { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if matches!(self.step, PolarStep::Count { .. } | PolarStep::Angle { .. }) {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let t = text.trim().replace(',', ".");
|
let t = text.trim().replace(',', ".");
|
||||||
let t = t.as_str();
|
let t = t.as_str();
|
||||||
|
|
@ -601,6 +613,14 @@ impl CadCommand for ArrayPathCommand {
|
||||||
matches!(self.step, PathStep::Count { .. })
|
matches!(self.step, PathStep::Count { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if matches!(self.step, PathStep::Count { .. }) {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||||
if handle.is_null() || self.handles.contains(&handle) {
|
if handle.is_null() || self.handles.contains(&handle) {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
|
|
@ -811,6 +831,10 @@ impl CadCommand for Array3DCommand {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let t = text.trim().replace(',', ".");
|
let t = text.trim().replace(',', ".");
|
||||||
let t = t.as_str();
|
let t = t.as_str();
|
||||||
|
|
|
||||||
|
|
@ -1235,6 +1235,14 @@ impl CadCommand for FilletCommand {
|
||||||
matches!(self.step, FilletStep::WaitingForRadius)
|
matches!(self.step, FilletStep::WaitingForRadius)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if matches!(self.step, FilletStep::WaitingForRadius) {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
match self.step {
|
match self.step {
|
||||||
FilletStep::WaitingForRadius => {
|
FilletStep::WaitingForRadius => {
|
||||||
|
|
@ -1574,6 +1582,17 @@ impl CadCommand for ChamferCommand {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if matches!(
|
||||||
|
self.step,
|
||||||
|
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2
|
||||||
|
) {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
match self.step {
|
match self.step {
|
||||||
ChamferStep::WaitingForDist1 => {
|
ChamferStep::WaitingForDist1 => {
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,14 @@ impl CadCommand for LengthenCommand {
|
||||||
matches!(self.state, LenState::PickOption { .. })
|
matches!(self.state, LenState::PickOption { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
if matches!(self.state, LenState::PickOption { .. }) {
|
||||||
|
crate::command::DynField::Scalar
|
||||||
|
} else {
|
||||||
|
crate::command::DynField::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
let (handle, pick_pt) = match &self.state {
|
let (handle, pick_pt) = match &self.state {
|
||||||
LenState::PickOption { handle, pick_pt } => (*handle, *pick_pt),
|
LenState::PickOption { handle, pick_pt } => (*handle, *pick_pt),
|
||||||
|
|
|
||||||
|
|
@ -381,6 +381,44 @@ fn compute_offset(entity: &EntityType, dist: f64, side_pt: Vec3) -> Option<Entit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Through-mode distance ─────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Nearest distance from the cursor to the entity outline, used by "through"
|
||||||
|
// mode so the offset copy passes through the cursor. Measured against the
|
||||||
|
// tessellated wire (point-to-segment), which approximates the perpendicular
|
||||||
|
// distance for every supported entity type.
|
||||||
|
|
||||||
|
fn perp_distance(entity: &EntityType, pt: Vec3) -> f64 {
|
||||||
|
let pts = entity_wire_pts(entity);
|
||||||
|
if pts.len() < 2 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let px = pt.x as f64;
|
||||||
|
let py = pt.y as f64;
|
||||||
|
let mut best = f64::INFINITY;
|
||||||
|
for w in pts.windows(2) {
|
||||||
|
let ax = w[0][0] as f64;
|
||||||
|
let ay = w[0][1] as f64;
|
||||||
|
let bx = w[1][0] as f64;
|
||||||
|
let by = w[1][1] as f64;
|
||||||
|
let dx = bx - ax;
|
||||||
|
let dy = by - ay;
|
||||||
|
let len2 = dx * dx + dy * dy;
|
||||||
|
let t = if len2 < 1e-12 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(((px - ax) * dx + (py - ay) * dy) / len2).clamp(0.0, 1.0)
|
||||||
|
};
|
||||||
|
let cx = ax + t * dx;
|
||||||
|
let cy = ay + t * dy;
|
||||||
|
let d = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
|
||||||
|
if d < best {
|
||||||
|
best = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
// ── Wire preview points ─────────────────────────────────────────────────────
|
// ── Wire preview points ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn entity_wire_pts(e: &EntityType) -> Vec<[f32; 3]> {
|
fn entity_wire_pts(e: &EntityType) -> Vec<[f32; 3]> {
|
||||||
|
|
@ -546,29 +584,27 @@ fn lwpolyline_pts(p: &LwPolyline) -> Vec<[f32; 3]> {
|
||||||
// ── Command implementation ─────────────────────────────────────────────────
|
// ── Command implementation ─────────────────────────────────────────────────
|
||||||
|
|
||||||
enum Step {
|
enum Step {
|
||||||
Distance,
|
SelectObject,
|
||||||
SelectObject {
|
// `locked == None` is "through" mode: the offset magnitude follows the
|
||||||
dist: f64,
|
// cursor (perpendicular distance to the picked object). Typing a value
|
||||||
},
|
// locks the magnitude; the cursor then only chooses the side.
|
||||||
PickSide {
|
PickSide {
|
||||||
dist: f64,
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
entity: EntityType,
|
entity: EntityType,
|
||||||
|
locked: Option<f64>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OffsetCommand {
|
pub struct OffsetCommand {
|
||||||
default_dist: f64,
|
|
||||||
step: Step,
|
step: Step,
|
||||||
all_entities: Vec<EntityType>,
|
all_entities: Vec<EntityType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OffsetCommand {
|
impl OffsetCommand {
|
||||||
pub fn new(last_dist: f32, all_entities: Vec<EntityType>) -> Self {
|
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
default_dist: last_dist as f64,
|
step: Step::SelectObject,
|
||||||
step: Step::Distance,
|
|
||||||
all_entities,
|
all_entities,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -581,56 +617,26 @@ impl CadCommand for OffsetCommand {
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match &self.step {
|
||||||
Step::Distance => format!(
|
Step::SelectObject => "OFFSET Select object to offset:".into(),
|
||||||
"OFFSET Specify offset distance <{:.4}>:",
|
Step::PickSide {
|
||||||
self.default_dist
|
locked: Some(d), ..
|
||||||
|
} => format!("OFFSET Click side [distance {d:.4}, type to change]:"),
|
||||||
|
Step::PickSide { locked: None, .. } => format!(
|
||||||
|
"OFFSET Click through point, or type a distance (last {:.4}):",
|
||||||
|
defaults::get_offset_dist()
|
||||||
),
|
),
|
||||||
Step::SelectObject { dist } => {
|
|
||||||
format!("OFFSET Select object to offset [dist={dist:.4}]:")
|
|
||||||
}
|
|
||||||
Step::PickSide { dist, .. } => {
|
|
||||||
format!("OFFSET Specify side to offset [dist={dist:.4}]:")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
|
||||||
matches!(self.step, Step::Distance)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
|
||||||
if let Step::Distance = self.step {
|
|
||||||
let dist = if text.trim().is_empty() {
|
|
||||||
self.default_dist
|
|
||||||
} else {
|
|
||||||
text.trim()
|
|
||||||
.replace(',', ".")
|
|
||||||
.parse::<f64>()
|
|
||||||
.unwrap_or(self.default_dist)
|
|
||||||
.abs()
|
|
||||||
.max(1e-9)
|
|
||||||
};
|
|
||||||
defaults::set_offset_dist(dist as f32);
|
|
||||||
self.default_dist = dist;
|
|
||||||
self.step = Step::SelectObject { dist };
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
matches!(self.step, Step::SelectObject { .. })
|
matches!(self.step, Step::SelectObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||||
if handle.is_null() {
|
if handle.is_null() || !matches!(self.step, Step::SelectObject) {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dist = match &self.step {
|
|
||||||
Step::SelectObject { dist } => *dist,
|
|
||||||
_ => return CmdResult::NeedPoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
let entity = self
|
let entity = self
|
||||||
.all_entities
|
.all_entities
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -643,9 +649,9 @@ impl CadCommand for OffsetCommand {
|
||||||
| Some(e @ EntityType::Arc(_))
|
| Some(e @ EntityType::Arc(_))
|
||||||
| Some(e @ EntityType::LwPolyline(_)) => {
|
| Some(e @ EntityType::LwPolyline(_)) => {
|
||||||
self.step = Step::PickSide {
|
self.step = Step::PickSide {
|
||||||
dist,
|
|
||||||
handle,
|
handle,
|
||||||
entity: e,
|
entity: e,
|
||||||
|
locked: None,
|
||||||
};
|
};
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|
@ -653,6 +659,44 @@ impl CadCommand for OffsetCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The side step accepts an optional typed magnitude — through mode by
|
||||||
|
// default, a fixed distance once the user types one.
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
matches!(self.step, Step::PickSide { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
|
match self.step {
|
||||||
|
Step::PickSide { .. } => crate::command::DynField::Scalar,
|
||||||
|
_ => crate::command::DynField::Point,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> {
|
||||||
|
match &self.step {
|
||||||
|
Step::PickSide { entity, locked, .. } => {
|
||||||
|
Some(locked.unwrap_or_else(|| perp_distance(entity, cursor)))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
if let Step::PickSide { locked, .. } = &mut self.step {
|
||||||
|
let t = text.trim().replace(',', ".");
|
||||||
|
if !t.is_empty() {
|
||||||
|
if let Ok(d) = t.parse::<f64>() {
|
||||||
|
let d = d.abs().max(1e-9);
|
||||||
|
defaults::set_offset_dist(d as f32);
|
||||||
|
*locked = Some(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Stay on the side step — the click chooses which side.
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn on_hover_entity(&mut self, handle: Handle, _pt: Vec3) -> Vec<WireModel> {
|
fn on_hover_entity(&mut self, handle: Handle, _pt: Vec3) -> Vec<WireModel> {
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
return vec![];
|
return vec![];
|
||||||
|
|
@ -676,24 +720,32 @@ impl CadCommand for OffsetCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||||
let (dist, entity) = match &self.step {
|
let (locked, entity) = match &self.step {
|
||||||
Step::PickSide { dist, entity, .. } => (*dist, entity.clone()),
|
Step::PickSide { locked, entity, .. } => (*locked, entity.clone()),
|
||||||
_ => return CmdResult::NeedPoint,
|
_ => return CmdResult::NeedPoint,
|
||||||
};
|
};
|
||||||
|
let mag = locked.unwrap_or_else(|| perp_distance(&entity, pt));
|
||||||
|
if mag < 1e-9 {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
|
||||||
match compute_offset(&entity, dist, pt) {
|
match compute_offset(&entity, mag, pt) {
|
||||||
Some(new_entity) => CmdResult::CommitAndExit(new_entity),
|
Some(new_entity) => CmdResult::CommitAndExit(new_entity),
|
||||||
None => CmdResult::NeedPoint,
|
None => CmdResult::NeedPoint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
|
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
|
||||||
let (dist, entity) = match &self.step {
|
let (locked, entity) = match &self.step {
|
||||||
Step::PickSide { dist, entity, .. } => (*dist, entity.clone()),
|
Step::PickSide { locked, entity, .. } => (*locked, entity.clone()),
|
||||||
_ => return vec![],
|
_ => return vec![],
|
||||||
};
|
};
|
||||||
|
let mag = locked.unwrap_or_else(|| perp_distance(&entity, pt));
|
||||||
|
if mag < 1e-9 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(result) = compute_offset(&entity, dist, pt) {
|
if let Some(result) = compute_offset(&entity, mag, pt) {
|
||||||
let pts = entity_wire_pts(&result);
|
let pts = entity_wire_pts(&result);
|
||||||
if !pts.is_empty() {
|
if !pts.is_empty() {
|
||||||
return vec![WireModel::solid(
|
return vec![WireModel::solid(
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,12 @@ impl CommandLine {
|
||||||
matches
|
matches
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self, show_autocomplete: bool, dyn_capturing: bool) -> Element<'_, Message> {
|
pub fn view(
|
||||||
|
&self,
|
||||||
|
show_autocomplete: bool,
|
||||||
|
dyn_capturing: bool,
|
||||||
|
active_prompt: Option<String>,
|
||||||
|
) -> Element<'_, Message> {
|
||||||
// Only the most recent entries pushed within the last few
|
// Only the most recent entries pushed within the last few
|
||||||
// seconds show on the overlay. The dropdown button keeps the
|
// seconds show on the overlay. The dropdown button keeps the
|
||||||
// full backlog reachable when the user actually wants it.
|
// full backlog reachable when the user actually wants it.
|
||||||
|
|
@ -247,7 +252,12 @@ impl CommandLine {
|
||||||
};
|
};
|
||||||
col.push(container(text(&entry.text).size(11).color(color)).padding([1, 8]))
|
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 a command is running, the prompt shows what the current
|
||||||
|
// step is asking for and stays put until that step completes — it
|
||||||
|
// does not fade like the history lines. With no active command it
|
||||||
|
// falls back to the generic entry prompt.
|
||||||
|
let prompt_label = active_prompt.unwrap_or_else(|| "Command:".to_string());
|
||||||
|
let prompt = container(text(prompt_label).size(11).color(PROMPT_COLOR)).padding([5, 8]);
|
||||||
// While dynamic input is capturing keystrokes, the command-line
|
// While dynamic input is capturing keystrokes, the command-line
|
||||||
// text field is left without an `on_input` handler so it can't
|
// text field is left without an `on_input` handler so it can't
|
||||||
// grab focus or swallow numeric keys — those flow through the
|
// grab focus or swallow numeric keys — those flow through the
|
||||||
|
|
|
||||||
|
|
@ -1188,10 +1188,15 @@ pub struct DynBox {
|
||||||
pub locked: bool,
|
pub locked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dynamic_input_overlay<'a>(cursor_screen: Point, boxes: Vec<DynBox>) -> Element<'a, Message> {
|
pub fn dynamic_input_overlay<'a>(
|
||||||
|
cursor_screen: Point,
|
||||||
|
boxes: Vec<DynBox>,
|
||||||
|
prompt: String,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
canvas(DynInputCanvas {
|
canvas(DynInputCanvas {
|
||||||
cursor_screen,
|
cursor_screen,
|
||||||
boxes,
|
boxes,
|
||||||
|
prompt,
|
||||||
})
|
})
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|
@ -1201,6 +1206,9 @@ pub fn dynamic_input_overlay<'a>(cursor_screen: Point, boxes: Vec<DynBox>) -> El
|
||||||
struct DynInputCanvas {
|
struct DynInputCanvas {
|
||||||
cursor_screen: Point,
|
cursor_screen: Point,
|
||||||
boxes: Vec<DynBox>,
|
boxes: Vec<DynBox>,
|
||||||
|
/// The active command's current prompt — tells the user what this step
|
||||||
|
/// is asking for, drawn just above the input boxes.
|
||||||
|
prompt: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl canvas::Program<Message> for DynInputCanvas {
|
impl canvas::Program<Message> for DynInputCanvas {
|
||||||
|
|
@ -1258,6 +1266,31 @@ impl canvas::Program<Message> for DynInputCanvas {
|
||||||
by = (self.cursor_screen.y - BOX_H - 4.0).max(0.0);
|
by = (self.cursor_screen.y - BOX_H - 4.0).max(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prompt line above the boxes — what this step wants right now.
|
||||||
|
// Drawn on an opaque pill so the viewport behind it stays legible.
|
||||||
|
if !self.prompt.is_empty() {
|
||||||
|
let py = (by - BOX_H - 2.0).max(0.0);
|
||||||
|
let pw = (self.prompt.len() as f32 * CHAR_W) + PAD * 2.0;
|
||||||
|
let prect = canvas::Path::rectangle(
|
||||||
|
Point { x: bx, y: py },
|
||||||
|
Size { width: pw, height: BOX_H },
|
||||||
|
);
|
||||||
|
frame.fill(&prect, Color { r: 0.10, g: 0.10, b: 0.12, a: 1.0 });
|
||||||
|
frame.stroke(
|
||||||
|
&prect,
|
||||||
|
canvas::Stroke::default()
|
||||||
|
.with_color(Color { r: 0.35, g: 0.55, b: 0.90, a: 0.9 })
|
||||||
|
.with_width(1.0),
|
||||||
|
);
|
||||||
|
frame.fill_text(canvas::Text {
|
||||||
|
content: self.prompt.clone(),
|
||||||
|
position: Point { x: bx + PAD, y: py + PAD },
|
||||||
|
color: Color { r: 0.70, g: 0.85, b: 0.70, a: 1.0 },
|
||||||
|
size: iced::Pixels(FONT_SIZE),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let mut x = bx;
|
let mut x = bx;
|
||||||
for (i, b) in self.boxes.iter().enumerate() {
|
for (i, b) in self.boxes.iter().enumerate() {
|
||||||
let w = widths[i];
|
let w = widths[i];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue