feat: Dynamic Input overlay (DYNMODE / F12)

- overlay.rs: dynamic_input_overlay() canvas widget draws a semi-transparent
  coordinate tooltip near the cursor; shows X/Y in absolute mode or
  dist/angle relative to last_point when a command base point exists
- statusbar: DYN toggle pill with F12 tooltip (calls ToggleDynInput)
- app/mod.rs: dyn_input: bool field (default true), ToggleDynInput message
- view.rs: builds DYN label and pushes overlay into viewport_stack when
  dyn_input=true and a command is active
- document.rs: last_cursor_screen: iced::Point — updated on every ViewportMove
- ROADMAP.md: Dynamic Input overlay marked ✅

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-08 00:12:09 +03:00
1 parent f80d694818
commit 51e6c4b359
7 files changed
+127 -2

No files matched your search

+84
View File
@@ -849,3 +849,87 @@ fn draw_ucs_icon(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle) {
let circle = canvas::Path::circle(icon_origin, 3.0);
frame.fill(&circle, Color { r: 0.9, g: 0.9, b: 0.9, a: 0.9 });
}
// ── Dynamic Input overlay ─────────────────────────────────────────────────
/// Draw a small coordinate / distance-angle tooltip near the cursor.
///
/// `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> {
canvas(DynInputCanvas { cursor_screen, label })
.width(Length::Fill)
.height(Length::Fill)
.into()
}
struct DynInputCanvas {
cursor_screen: Point,
label: String,
}
impl canvas::Program<Message> for DynInputCanvas {
type State = ();
fn mouse_interaction(
&self,
_state: &(),
_bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> mouse::Interaction {
mouse::Interaction::None
}
fn draw(
&self,
_state: &(),
renderer: &iced::Renderer,
_theme: &Theme,
bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
// Offset the box 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 FONT_SIZE: f32 = 11.0;
const BOX_W: f32 = 160.0;
const BOX_H: f32 = FONT_SIZE + PAD * 2.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 by + BOX_H > bounds.height { by = self.cursor_screen.y - BOX_H - 4.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()
});
vec![frame.into_geometry()]
}
}
+5
View File
@@ -28,6 +28,7 @@ impl StatusBar {
polar_mode: bool,
polar_increment_deg: f32,
show_grid: bool,
dyn_input: bool,
layouts: Vec<String>,
current_layout: String,
// If `Some((original, edit_value))`, the named tab shows a text input.
@@ -82,6 +83,10 @@ impl StatusBar {
"Orthogonal Mode\nF8"
),
polar_pill(polar_mode, polar_increment_deg),
tip(
toggle_pill("DYN", dyn_input, Message::ToggleDynInput),
"Dynamic Input\nF12"
),
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
status_pill(space_label),
status_pill(scale_label),