From 51e6c4b35922046b70bd43ab32c8ee4a8ca3d92d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 8 Apr 2026 00:12:09 +0300 Subject: [PATCH] feat: Dynamic Input overlay (DYNMODE / F12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ROADMAP.md | 2 +- src/app/document.rs | 3 ++ src/app/mod.rs | 5 +++ src/app/update.rs | 2 ++ src/app/view.rs | 28 ++++++++++++++- src/ui/overlay.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++ src/ui/statusbar.rs | 5 +++ 7 files changed, 127 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8d684ac50..1578fd5a6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -197,7 +197,7 @@ Underlay (PDF/DWF/DGN) | Named UCS kaydetme | ⬜ | | VPORTS (viewport bölme) | ⬜ | | Nesne snap izleme (Object Snap Tracking) | ⬜ | -| Dynamic Input overlay | ⬜ | +| Dynamic Input overlay | ✅ F12 toggle, absolute XY + relative dist/angle | --- diff --git a/src/app/document.rs b/src/app/document.rs index 736dc8865..8f7a985da 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -8,6 +8,7 @@ use acadrust::{CadDocument, Handle}; use acadrust::tables::Ucs; use crate::linetypes; use std::path::PathBuf; +use iced; // ── Per-document tab state ───────────────────────────────────────────────── @@ -27,6 +28,7 @@ pub(super) struct DocumentTab { pub(super) wireframe: bool, pub(super) visual_style: String, pub(super) last_cursor_world: glam::Vec3, + pub(super) last_cursor_screen: iced::Point, pub(super) history: HistoryState, pub(super) active_layer: String, /// Currently active UCS. `None` means WCS (identity transform). @@ -57,6 +59,7 @@ impl DocumentTab { wireframe: false, visual_style: "Shaded".into(), last_cursor_world: glam::Vec3::ZERO, + last_cursor_screen: iced::Point::ORIGIN, history: HistoryState::default(), active_layer: "0".to_string(), active_ucs: None, diff --git a/src/app/mod.rs b/src/app/mod.rs index 33eb21903..916e4fdef 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -49,6 +49,8 @@ pub(super) struct H7CAD { polar_increment_deg: f32, /// Show grid lines in the viewport (F7). show_grid: bool, + /// Dynamic input overlay (F12): show coordinate tooltip near cursor. + dyn_input: bool, /// Show the UCS icon in the bottom-left corner of model space (UCSICON). show_ucs_icon: bool, /// Last point committed by a drawing command — used as ortho/polar base. @@ -230,6 +232,8 @@ pub enum Message { TogglePolar, /// Set polar tracking angle increment (right-click POLAR button). SetPolarAngle(f32), + /// Toggle dynamic input overlay (F12). + ToggleDynInput, /// Toggle an individual snap mode (from popup row click). ToggleSnap(crate::snap::SnapType), /// Open / close the OSNAP popup (▾ arrow click). @@ -450,6 +454,7 @@ impl H7CAD { polar_mode: false, polar_increment_deg: 45.0, show_grid: false, + dyn_input: true, show_ucs_icon: true, last_point: None, layer_window: None, diff --git a/src/app/update.rs b/src/app/update.rs index 00e036acd..7912ac03b 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1031,6 +1031,7 @@ impl H7CAD { pt }; self.tabs[i].last_cursor_world = effective; + self.tabs[i].last_cursor_screen = p; let mut previews = if needs_entity { let hover_handle = @@ -1658,6 +1659,7 @@ impl H7CAD { if self.polar_mode { self.ortho_mode = false; } Task::none() } + Message::ToggleDynInput => { self.dyn_input ^= true; Task::none() } Message::SetPolarAngle(deg) => { self.polar_increment_deg = deg; self.polar_mode = true; diff --git a/src/app/view.rs b/src/app/view.rs index b237381b1..a045e7c24 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -135,7 +135,26 @@ impl H7CAD { .unwrap_or(Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0 }) }; - let viewport_stack = stack![ + // Dynamic input overlay — shown when a command is active and DYN is on. + let dyn_input_overlay: Option> = + if self.dyn_input && tab.active_cmd.is_some() { + let w = tab.last_cursor_world; + let label = if let Some(base) = self.last_point { + // Show relative distance + angle when we have a base point. + let dx = (w.x - base.x) as f64; + let dy = (w.z - base.z) as f64; + let dist = (dx * dx + dy * dy).sqrt(); + let ang = dy.atan2(dx).to_degrees(); + format!("d={:.3} <{:.1}°", dist, ang) + } else { + format!("X:{:.3} Y:{:.3}", w.x, w.z) + }; + Some(overlay::dynamic_input_overlay(tab.last_cursor_screen, label)) + } else { + None + }; + + let mut viewport_stack = stack![ container(viewport_3d) .style(move |_: &Theme| container::Style { background: Some(Background::Color(bg_color)), @@ -151,6 +170,9 @@ impl H7CAD { ] .width(Fill) .height(Fill); + if let Some(dyn_ol) = dyn_input_overlay { + viewport_stack = viewport_stack.push(dyn_ol); + } let center_stack = iced::widget::stack![ row![tab.properties.view(), viewport_stack] @@ -179,6 +201,7 @@ impl H7CAD { self.polar_mode, self.polar_increment_deg, self.show_grid, + self.dyn_input, tab.scene.layout_names(), tab.scene.current_layout.clone(), self.layout_rename_state.as_ref(), @@ -342,6 +365,9 @@ impl H7CAD { keyboard::Key::Named(keyboard::key::Named::F10) => { Some(Message::TogglePolar) } + keyboard::Key::Named(keyboard::key::Named::F12) => { + Some(Message::ToggleDynInput) + } keyboard::Key::Character(c) if ctrl => match c.as_str() { "n" => Some(Message::ClearScene), "o" => Some(Message::OpenFile), diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 2c900532c..8adac9c0c 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -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 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 { + 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()] + } +} diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index eb90635d6..47d068c42 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -28,6 +28,7 @@ impl StatusBar { polar_mode: bool, polar_increment_deg: f32, show_grid: bool, + dyn_input: bool, layouts: Vec, 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),