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:
parent
2c4a28a6da
commit
8dc9666214
4 changed files with 142 additions and 60 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue