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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-09 18:32:03 +03:00
1 parent c9c6e45793
commit 4b878b88c4
15 files changed
+405 -70

No files matched your search

+12 -2
View File
@@ -226,7 +226,12 @@ impl CommandLine {
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
// seconds show on the overlay. The dropdown button keeps the
// 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]))
});
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
// text field is left without an `on_input` handler so it can't
// grab focus or swallow numeric keys — those flow through the
+34 -1
View File
@@ -1188,10 +1188,15 @@ pub struct DynBox {
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 {
cursor_screen,
boxes,
prompt,
})
.width(Length::Fill)
.height(Length::Fill)
@@ -1201,6 +1206,9 @@ pub fn dynamic_input_overlay<'a>(cursor_screen: Point, boxes: Vec<DynBox>) -> El
struct DynInputCanvas {
cursor_screen: Point,
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 {
@@ -1258,6 +1266,31 @@ impl canvas::Program<Message> for DynInputCanvas {
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;
for (i, b) in self.boxes.iter().enumerate() {
let w = widths[i];