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)]
pub(crate) struct LineHighlightStyle {
offset: usize,
pub(crate) styles: Rc<Vec<(Range<usize>, HighlightStyle)>>,
}
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
.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
.chain(std::iter::once(text_style.clone().to_run(1)))
.collect()
@ -52,21 +70,29 @@ impl CodeHighlighter {
}
let mut lines = vec![];
let mut offset = 0;
let mut new_cache = HashMap::new();
for line in text.split('\n') {
let cache_key = gpui::hash(&line);
// cache hit
if let Some(line_style) = self.cache.get(&cache_key) {
new_cache.insert(cache_key, line_style.clone());
lines.push(line_style.clone());
let new_style = LineHighlightStyle {
offset,
styles: line_style.styles.clone(),
};
new_cache.insert(cache_key, new_style.clone());
lines.push(new_style);
} else {
// cache miss
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());
lines.push(line_style);
}
// +1 for '\n'
offset += line.len() + 1;
}
// Ensure to recreate cache to remove unused caches.

View file

@ -1,5 +1,3 @@
use std::rc::Rc;
use gpui::{
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path,
@ -58,7 +56,11 @@ impl TextElement {
cx: &mut App,
) -> (Option<PaintQuad>, Point<Pixels>, usize) {
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 mut current_line_index = 0;
let mut scroll_offset = input.scroll_handle.offset();
@ -193,7 +195,12 @@ impl TextElement {
cx: &mut App,
) -> Option<Path<Pixels>> {
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() {
return None;
}
@ -322,13 +329,6 @@ impl TextElement {
_ => 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 {
@ -513,8 +513,31 @@ impl Element for TextElement {
underline: 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![
TextRun {
len: marked_range.start,
@ -522,11 +545,7 @@ impl Element for TextElement {
},
TextRun {
len: marked_range.end - marked_range.start,
underline: Some(UnderlineStyle {
color: Some(run.color),
thickness: px(1.0),
wavy: false,
}),
underline: marked_run.underline,
..run.clone()
},
TextRun {
@ -537,16 +556,6 @@ impl Element for TextElement {
.into_iter()
.filter(|run| run.len > 0)
.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 {
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
let line_height = window.line_height();
let origin = bounds.origin;
@ -690,10 +694,14 @@ impl Element for TextElement {
for (ix, line) in line_numbers.iter().enumerate() {
let p = point(origin.x, origin.y + offset_y);
let line_size = line.size(line_height);
// Paint the current line background
if prepaint.current_line_index == ix {
if let Some(bg_color) = self
.highlighter(cx)
.input
.read(cx)
.mode
.highlighter()
.and_then(|h| h.theme.settings().line_highlight)
.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.);
for line in prepaint.lines.iter() {
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 super::code_highlighter::CodeHighlighter;
@ -148,6 +150,13 @@ impl InputMode {
_ => None,
}
}
pub(super) fn highlighter(&self) -> Option<Rc<crate::highlighter::Highlighter<'static>>> {
match &self {
InputMode::CodeEditor { highlighter, .. } => Some(highlighter.highlighter.clone()),
_ => None,
}
}
}
#[cfg(test)]

View file

@ -225,6 +225,7 @@ pub struct InputState {
pub(super) selection_reversed: bool,
/// The index of the current line, zero-based.
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) last_layout: Option<SmallVec<[WrappedLine; 1]>>,
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>) {
if self.marked_range.is_some() {
self.unmark_text(window, cx);
}
if self.selected_range.len() > 0 {
return self.unselect(window, cx);
}
@ -1319,6 +1323,14 @@ impl InputState {
window: &mut Window,
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;
let offset = self.index_for_mouse_position(event.position, window, cx);
// Double click to select word
@ -1468,6 +1480,10 @@ impl InputState {
}
pub(super) fn cursor_offset(&self) -> usize {
if let Some(marked_range) = &self.marked_range {
return marked_range.end;
}
if self.selection_reversed {
self.selected_range.start
} else {
@ -1938,6 +1954,7 @@ impl EntityInputHandler for InputState {
cx.notify();
}
/// Mark text is the IME temporary insert on typing.
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
@ -1963,12 +1980,20 @@ impl EntityInputHandler for InputState {
self.push_history(&range, new_text, window, cx);
self.text = pending_text;
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.text_wrapper.update(self.text.clone(), false, cx);
if new_text.is_empty() {
// Cancel selection, when cancel IME input.
self.selected_range = range.start..range.start;
self.marked_range = None;
} 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.notify();
}
@ -1988,36 +2013,44 @@ impl EntityInputHandler for InputState {
let mut start_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 index_offset = 0;
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() {
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 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(
bounds.origin + start_origin,
bounds.origin + line_number_origin + start_origin,
// + 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),
))
}