input: Fix cursor and IME popup position when IME typing. (#889)

- Fix cursor position when IME typing.
- Fix selection range when canceled IME typing.
- Fix IME popup position to follow the cursor position.
- Fix IME text to render with underline style.

Close #55

<img width="1231" alt="image"
src="https://github.com/user-attachments/assets/c746703f-23de-4b3b-bd5b-210a8447eeba"
/>
This commit is contained in:
Jason Lee 2025-05-23 15:00:24 +08:00 committed by GitHub
parent 2c4a28a6da
commit 8dc9666214
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 142 additions and 60 deletions

View file

@ -6,14 +6,32 @@ use crate::highlighter::Highlighter;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct LineHighlightStyle { pub(crate) struct LineHighlightStyle {
offset: usize,
pub(crate) styles: Rc<Vec<(Range<usize>, HighlightStyle)>>, pub(crate) styles: Rc<Vec<(Range<usize>, HighlightStyle)>>,
} }
impl LineHighlightStyle { impl LineHighlightStyle {
pub(super) fn to_run(&self, text_style: &TextStyle) -> Vec<TextRun> { pub(super) fn to_run(
&self,
text_style: &TextStyle,
marked_range: &Option<Range<usize>>,
marked_run: &TextRun,
) -> Vec<TextRun> {
self.styles self.styles
.iter() .iter()
.map(|(range, style)| text_style.clone().highlight(*style).to_run(range.len())) .map(|(range, style)| {
let mut run = text_style.clone().highlight(*style).to_run(range.len());
if let Some(marked_range) = marked_range {
if self.offset + range.start >= marked_range.start
&& self.offset + range.end <= marked_range.end
{
run.color = marked_run.color;
run.strikethrough = marked_run.strikethrough;
run.underline = marked_run.underline;
}
}
run
})
// Add last `\n` Run with len 1 // Add last `\n` Run with len 1
.chain(std::iter::once(text_style.clone().to_run(1))) .chain(std::iter::once(text_style.clone().to_run(1)))
.collect() .collect()
@ -52,21 +70,29 @@ impl CodeHighlighter {
} }
let mut lines = vec![]; let mut lines = vec![];
let mut offset = 0;
let mut new_cache = HashMap::new(); let mut new_cache = HashMap::new();
for line in text.split('\n') { for line in text.split('\n') {
let cache_key = gpui::hash(&line); let cache_key = gpui::hash(&line);
// cache hit // cache hit
if let Some(line_style) = self.cache.get(&cache_key) { if let Some(line_style) = self.cache.get(&cache_key) {
new_cache.insert(cache_key, line_style.clone()); let new_style = LineHighlightStyle {
lines.push(line_style.clone()); offset,
styles: line_style.styles.clone(),
};
new_cache.insert(cache_key, new_style.clone());
lines.push(new_style);
} else { } else {
// cache miss // cache miss
let styles = Rc::new(self.highlighter.highlight(line)); let styles = Rc::new(self.highlighter.highlight(line));
let line_style = LineHighlightStyle { styles }; let line_style = LineHighlightStyle { offset, styles };
new_cache.insert(cache_key, line_style.clone()); new_cache.insert(cache_key, line_style.clone());
lines.push(line_style); lines.push(line_style);
} }
// +1 for '\n'
offset += line.len() + 1;
} }
// Ensure to recreate cache to remove unused caches. // Ensure to recreate cache to remove unused caches.

View file

@ -1,5 +1,3 @@
use std::rc::Rc;
use gpui::{ use gpui::{
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler, fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path, Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path,
@ -58,7 +56,11 @@ impl TextElement {
cx: &mut App, cx: &mut App,
) -> (Option<PaintQuad>, Point<Pixels>, usize) { ) -> (Option<PaintQuad>, Point<Pixels>, usize) {
let input = self.input.read(cx); let input = self.input.read(cx);
let selected_range = &input.selected_range; let mut selected_range = input.selected_range.clone();
if let Some(marked_range) = &input.marked_range {
selected_range = marked_range.end..marked_range.end;
}
let cursor_offset = input.cursor_offset(); let cursor_offset = input.cursor_offset();
let mut current_line_index = 0; let mut current_line_index = 0;
let mut scroll_offset = input.scroll_handle.offset(); let mut scroll_offset = input.scroll_handle.offset();
@ -193,7 +195,12 @@ impl TextElement {
cx: &mut App, cx: &mut App,
) -> Option<Path<Pixels>> { ) -> Option<Path<Pixels>> {
let input = self.input.read(cx); let input = self.input.read(cx);
let selected_range = &input.selected_range; let mut selected_range = input.selected_range.clone();
if let Some(marked_range) = &input.marked_range {
if !marked_range.is_empty() {
selected_range = marked_range.end..marked_range.end;
}
}
if selected_range.is_empty() { if selected_range.is_empty() {
return None; return None;
} }
@ -322,13 +329,6 @@ impl TextElement {
_ => None, _ => None,
}) })
} }
fn highlighter(&self, cx: &App) -> Option<Rc<crate::highlighter::Highlighter<'static>>> {
match &self.input.read(cx).mode {
InputMode::CodeEditor { highlighter, .. } => Some(highlighter.highlighter.clone()),
_ => None,
}
}
} }
pub(super) struct PrepaintState { pub(super) struct PrepaintState {
@ -513,8 +513,31 @@ impl Element for TextElement {
underline: None, underline: None,
strikethrough: None, strikethrough: None,
}; };
let marked_run = TextRun {
len: 0,
font: style.font(),
color: text_color,
background_color: None,
underline: Some(UnderlineStyle {
thickness: px(1.),
color: Some(text_color),
wavy: false,
}),
strikethrough: None,
};
let runs = if let Some(marked_range) = input.marked_range.as_ref() { let runs = if !is_empty {
if let Some(highlight_lines) = highlight_lines {
let mut runs = vec![];
for style in highlight_lines {
runs.extend(style.to_run(&text_style, &input.marked_range, &marked_run));
}
runs.into_iter().filter(|run| run.len > 0).collect()
} else {
vec![run]
}
} else if let Some(marked_range) = &input.marked_range {
// IME marked text
vec![ vec![
TextRun { TextRun {
len: marked_range.start, len: marked_range.start,
@ -522,11 +545,7 @@ impl Element for TextElement {
}, },
TextRun { TextRun {
len: marked_range.end - marked_range.start, len: marked_range.end - marked_range.start,
underline: Some(UnderlineStyle { underline: marked_run.underline,
color: Some(run.color),
thickness: px(1.0),
wavy: false,
}),
..run.clone() ..run.clone()
}, },
TextRun { TextRun {
@ -537,16 +556,6 @@ impl Element for TextElement {
.into_iter() .into_iter()
.filter(|run| run.len > 0) .filter(|run| run.len > 0)
.collect() .collect()
} else if !is_empty {
if let Some(highlight_lines) = highlight_lines {
let mut runs = vec![];
for style in highlight_lines {
runs.extend(style.to_run(&text_style));
}
runs.into_iter().filter(|run| run.len > 0).collect()
} else {
vec![run]
}
} else { } else {
vec![run] vec![run]
}; };
@ -667,11 +676,6 @@ impl Element for TextElement {
} }
}); });
// Paint selections
if let Some(path) = prepaint.selection_path.take() {
window.paint_path(path, cx.theme().selection);
}
// Paint multi line text // Paint multi line text
let line_height = window.line_height(); let line_height = window.line_height();
let origin = bounds.origin; let origin = bounds.origin;
@ -690,10 +694,14 @@ impl Element for TextElement {
for (ix, line) in line_numbers.iter().enumerate() { for (ix, line) in line_numbers.iter().enumerate() {
let p = point(origin.x, origin.y + offset_y); let p = point(origin.x, origin.y + offset_y);
let line_size = line.size(line_height); let line_size = line.size(line_height);
// Paint the current line background // Paint the current line background
if prepaint.current_line_index == ix { if prepaint.current_line_index == ix {
if let Some(bg_color) = self if let Some(bg_color) = self
.highlighter(cx) .input
.read(cx)
.mode
.highlighter()
.and_then(|h| h.theme.settings().line_highlight) .and_then(|h| h.theme.settings().line_highlight)
.map(crate::highlighter::color_to_hsla) .map(crate::highlighter::color_to_hsla)
{ {
@ -709,6 +717,12 @@ impl Element for TextElement {
} }
} }
// Paint selections
if let Some(path) = prepaint.selection_path.take() {
window.paint_path(path, cx.theme().selection);
}
// Paint text
let mut offset_y = px(0.); let mut offset_y = px(0.);
for line in prepaint.lines.iter() { for line in prepaint.lines.iter() {
let p = point(origin.x + prepaint.line_number_width, origin.y + offset_y); let p = point(origin.x + prepaint.line_number_width, origin.y + offset_y);

View file

@ -1,3 +1,5 @@
use std::rc::Rc;
use gpui::{DefiniteLength, SharedString}; use gpui::{DefiniteLength, SharedString};
use super::code_highlighter::CodeHighlighter; use super::code_highlighter::CodeHighlighter;
@ -148,6 +150,13 @@ impl InputMode {
_ => None, _ => None,
} }
} }
pub(super) fn highlighter(&self) -> Option<Rc<crate::highlighter::Highlighter<'static>>> {
match &self {
InputMode::CodeEditor { highlighter, .. } => Some(highlighter.highlighter.clone()),
_ => None,
}
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -225,6 +225,7 @@ pub struct InputState {
pub(super) selection_reversed: bool, pub(super) selection_reversed: bool,
/// The index of the current line, zero-based. /// The index of the current line, zero-based.
pub(super) current_line_index: Option<usize>, pub(super) current_line_index: Option<usize>,
/// The marked range is the temporary insert text on IME typing.
pub(super) marked_range: Option<Range<usize>>, pub(super) marked_range: Option<Range<usize>>,
pub(super) last_layout: Option<SmallVec<[WrappedLine; 1]>>, pub(super) last_layout: Option<SmallVec<[WrappedLine; 1]>>,
pub(super) last_cursor_offset: Option<usize>, pub(super) last_cursor_offset: Option<usize>,
@ -1302,6 +1303,9 @@ impl InputState {
} }
pub(super) fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) {
if self.marked_range.is_some() {
self.unmark_text(window, cx);
}
if self.selected_range.len() > 0 { if self.selected_range.len() > 0 {
return self.unselect(window, cx); return self.unselect(window, cx);
} }
@ -1319,6 +1323,14 @@ impl InputState {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
// If there have IME marked range and is empty (Means pressed Esc to abort IME typing)
// Clear the marked range.
if let Some(marked_range) = &self.marked_range {
if marked_range.len() == 0 {
self.marked_range = None;
}
}
self.selecting = true; self.selecting = true;
let offset = self.index_for_mouse_position(event.position, window, cx); let offset = self.index_for_mouse_position(event.position, window, cx);
// Double click to select word // Double click to select word
@ -1468,6 +1480,10 @@ impl InputState {
} }
pub(super) fn cursor_offset(&self) -> usize { pub(super) fn cursor_offset(&self) -> usize {
if let Some(marked_range) = &self.marked_range {
return marked_range.end;
}
if self.selection_reversed { if self.selection_reversed {
self.selected_range.start self.selected_range.start
} else { } else {
@ -1938,6 +1954,7 @@ impl EntityInputHandler for InputState {
cx.notify(); cx.notify();
} }
/// Mark text is the IME temporary insert on typing.
fn replace_and_mark_text_in_range( fn replace_and_mark_text_in_range(
&mut self, &mut self,
range_utf16: Option<Range<usize>>, range_utf16: Option<Range<usize>>,
@ -1963,12 +1980,20 @@ impl EntityInputHandler for InputState {
self.push_history(&range, new_text, window, cx); self.push_history(&range, new_text, window, cx);
self.text = pending_text; self.text = pending_text;
self.marked_range = Some(range.start..range.start + new_text.len()); self.text_wrapper.update(self.text.clone(), false, cx);
self.selected_range = new_selected_range_utf16 if new_text.is_empty() {
.as_ref() // Cancel selection, when cancel IME input.
.map(|range_utf16| self.range_from_utf16(range_utf16)) self.selected_range = range.start..range.start;
.map(|new_range| new_range.start + range.start..new_range.end + range.end) self.marked_range = None;
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()); } else {
self.marked_range = Some(range.start..range.start + new_text.len());
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
}
self.mode.update_auto_grow(&self.text_wrapper);
cx.emit(InputEvent::Change(self.unmask_value())); cx.emit(InputEvent::Change(self.unmask_value()));
cx.notify(); cx.notify();
} }
@ -1988,36 +2013,44 @@ impl EntityInputHandler for InputState {
let mut start_origin = None; let mut start_origin = None;
let mut end_origin = None; let mut end_origin = None;
let line_number_origin = point(self.line_number_width, px(0.));
let mut y_offset = px(0.); let mut y_offset = px(0.);
let mut index_offset = 0; let mut index_offset = 0;
for line in lines.iter() { for line in lines.iter() {
if let Some(p) =
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
{
start_origin = Some(p + point(px(0.), y_offset));
}
if let Some(p) =
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
{
end_origin = Some(p + point(px(0.), y_offset));
}
y_offset += line.size(line_height).height;
if start_origin.is_some() && end_origin.is_some() { if start_origin.is_some() && end_origin.is_some() {
break; break;
} }
index_offset += line.len(); if start_origin.is_none() {
if let Some(p) =
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
{
start_origin = Some(p + point(px(0.), y_offset));
}
}
if end_origin.is_none() {
if let Some(p) =
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
{
end_origin = Some(p + point(px(0.), y_offset));
}
}
index_offset += line.len() + 1;
y_offset += line.size(line_height).height;
} }
let start_origin = start_origin.unwrap_or_default(); let start_origin = start_origin.unwrap_or_default();
let end_origin = end_origin.unwrap_or_default(); let mut end_origin = end_origin.unwrap_or_default();
// Ensure at same line.
end_origin.y = start_origin.y;
Some(Bounds::from_corners( Some(Bounds::from_corners(
bounds.origin + start_origin, bounds.origin + line_number_origin + start_origin,
// + line_height for show IME panel under the cursor line. // + line_height for show IME panel under the cursor line.
bounds.origin + point(end_origin.x, end_origin.y + line_height), bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
)) ))
} }