From 3144d1e5c72c430a449b14f2c112e0d29a9c9cf3 Mon Sep 17 00:00:00 2001 From: iamazy Date: Mon, 11 Aug 2025 19:39:45 +0800 Subject: [PATCH 1/3] feat: support ime --- crates/egui-term/src/display/mod.rs | 11 +++--- crates/egui-term/src/input/mod.rs | 27 +++++++------- crates/egui-term/src/view.rs | 55 ++++++++++++++++++----------- nxshell/src/app.rs | 5 --- 4 files changed, 56 insertions(+), 42 deletions(-) diff --git a/crates/egui-term/src/display/mod.rs b/crates/egui-term/src/display/mod.rs index 799a115..f1c17ca 100644 --- a/crates/egui-term/src/display/mod.rs +++ b/crates/egui-term/src/display/mod.rs @@ -46,10 +46,11 @@ impl TerminalView<'_> { .term_ctx .to_range() .is_some_and(|r| r.contains(indexed.point)); - let is_hovered_hyperlink = - self.term_ctx.hovered_hyperlink.as_ref().is_some_and(|r| { - r.contains(&indexed.point) && r.contains(&state.mouse_position) - }); + let is_hovered_hyperlink = self + .term_ctx + .hovered_hyperlink + .as_ref() + .is_some_and(|r| r.contains(&indexed.point) && r.contains(&state.mouse_point)); let is_text_cell = indexed.c != ' ' && indexed.c != '\t'; let x = layout_min.x + indexed.point.column.saturating_mul(cell_width as usize) as f32; @@ -112,6 +113,7 @@ impl TerminalView<'_> { cell_width / 2. }; + state.cursor_position = Some(Pos2::new(x, y)); shapes.push(Shape::Rect(RectShape::filled( Rect::from_min_size(Pos2::new(x, y), Vec2::new(cursor_width, cell_height)), CornerRadius::default(), @@ -191,6 +193,7 @@ impl TerminalView<'_> { if let Ok(data) = self.term_ctx.clipboard.get_contents() { self.term_ctx.write_data(data.into_bytes()); self.term_ctx.terminal.selection = None; + ui.ctx().request_repaint(); } ui.close(); } diff --git a/crates/egui-term/src/input/mod.rs b/crates/egui-term/src/input/mod.rs index e4f5bd3..7cac9c1 100644 --- a/crates/egui-term/src/input/mod.rs +++ b/crates/egui-term/src/input/mod.rs @@ -134,7 +134,7 @@ impl TerminalView<'_> { self.left_button_click(state, layout, position, modifiers, pressed) } PointerButton::Secondary => { - state.cursor_position = Some(position); + state.context_menu_position = Some(position); None } _ => None, @@ -149,7 +149,7 @@ impl TerminalView<'_> { modifiers: &Modifiers, pressed: bool, ) -> Option { - if state.cursor_position.is_some() { + if state.context_menu_position.is_some() { return None; } let terminal_mode = self.term_ctx.terminal.mode(); @@ -157,7 +157,7 @@ impl TerminalView<'_> { Some(InputAction::BackendCall(BackendCommand::MouseReport( MouseButton::LeftButton, *modifiers, - state.mouse_position, + state.mouse_point, pressed, ))) } else if pressed { @@ -189,7 +189,7 @@ impl TerminalView<'_> { *self.term_ctx.terminal.mode(), ) { Some(BindingAction::LinkOpen) => Some(InputAction::BackendCall( - BackendCommand::ProcessLink(LinkAction::Open, state.mouse_position), + BackendCommand::ProcessLink(LinkAction::Open, state.mouse_point), )), _ => None, } @@ -203,21 +203,22 @@ impl TerminalView<'_> { position: Pos2, modifiers: &Modifiers, ) -> Vec { - let cursor_x = position.x - layout.rect.min.x; - let cursor_y = position.y - layout.rect.min.y; + let mouse_x = position.x - layout.rect.min.x; + let mouse_y = position.y - layout.rect.min.y; - state.mouse_position = selection_point( - cursor_x, - cursor_y, + state.mouse_point = selection_point( + mouse_x, + mouse_y, self.term_ctx.size, self.term_ctx.terminal.grid().display_offset(), ); + state.mouse_position = Some(position); let mut actions = vec![]; // Handle command or selection update based on terminal mode and modifiers if state.is_dragged { if !self.term_ctx.selection_is_empty() { - if let Some(action) = self.update_selection_scrolling(cursor_y as i32) { + if let Some(action) = self.update_selection_scrolling(mouse_y as i32) { actions.push(action); } } @@ -232,11 +233,11 @@ impl TerminalView<'_> { InputAction::BackendCall(BackendCommand::MouseReport( MouseButton::LeftMove, *modifiers, - state.mouse_position, + state.mouse_point, true, )) } else { - InputAction::BackendCall(BackendCommand::SelectUpdate(cursor_x, cursor_y)) + InputAction::BackendCall(BackendCommand::SelectUpdate(mouse_x, mouse_y)) }; actions.push(cmd); @@ -245,7 +246,7 @@ impl TerminalView<'_> { // Handle link hover if applicable actions.push(InputAction::BackendCall(BackendCommand::ProcessLink( LinkAction::Hover, - state.mouse_position, + state.mouse_point, ))); actions diff --git a/crates/egui-term/src/view.rs b/crates/egui-term/src/view.rs index 5cb3637..9b441a2 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -9,11 +9,12 @@ use crate::types::Size; use alacritty_terminal::grid::{Dimensions, Scroll}; use alacritty_terminal::index::Point; use alacritty_terminal::vte::ansi::{Color, NamedColor}; -use egui::ImeEvent; +use egui::output::IMEOutput; use egui::Widget; use egui::{Context, Event}; use egui::{CursorIcon, Key}; use egui::{Id, Pos2}; +use egui::{ImeEvent, Rect}; use egui::{Response, Vec2}; #[derive(Clone, Default)] @@ -21,10 +22,10 @@ pub struct TerminalViewState { pub is_dragged: bool, pub scroll_pixels: f32, // for terminal - pub mouse_position: Point, + pub mouse_point: Point, + pub mouse_position: Option, + pub context_menu_position: Option, pub cursor_position: Option, - // ime_enabled: bool, - // ime_cursor_range: CursorRange, pub scrollbar_state: ScrollbarState, } @@ -80,13 +81,14 @@ impl Widget for TerminalView<'_> { } // context menu - if let Some(pos) = state.cursor_position { - if !out_of_terminal(pos, &layout) { + if let Some(pos) = state.context_menu_position { + if !out_of_terminal(pos, layout.rect) { self.context_menu(pos, &layout, ui); } } + if ui.input(|input_state| input_state.pointer.primary_clicked()) { - state.cursor_position = None; + state.context_menu_position = None; ui.close(); } @@ -97,6 +99,20 @@ impl Widget for TerminalView<'_> { .resize(&layout) .process_input(&mut state, &layout); + if let Some(pos) = state.mouse_position { + if !out_of_terminal(pos, layout.rect) { + if let Some(cur_pos) = state.cursor_position { + ui.ctx().output_mut(|output| { + let vec = Vec2::new(15., 15.); + output.ime = Some(IMEOutput { + rect: Rect::from_min_size(cur_pos, vec), + cursor_rect: Rect::from_min_size(cur_pos, vec), + }) + }); + } + } + } + let grid = term.term_ctx.terminal.grid_mut(); let total_lines = grid.total_lines() as f32; let display_offset = grid.display_offset() as f32; @@ -241,7 +257,7 @@ impl<'a> TerminalView<'a> { modifiers, pos, } => { - let new_pos = if out_of_terminal(pos, layout) { + let new_pos = if out_of_terminal(pos, layout.rect) { pos.clamp(layout.rect.min, layout.rect.max) } else { pos @@ -257,15 +273,17 @@ impl<'a> TerminalView<'a> { input_actions = self.mouse_move(state, layout, pos, &modifiers) } Event::Ime(event) => match event { - ImeEvent::Disabled => { - // state.ime_enabled = false; - } - ImeEvent::Enabled | ImeEvent::Preedit(_) => { - // state.ime_enabled = true; + ImeEvent::Preedit(text_mark) => { + if text_mark != "\n" && text_mark != "\r" { + // TODO: handle preedit + } } - ImeEvent::Commit(text) => { - input_actions.push(self.text_input(&text)); + ImeEvent::Commit(prediction) => { + if prediction != "\n" && prediction != "\r" { + input_actions.push(self.text_input(&prediction)); + } } + _ => {} }, _ => {} }; @@ -286,9 +304,6 @@ impl<'a> TerminalView<'a> { } } -fn out_of_terminal(pos: Pos2, layout: &Response) -> bool { - !(pos.x > layout.rect.min.x - && pos.x < layout.rect.max.x - && pos.y > layout.rect.min.y - && pos.y < layout.rect.max.y) +fn out_of_terminal(pos: Pos2, rect: Rect) -> bool { + !(pos.x > rect.min.x && pos.x < rect.max.x && pos.y > rect.min.y && pos.y < rect.max.y) } diff --git a/nxshell/src/app.rs b/nxshell/src/app.rs index 1f5e5f5..9476328 100644 --- a/nxshell/src/app.rs +++ b/nxshell/src/app.rs @@ -126,11 +126,6 @@ impl eframe::App for NxShell { ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { ui.label("Sessions"); }); - - // TODO: add close menu - // ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - // ui.label("Sessions"); - // }); }); self.search_sessions(ui); From 2e5df5e0a488f947416d4f1bae9299cc79203d3b Mon Sep 17 00:00:00 2001 From: iamazy Date: Mon, 11 Aug 2025 20:18:54 +0800 Subject: [PATCH 2/3] feat: support ime --- crates/egui-term/src/input/mod.rs | 8 ++++++-- crates/egui-term/src/view.rs | 16 ++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/egui-term/src/input/mod.rs b/crates/egui-term/src/input/mod.rs index 7cac9c1..dba8a30 100644 --- a/crates/egui-term/src/input/mod.rs +++ b/crates/egui-term/src/input/mod.rs @@ -4,7 +4,7 @@ use crate::{BindingAction, InputKind, TerminalView}; use alacritty_terminal::grid::Dimensions; use alacritty_terminal::selection::SelectionType; use alacritty_terminal::term::TermMode; -use egui::{Key, Modifiers, MouseWheelUnit, PointerButton, Pos2, Response, Vec2}; +use egui::{Key, Modifiers, MouseWheelUnit, PointerButton, Pos2, Rect, Response, Vec2}; use std::cmp::min; /// Minimum number of pixels at the bottom/top where selection scrolling is performed. @@ -160,7 +160,7 @@ impl TerminalView<'_> { state.mouse_point, pressed, ))) - } else if pressed { + } else if pressed && is_in_terminal(position, layout.rect) { state.is_dragged = true; Some(InputAction::BackendCall(start_select_command( layout, position, @@ -294,3 +294,7 @@ fn start_select_command(layout: &Response, cursor_position: Pos2) -> BackendComm cursor_position.y - layout.rect.min.y, ) } + +pub fn is_in_terminal(pos: Pos2, rect: Rect) -> bool { + pos.x > rect.min.x && pos.x < rect.max.x && pos.y > rect.min.y && pos.y < rect.max.y +} diff --git a/crates/egui-term/src/view.rs b/crates/egui-term/src/view.rs index 9b441a2..d1f6553 100644 --- a/crates/egui-term/src/view.rs +++ b/crates/egui-term/src/view.rs @@ -2,7 +2,7 @@ use crate::alacritty::{BackendCommand, TerminalContext}; use crate::bindings::Binding; use crate::bindings::{BindingAction, Bindings, InputKind}; use crate::font::TerminalFont; -use crate::input::InputAction; +use crate::input::{is_in_terminal, InputAction}; use crate::scroll_bar::{InteractiveScrollbar, ScrollbarState}; use crate::theme::TerminalTheme; use crate::types::Size; @@ -82,7 +82,7 @@ impl Widget for TerminalView<'_> { // context menu if let Some(pos) = state.context_menu_position { - if !out_of_terminal(pos, layout.rect) { + if is_in_terminal(pos, layout.rect) { self.context_menu(pos, &layout, ui); } } @@ -100,7 +100,7 @@ impl Widget for TerminalView<'_> { .process_input(&mut state, &layout); if let Some(pos) = state.mouse_position { - if !out_of_terminal(pos, layout.rect) { + if is_in_terminal(pos, layout.rect) { if let Some(cur_pos) = state.cursor_position { ui.ctx().output_mut(|output| { let vec = Vec2::new(15., 15.); @@ -257,10 +257,10 @@ impl<'a> TerminalView<'a> { modifiers, pos, } => { - let new_pos = if out_of_terminal(pos, layout.rect) { - pos.clamp(layout.rect.min, layout.rect.max) - } else { + let new_pos = if is_in_terminal(pos, layout.rect) { pos + } else { + pos.clamp(layout.rect.min, layout.rect.max) }; if let Some(action) = @@ -303,7 +303,3 @@ impl<'a> TerminalView<'a> { self } } - -fn out_of_terminal(pos: Pos2, rect: Rect) -> bool { - !(pos.x > rect.min.x && pos.x < rect.max.x && pos.y > rect.min.y && pos.y < rect.max.y) -} From 52977fb60ba1e5b5628040d06ec0e3ed67867dec Mon Sep 17 00:00:00 2001 From: iamazy Date: Mon, 11 Aug 2025 20:25:31 +0800 Subject: [PATCH 3/3] feat: support ime --- crates/egui-term/src/display/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/egui-term/src/display/mod.rs b/crates/egui-term/src/display/mod.rs index f1c17ca..188bf9b 100644 --- a/crates/egui-term/src/display/mod.rs +++ b/crates/egui-term/src/display/mod.rs @@ -193,7 +193,6 @@ impl TerminalView<'_> { if let Ok(data) = self.term_ctx.clipboard.get_contents() { self.term_ctx.write_data(data.into_bytes()); self.term_ctx.terminal.selection = None; - ui.ctx().request_repaint(); } ui.close(); }