feat(dyn-input): TAB-editable distance/angle/coordinate fields

Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.

- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
  türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
  açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
  Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
  ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
  geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
  kenar rengi

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-27 14:08:57 +03:00
1 parent 1fc73ba8b0
commit 4abde6001a
6 files changed
+377 -103

No files matched your search

+12 -5
View File
@@ -226,7 +226,7 @@ impl CommandLine {
matches
}
pub fn view(&self, show_autocomplete: bool) -> Element<'_, Message> {
pub fn view(&self, show_autocomplete: bool, dyn_capturing: bool) -> 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.
@@ -248,10 +248,17 @@ 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]);
let input = text_input("", &self.input)
.id(cmd_input_id())
.on_input(Message::CommandInput)
.on_submit(Message::CommandSubmit)
// 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
// global key subscription into the dynamic-input fields instead.
let mut input = text_input("", &self.input).id(cmd_input_id());
if !dyn_capturing {
input = input
.on_input(Message::CommandInput)
.on_submit(Message::CommandSubmit);
}
let input = input
.style(|_: &Theme, _| text_input::Style {
background: Background::Color(INPUT_BG),
border: Border {
+75 -52
View File
@@ -964,10 +964,22 @@ fn draw_ucs_icon(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle) {
///
/// `cursor_screen` — cursor position in viewport pixels.
/// `label` — text to display (e.g. "X: 12.34 Y: 56.78").
pub fn dynamic_input_overlay<'a>(cursor_screen: Point, label: String) -> Element<'a, Message> {
/// One labelled box in the dynamic-input overlay (e.g. `d` = distance,
/// `<` = angle, `X` / `Y` = ordinates).
#[derive(Clone)]
pub struct DynBox {
pub label: String,
pub value: String,
/// TAB-focused box — keystrokes edit this one.
pub active: bool,
/// User has typed a value (the box no longer tracks the cursor).
pub locked: bool,
}
pub fn dynamic_input_overlay<'a>(cursor_screen: Point, boxes: Vec<DynBox>) -> Element<'a, Message> {
canvas(DynInputCanvas {
cursor_screen,
label,
boxes,
})
.width(Length::Fill)
.height(Length::Fill)
@@ -976,7 +988,7 @@ pub fn dynamic_input_overlay<'a>(cursor_screen: Point, label: String) -> Element
struct DynInputCanvas {
cursor_screen: Point,
label: String,
boxes: Vec<DynBox>,
}
impl canvas::Program<Message> for DynInputCanvas {
@@ -1000,68 +1012,79 @@ impl canvas::Program<Message> for DynInputCanvas {
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
if self.boxes.is_empty() {
return vec![frame.into_geometry()];
}
// Offset the box 14 px right and 20 px below the cursor.
// Offset the row 14 px right and 20 px below the cursor.
const OFFSET_X: f32 = 14.0;
const OFFSET_Y: f32 = 20.0;
const PAD: f32 = 4.0;
const GAP: f32 = 6.0;
const FONT_SIZE: f32 = 11.0;
const BOX_W: f32 = 160.0;
const CHAR_W: f32 = FONT_SIZE * 0.62; // monospace-ish width estimate
const BOX_H: f32 = FONT_SIZE + PAD * 2.0;
// Each box is "<label>:<value>"; width tracks the text length.
let texts: Vec<String> = self
.boxes
.iter()
.map(|b| format!("{}:{}", b.label, b.value))
.collect();
let widths: Vec<f32> = texts
.iter()
.map(|t| (t.len() as f32 * CHAR_W) + PAD * 2.0)
.collect();
let total_w: f32 = widths.iter().sum::<f32>() + GAP * (self.boxes.len() as f32 - 1.0);
let mut bx = self.cursor_screen.x + OFFSET_X;
let mut by = self.cursor_screen.y + OFFSET_Y;
// Keep box inside viewport.
if bx + BOX_W > bounds.width {
bx = self.cursor_screen.x - BOX_W - 4.0;
if bx + total_w > bounds.width {
bx = (self.cursor_screen.x - total_w - 4.0).max(0.0);
}
if by + BOX_H > bounds.height {
by = self.cursor_screen.y - BOX_H - 4.0;
by = (self.cursor_screen.y - BOX_H - 4.0).max(0.0);
}
let bg = canvas::Path::rectangle(
Point { x: bx, y: by },
Size {
width: BOX_W,
height: BOX_H,
},
);
frame.fill(
&bg,
Color {
r: 0.05,
g: 0.05,
b: 0.12,
a: 0.85,
},
);
frame.stroke(
&bg,
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.label.clone(),
position: Point {
x: bx + PAD,
y: by + PAD,
},
color: Color {
r: 0.90,
g: 0.90,
b: 0.90,
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];
let rect = canvas::Path::rectangle(Point { x, y: by }, Size { width: w, height: BOX_H });
// Active box: brighter fill + accent border. Locked (typed)
// boxes get a warm border so it's clear they hold a fixed
// value rather than tracking the cursor.
let (fill, border) = if b.active {
(
Color { r: 0.12, g: 0.18, b: 0.30, a: 0.92 },
Color { r: 0.45, g: 0.70, b: 1.0, a: 1.0 },
)
} else if b.locked {
(
Color { r: 0.05, g: 0.05, b: 0.12, a: 0.85 },
Color { r: 0.95, g: 0.75, b: 0.30, a: 0.9 },
)
} else {
(
Color { r: 0.05, g: 0.05, b: 0.12, a: 0.85 },
Color { r: 0.35, g: 0.55, b: 0.90, a: 0.9 },
)
};
frame.fill(&rect, fill);
frame.stroke(
&rect,
canvas::Stroke::default()
.with_color(border)
.with_width(if b.active { 1.6 } else { 1.0 }),
);
frame.fill_text(canvas::Text {
content: texts[i].clone(),
position: Point { x: x + PAD, y: by + PAD },
color: Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 },
size: iced::Pixels(FONT_SIZE),
..Default::default()
});
x += w + GAP;
}
vec![frame.into_geometry()]
}