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)]
|
#[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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue