diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index e27326990..fe296a5d2 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -1539,13 +1539,14 @@ impl OpenCADStudio { self.restore_pre_cmd_tangent(); } } - // Focus the command-line input while a command is active; blur it when the command ends. - if self.tabs[i].active_cmd.is_some() { - self.focus_cmd_input() - } else { + // Keep the command-line input focused at all times — every typed + // character is meant to route there (the command processor reads + // its keystroke stream from this widget). When no command is + // running the ribbon tool button still has to visually deactivate. + if self.tabs[i].active_cmd.is_none() { self.ribbon.deactivate_tool(); - self.blur_cmd_input() } + self.focus_cmd_input() } /// Restore the tangent snap state that was in effect before the command started. diff --git a/src/app/mod.rs b/src/app/mod.rs index 248614336..3593c73cd 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -343,6 +343,14 @@ pub enum Message { CommandInput(String), CommandSubmit, Command(String), + /// Append one typed character to the command-line input from the + /// global key-press subscription. Used when the text-input widget + /// itself isn't focused (focus parked on viewport / button / etc.) + /// so typing still routes to the command line. + CommandAppendChar(String), + /// Pop the trailing character off the command-line input — backspace + /// counterpart to `CommandAppendChar`. + CommandBackspace, /// Recall previous command in history (↑ arrow key). CommandHistoryPrev, /// Recall next command in history (↓ arrow key). @@ -848,7 +856,8 @@ impl OpenCADStudio { crate::update_check::check_for_update(), Message::UpdateCheckResult, ); - (s, Task::batch([open_main, check_update])) + let focus_cmd = s.focus_cmd_input(); + (s, Task::batch([open_main, check_update, focus_cmd])) } } diff --git a/src/app/update.rs b/src/app/update.rs index 56bdb6229..2e5459a76 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -691,7 +691,7 @@ impl OpenCADStudio { Message::RibbonSelectTab(idx) => { self.ribbon.select(idx); - Task::none() + self.focus_cmd_input() } Message::RibbonToolClick { tool_id, event } => { @@ -727,7 +727,7 @@ impl OpenCADStudio { return Task::done(Message::ToggleLayers); } } - Task::none() + self.focus_cmd_input() } // ── Application menu ────────────────────────────────────────── @@ -759,7 +759,7 @@ impl OpenCADStudio { // fresh tab's defaults (ByLayer) instead of inheriting the // previous tab's last selection. self.sync_ribbon_from_selection(); - Task::none() + self.focus_cmd_input() } Message::TabSwitch(idx) => { @@ -772,7 +772,7 @@ impl OpenCADStudio { // if there is one), not the prior tab's choice. self.sync_ribbon_from_selection(); } - Task::none() + self.focus_cmd_input() } Message::TabClose(idx) => { @@ -807,11 +807,40 @@ impl OpenCADStudio { } Message::CommandInput(s) => { - // Any manual typing resets the history recall cursor. + // Space submits the current input the same way Enter does + // (CAD convention) — unless the active command is collecting + // free-form text (TEXT / MTEXT / DDEDIT / attribute value + // prompts) where Space must reach the buffer as a literal + // character. `wants_text_with_spaces()` flags those prompts. + let i = self.active_tab; + let allow_literal_space = self + .tabs[i] + .active_cmd + .as_ref() + .map(|c| c.wants_text_input() && c.wants_text_with_spaces()) + .unwrap_or(false); + if !allow_literal_space && s.ends_with(' ') { + self.command_line.input = s.trim_end_matches(' ').to_string(); + return Task::done(Message::CommandSubmit); + } self.command_line.input = s; Task::none() } + Message::CommandAppendChar(s) => { + // Filter out control characters — only push the typed + // glyph(s). `Tab`, etc. arrive as Named keys, not here. + if s.chars().all(|c| !c.is_control()) { + self.command_line.input.push_str(&s); + } + self.focus_cmd_input() + } + + Message::CommandBackspace => { + self.command_line.input.pop(); + self.focus_cmd_input() + } + Message::CommandHistoryPrev => { self.command_line.history_prev(); Task::none() @@ -1723,11 +1752,15 @@ impl OpenCADStudio { Message::ViewportLeftPress => { let i = self.active_tab; + // Clicking inside the viewport must not park focus on the + // viewport widget — typed characters need to keep flowing + // into the command-line input. Refocus on every exit path. + let refocus = self.focus_cmd_input(); let (p, vp_size) = { let sel = self.tabs[i].scene.selection.borrow(); let p = match sel.last_move_pos { Some(p) => p, - None => return Task::none(), + None => return refocus, }; (p, sel.vp_size) }; @@ -1744,7 +1777,7 @@ impl OpenCADStudio { if scene::hit_test(p.x, p.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX) .is_some() { - return Task::none(); + return refocus; } } @@ -1780,7 +1813,7 @@ impl OpenCADStudio { origin_world: world, last_world: world, }); - return Task::none(); + return refocus; } } } @@ -1791,7 +1824,7 @@ impl OpenCADStudio { sel.left_press_pos = Some(p); sel.left_press_time = Some(Instant::now()); sel.left_dragging = false; - Task::none() + refocus } Message::ViewportLeftRelease => { @@ -2443,7 +2476,7 @@ impl OpenCADStudio { ) { return Task::done(Message::ViewCubeSnap(region)); } - Task::none() + self.focus_cmd_input() } Message::WindowResized(w, h) => { @@ -3129,13 +3162,13 @@ impl OpenCADStudio { self.ribbon.select(idx); } self.command_line.push_output(&format!( - "Layout \"{new_name}\" oluşturuldu — MVIEW ile viewport ekleyin" + "Layout \"{new_name}\" created — use MVIEW to add a viewport" )); self.tabs[i].dirty = true; } Err(e) => self .command_line - .push_error(&format!("Layout oluşturulamadı: {e}")), + .push_error(&format!("Failed to create layout: {e}")), } Task::none() } @@ -3187,7 +3220,7 @@ impl OpenCADStudio { .any(|n| *n == new_name); if exists { self.command_line - .push_error(&format!("\"{}\" adı zaten kullanımda", new_name)); + .push_error(&format!("\"{}\" name already in use", new_name)); } else { self.push_undo_snapshot(i, "LAYOUT RENAME"); self.tabs[i].scene.rename_layout(&orig, &new_name); diff --git a/src/app/view.rs b/src/app/view.rs index c758e1468..b9b4d68c7 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -678,6 +678,11 @@ impl OpenCADStudio { { Some(Message::DeleteSelected) } + keyboard::Key::Named(keyboard::key::Named::Backspace) + if status == Status::Ignored => + { + Some(Message::CommandBackspace) + } keyboard::Key::Named(keyboard::key::Named::ArrowUp) => { Some(Message::CommandHistoryPrev) } @@ -718,6 +723,15 @@ impl OpenCADStudio { "v" => Some(Message::Command("PASTECLIP".to_string())), _ => None, }, + // Plain character keys (no ctrl) that nothing + // else consumed — route them into the command + // line input so typing always reaches it even + // when focus is parked on a button / viewport. + keyboard::Key::Character(c) + if !ctrl && status == Status::Ignored => + { + Some(Message::CommandAppendChar(c.to_string())) + } _ => None, } } @@ -730,11 +744,6 @@ impl OpenCADStudio { pub(super) fn focus_cmd_input(&self) -> Task { iced::widget::operation::focus(iced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID)) } - - pub(super) fn blur_cmd_input(&self) -> Task { - let op = iced::advanced::widget::operation::focusable::unfocus::(); - iced::advanced::widget::operate(op) - } } // ── Paper canvas ────────────────────────────────────────────────────────── diff --git a/src/command/mod.rs b/src/command/mod.rs index fb3f3eaca..3c7f58678 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -241,6 +241,19 @@ pub trait CadCommand: Send { false } + /// Returns `true` when the active text prompt expects free-form prose + /// that can legitimately contain whitespace (the body of a TEXT / + /// MTEXT / DDEDIT entity, an attribute default value, etc.). For + /// these prompts the command-line input must let `Space` be typed as + /// a literal character; for every other prompt `Space` submits the + /// input the same way `Enter` does. + /// + /// Default `false` — single-token prompts (option letters, numeric + /// radius, block name) do not embed spaces. + fn wants_text_with_spaces(&self) -> bool { + false + } + /// Called when the user submits text via the command line while `wants_text_input` is true. fn on_text_input(&mut self, _text: &str) -> Option { None diff --git a/src/modules/annotate/ddedit.rs b/src/modules/annotate/ddedit.rs index d6d040e58..645b15374 100644 --- a/src/modules/annotate/ddedit.rs +++ b/src/modules/annotate/ddedit.rs @@ -80,6 +80,10 @@ impl CadCommand for DdeditCommand { matches!(self.step, DdeditStep::EnterText { .. }) } + fn wants_text_with_spaces(&self) -> bool { + matches!(self.step, DdeditStep::EnterText { .. }) + } + fn on_text_input(&mut self, text: &str) -> Option { let (handle, current) = match &self.step { DdeditStep::EnterText { handle, current } => (*handle, current.clone()), diff --git a/src/modules/annotate/mtext.rs b/src/modules/annotate/mtext.rs index c4eb76aaa..9f9eeca89 100644 --- a/src/modules/annotate/mtext.rs +++ b/src/modules/annotate/mtext.rs @@ -65,6 +65,10 @@ impl CadCommand for MTextCommand { matches!(self.step, Step::WaitText(_)) } + fn wants_text_with_spaces(&self) -> bool { + matches!(self.step, Step::WaitText(_)) + } + fn on_text_input(&mut self, text: &str) -> Option { if let Step::WaitText(pos) = &self.step { if text.trim().is_empty() { diff --git a/src/modules/annotate/text.rs b/src/modules/annotate/text.rs index df4c78ccc..04f3823bc 100644 --- a/src/modules/annotate/text.rs +++ b/src/modules/annotate/text.rs @@ -65,6 +65,10 @@ impl CadCommand for TextCommand { matches!(self.step, Step::WaitText(_)) } + fn wants_text_with_spaces(&self) -> bool { + matches!(self.step, Step::WaitText(_)) + } + fn on_text_input(&mut self, text: &str) -> Option { if let Step::WaitText(pos) = &self.step { if text.trim().is_empty() { diff --git a/src/modules/home/draw/attdef.rs b/src/modules/home/draw/attdef.rs index 38b0ab750..c5088770a 100644 --- a/src/modules/home/draw/attdef.rs +++ b/src/modules/home/draw/attdef.rs @@ -65,6 +65,12 @@ impl CadCommand for AttdefCommand { !matches!(self.step, Step::Insertion { .. }) } + fn wants_text_with_spaces(&self) -> bool { + // Tag must be a single token (so disallow spaces there); the + // Prompt and Default value steps are free-form text. + matches!(self.step, Step::Prompt { .. } | Step::Default { .. }) + } + fn on_text_input(&mut self, text: &str) -> Option { match &self.step { Step::Tag => { diff --git a/src/modules/home/draw/hatch.rs b/src/modules/home/draw/hatch.rs index 2936e2104..46b815753 100644 --- a/src/modules/home/draw/hatch.rs +++ b/src/modules/home/draw/hatch.rs @@ -134,18 +134,18 @@ impl CadCommand for HatchCommand { match &self.mode { Mode::PickInside => { let miss = if self.missed { - " ⚠ Kapalı sınır bulunamadı." + " ⚠ No closed boundary found." } else { "" }; - format!("HATCH İçini tıklayın [S=Elle çiz]:{miss}") + format!("HATCH Pick internal point [S=Draw manually]:{miss}") } Mode::Manual => { if self.manual_pts.is_empty() { - "HATCH Sınır noktası 1:".into() + "HATCH Boundary point 1:".into() } else { format!( - "HATCH Nokta {} [Enter=onayla, ≥3 nokta]:", + "HATCH Point {} [Enter=accept, ≥3 points]:", self.manual_pts.len() + 1 ) } @@ -271,18 +271,18 @@ impl CadCommand for GradientCommand { match &self.mode { Mode::PickInside => { let miss = if self.missed { - " ⚠ Kapalı sınır bulunamadı." + " ⚠ No closed boundary found." } else { "" }; - format!("GRADIENT İçini tıklayın [S=Elle çiz]:{miss}") + format!("GRADIENT Pick internal point [S=Draw manually]:{miss}") } Mode::Manual => { if self.manual_pts.is_empty() { - "GRADIENT Sınır noktası 1:".into() + "GRADIENT Boundary point 1:".into() } else { format!( - "GRADIENT Nokta {} [Enter=onayla, ≥3 nokta]:", + "GRADIENT Point {} [Enter=accept, ≥3 points]:", self.manual_pts.len() + 1 ) } @@ -386,11 +386,11 @@ impl CadCommand for BoundaryCommand { fn prompt(&self) -> String { let miss = if self.missed { - " ⚠ Kapalı sınır bulunamadı." + " ⚠ No closed boundary found." } else { "" }; - format!("BOUNDARY İçini tıklayın:{miss}") + format!("BOUNDARY Pick internal point:{miss}") } fn on_point(&mut self, pt: Vec3) -> CmdResult { diff --git a/src/modules/home/modify/attedit.rs b/src/modules/home/modify/attedit.rs index 6ccdc7303..f609d0d27 100644 --- a/src/modules/home/modify/attedit.rs +++ b/src/modules/home/modify/attedit.rs @@ -82,6 +82,12 @@ impl CadCommand for AtteditCommand { } } + fn wants_text_with_spaces(&self) -> bool { + // Editing an attribute value — same free-form semantics as the + // text-content prompts in TEXT / MTEXT / DDEDIT. + self.wants_text_input() + } + fn on_text_input(&mut self, text: &str) -> Option { let Step::EditAttr { handle, attrs, idx } = &mut self.step else { return None; diff --git a/src/modules/insert/insert_block.rs b/src/modules/insert/insert_block.rs index 35d95d4fe..ea72e4611 100644 --- a/src/modules/insert/insert_block.rs +++ b/src/modules/insert/insert_block.rs @@ -116,6 +116,11 @@ impl CadCommand for InsertBlockCommand { matches!(self.step, Step::Name | Step::FillAttr { .. }) } + fn wants_text_with_spaces(&self) -> bool { + // Block names don't embed whitespace, but attribute values do. + matches!(self.step, Step::FillAttr { .. }) + } + fn on_text_input(&mut self, text: &str) -> Option { match &self.step { Step::Name => { diff --git a/src/modules/layout/mview.rs b/src/modules/layout/mview.rs index 72b146db5..4d586b764 100644 --- a/src/modules/layout/mview.rs +++ b/src/modules/layout/mview.rs @@ -43,9 +43,9 @@ impl CadCommand for MviewCommand { fn prompt(&self) -> String { if self.corner1.is_none() { - "MVIEW İlk köşeyi belirtin:".to_string() + "MVIEW Specify first corner:".to_string() } else { - "MVIEW Karşı köşeyi belirtin:".to_string() + "MVIEW Specify opposite corner:".to_string() } } diff --git a/src/modules/manage/mview.rs b/src/modules/manage/mview.rs index 8ffa4695a..c4f2ee410 100644 --- a/src/modules/manage/mview.rs +++ b/src/modules/manage/mview.rs @@ -39,9 +39,9 @@ impl CadCommand for MviewCommand { fn prompt(&self) -> String { if self.corner1.is_none() { - "MVIEW İlk köşeyi belirtin:".to_string() + "MVIEW Specify first corner:".to_string() } else { - "MVIEW Karşı köşeyi belirtin:".to_string() + "MVIEW Specify opposite corner:".to_string() } } diff --git a/src/scene/pipeline/viewcube.rs b/src/scene/pipeline/viewcube.rs index 2ba0685f2..3e7d3472d 100644 --- a/src/scene/pipeline/viewcube.rs +++ b/src/scene/pipeline/viewcube.rs @@ -909,11 +909,11 @@ pub fn build_geometry() -> (Vec, Vec) { &mut is, ); for &([sx, sy, sz], region) in &[ - ([1.0f32, 1.0, 1.0], CORNER_TBK_R), // sy=+1 = BACK yönü + ([1.0f32, 1.0, 1.0], CORNER_TBK_R), // sy=+1 → BACK direction ([-1.0, 1.0, 1.0], CORNER_TBK_L), ([1.0, 1.0, -1.0], CORNER_BBK_R), ([-1.0, 1.0, -1.0], CORNER_BBK_L), - ([1.0, -1.0, 1.0], CORNER_TPF_R), // sy=-1 = FRONT yönü + ([1.0, -1.0, 1.0], CORNER_TPF_R), // sy=-1 → FRONT direction ([-1.0, -1.0, 1.0], CORNER_TPF_L), ([1.0, -1.0, -1.0], CORNER_BTF_R), ([-1.0, -1.0, -1.0], CORNER_BTF_L), diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 864e2227c..dc8cdc4c6 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -359,7 +359,7 @@ pub(super) fn render_small<'a>( .height(ROW_H) .padding([4, 4]); - let arr_tip = format!("{} seçenekleri", cur_label); + let arr_tip = format!("{} options", cur_label); let arr_btn = button( container(text("▾").size(7).color(ARROW_COLOR)) .width(Fill) @@ -488,7 +488,7 @@ pub(super) fn render_large<'a>( .or_else(|| items.first().map(|(_, lbl, _)| *lbl)) .unwrap_or(label); let tip_text = format!("{}\nCommand: {}", cur_label, last); - let arr_tip = format!("{} seçenekleri", label); + let arr_tip = format!("{} options", label); let top_btn = button( column![