From 18462517f0d09453f66bcc8179e5626a8621dbe0 Mon Sep 17 00:00:00 2001 From: KENZ Date: Sun, 27 Apr 2025 12:54:46 +0900 Subject: [PATCH 1/7] Add minimal support CJK input method to terminal box excepts IME cursor positioning which is a bit tricky --- src/terminal_box.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/terminal_box.rs b/src/terminal_box.rs index e070498..8eac907 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -13,6 +13,7 @@ use cosmic::{ iced::core::{ Border, Shell, clipboard::Clipboard, + input_method::{self, InputMethod}, keyboard::key::Named, layout::{self, Layout}, renderer::{self, Quad, Renderer as _}, @@ -233,6 +234,18 @@ where self.disabled = disabled; self } + + fn input_method<'b>(&self, state: &'b State, layout: Layout<'_>) -> InputMethod<&'b str> { + if !state.is_focused { + return InputMethod::Disabled; + } + + InputMethod::Enabled { + cursor: Rectangle::default(), + purpose: input_method::Purpose::Normal, + preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), + } + } } pub fn terminal_box<'a, Message>( @@ -879,6 +892,7 @@ where shell.request_redraw(); } } + shell.request_input_method(&self.input_method(state, layout)); } cosmic::iced::window::Event::Unfocused => { state.is_focused = false; @@ -1160,6 +1174,30 @@ where } } } + Event::InputMethod(event) => match event { + input_method::Event::Opened | input_method::Event::Closed => { + state.preedit = matches!(event, input_method::Event::Opened).then(|| { + let preedit = input_method::Preedit::new(); + preedit + }); + } + input_method::Event::Preedit(content, selection) => { + if state.is_focused { + let metrics = terminal.with_buffer(|buffer| buffer.metrics()); + state.preedit = Some(input_method::Preedit { + content: content.to_owned(), + selection: selection.clone(), + text_size: Some(metrics.font_size.into()), + }) + } + } + input_method::Event::Commit(text) => { + if state.is_focused { + terminal.paste(text.to_string()); + shell.capture_event(); + } + } + }, Event::Mouse(MouseEvent::ButtonPressed(button)) => { if let Some(p) = cursor_position.position_in(layout.bounds()) { let x = p.x - self.padding.left; @@ -1867,6 +1905,7 @@ pub struct State { scroll_pixels: f32, scrollbar_rect: Cell>, autoscroll: DragAutoscroll, + preedit: Option, } impl State { @@ -1880,6 +1919,7 @@ impl State { scroll_pixels: 0.0, scrollbar_rect: Cell::new(Rectangle::default()), autoscroll: DragAutoscroll::new(AUTOSCROLL_INTERVAL), + preedit: None, } } } From da57b9c12dcf7f3480247c5a7ac1df5e1a19deff Mon Sep 17 00:00:00 2001 From: KENZ Date: Fri, 2 May 2025 07:01:55 +0900 Subject: [PATCH 2/7] Estimate preedit position without calling layout_runs() --- src/terminal_box.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 8eac907..ed7f935 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -235,13 +235,31 @@ where self } - fn input_method<'b>(&self, state: &'b State, layout: Layout<'_>) -> InputMethod<&'b str> { + fn input_method<'b>( + &self, + state: &'b State, + layout: Layout<'_>, + terminal: &std::sync::MutexGuard<'_, Terminal>, + ) -> InputMethod<&'b str> { if !state.is_focused { return InputMethod::Disabled; } + let view_position = layout.position() + [self.padding.left, self.padding.top].into(); + + // Draw cursor + let cursor = terminal.term.lock().renderable_content().cursor; + let col = cursor.point.column.0; + let line = cursor.point.line.0; + let width = terminal.size().cell_width; + let height = terminal.size().cell_height; + let bottom_left = view_position + + Vector::new( + (col as f32 * width).floor(), + ((line + 1) as f32 * height).floor(), + ); InputMethod::Enabled { - cursor: Rectangle::default(), + cursor: Rectangle::new(bottom_left, Size::default()), purpose: input_method::Purpose::Normal, preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), } @@ -892,7 +910,7 @@ where shell.request_redraw(); } } - shell.request_input_method(&self.input_method(state, layout)); + shell.request_input_method(&self.input_method(state, layout, &terminal)); } cosmic::iced::window::Event::Unfocused => { state.is_focused = false; From 888b1b5f6b99f623fd881d2e347588f5cb532953 Mon Sep 17 00:00:00 2001 From: KENZ Date: Sun, 4 May 2025 11:21:43 +0900 Subject: [PATCH 3/7] Handle wide character widths by using glyph width to detect wide chars --- src/terminal_box.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/terminal_box.rs b/src/terminal_box.rs index ed7f935..26dee49 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -249,17 +249,25 @@ where // Draw cursor let cursor = terminal.term.lock().renderable_content().cursor; - let col = cursor.point.column.0; + let mut col = cursor.point.column.0; let line = cursor.point.line.0; let width = terminal.size().cell_width; - let height = terminal.size().cell_height; - let bottom_left = view_position - + Vector::new( - (col as f32 * width).floor(), - ((line + 1) as f32 * height).floor(), - ); + let mut bottom_left = Vector::::ZERO; + terminal.with_buffer(|buffer| { + let layout = buffer.layout_runs().nth(line as usize).unwrap(); + for glyph in layout.glyphs { + bottom_left.x += glyph.w; + let ch_width = if glyph.w > width { 2 } else { 1 }; + if ch_width > col { + break; + } + col -= ch_width; + } + bottom_left.y = layout.line_top + layout.line_height; + }); + InputMethod::Enabled { - cursor: Rectangle::new(bottom_left, Size::default()), + cursor: Rectangle::new(view_position + bottom_left, Size::default()), purpose: input_method::Purpose::Normal, preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), } From 768ee43d5aef10790e3a276ece280fe019d78afc Mon Sep 17 00:00:00 2001 From: KENZ Date: Thu, 26 Feb 2026 01:05:30 +0900 Subject: [PATCH 4/7] Specify the line height to the IME cursor height --- src/terminal_box.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 26dee49..2de9e68 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -252,22 +252,24 @@ where let mut col = cursor.point.column.0; let line = cursor.point.line.0; let width = terminal.size().cell_width; - let mut bottom_left = Vector::::ZERO; + let mut cursor_position = Vector::::ZERO; + let mut line_height = 0.0; terminal.with_buffer(|buffer| { let layout = buffer.layout_runs().nth(line as usize).unwrap(); for glyph in layout.glyphs { - bottom_left.x += glyph.w; + cursor_position.x += glyph.w; let ch_width = if glyph.w > width { 2 } else { 1 }; if ch_width > col { break; } col -= ch_width; } - bottom_left.y = layout.line_top + layout.line_height; + cursor_position.y = layout.line_top; + line_height = layout.line_height; }); InputMethod::Enabled { - cursor: Rectangle::new(view_position + bottom_left, Size::default()), + cursor: Rectangle::new(view_position + cursor_position, Size::new(1.0, line_height)), purpose: input_method::Purpose::Normal, preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), } From c22d1ccc28b6e9ecc68f688e311abefedab7352f Mon Sep 17 00:00:00 2001 From: KENZ Date: Thu, 25 Dec 2025 22:59:39 +0900 Subject: [PATCH 5/7] fix: menu doesn't show by unfocusing when clicking outside of the terminal_box --- src/terminal_box.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 2de9e68..8138e0a 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -1380,6 +1380,8 @@ where } shell.capture_event(); } + } else { + state.is_focused = false; } } Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => { From 1963929e052a84e3812f7ea5bbcea8faeace6e89 Mon Sep 17 00:00:00 2001 From: KENZ Date: Sat, 11 Apr 2026 10:23:51 +0900 Subject: [PATCH 6/7] fix: context menu by disabling IME when the widget has context menu --- src/terminal_box.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 8138e0a..7d1ad11 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -244,6 +244,9 @@ where if !state.is_focused { return InputMethod::Disabled; } + if let Some(_) = self.context_menu { + return InputMethod::Disabled; + } let view_position = layout.position() + [self.padding.left, self.padding.top].into(); From d413fdb593cd2e3bb26c6177a944e7a4c26fcea8 Mon Sep 17 00:00:00 2001 From: KENZ Date: Wed, 22 Apr 2026 05:38:52 +0900 Subject: [PATCH 7/7] fix: clippy lint --- src/terminal_box.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 7d1ad11..9be9f0d 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -244,7 +244,7 @@ where if !state.is_focused { return InputMethod::Disabled; } - if let Some(_) = self.context_menu { + if self.context_menu.is_some() { return InputMethod::Disabled; } @@ -1207,10 +1207,8 @@ where } Event::InputMethod(event) => match event { input_method::Event::Opened | input_method::Event::Closed => { - state.preedit = matches!(event, input_method::Event::Opened).then(|| { - let preedit = input_method::Preedit::new(); - preedit - }); + state.preedit = matches!(event, input_method::Event::Opened) + .then(input_method::Preedit::new); } input_method::Event::Preedit(content, selection) => { if state.is_focused {