begin merging paint implementations for multi-line runs
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use crate::editable_text::{
|
||||
EditableInputActionElement, EditableTextActionHandler, InitStorage, StateBackedElement,
|
||||
TextInputLayoutData, TextInputState,
|
||||
TextInputLayoutData, TextInputState, TextLayoutWrapping, TextLineSegment,
|
||||
};
|
||||
use gpui::{
|
||||
Along, App, Axis, Bounds, ContentMask, CursorStyle, DispatchPhase, Display, Element, ElementId,
|
||||
ElementInputHandler, Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla,
|
||||
InteractiveElement, Interactivity, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, PaintQuad, Pixels, Point, ScrollWheelEvent, ShapedLine, SharedString, Style,
|
||||
StyleRefinement, Styled, TextAlign, TextRun, TextStyle, Window, fill, point, size,
|
||||
StyleRefinement, Styled, TextAlign, TextRun, TextStyle, Window, WrappedLine, fill, point, px,
|
||||
size,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
@@ -75,12 +78,25 @@ impl super::StateBackedElement for TextInputElement {
|
||||
|
||||
enum PrepaintElement {
|
||||
Line {
|
||||
line: ShapedLine,
|
||||
line: Arc<WrappedLine>,
|
||||
point: Point<Pixels>,
|
||||
align: TextAlign,
|
||||
},
|
||||
Quad(PaintQuad),
|
||||
}
|
||||
impl PrepaintElement {
|
||||
fn build_quads(
|
||||
offset_corners: Vec<(Point<Pixels>, Point<Pixels>)>,
|
||||
origin: Point<Pixels>,
|
||||
color: Hsla,
|
||||
) -> impl Iterator<Item = Self> {
|
||||
let iter = offset_corners.into_iter();
|
||||
iter.map(move |(offset_start, offset_end)| {
|
||||
let bounds = Bounds::from_corners(origin + offset_start, origin + offset_end);
|
||||
PrepaintElement::Quad(fill(bounds, color))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub mod element {
|
||||
use smallvec::SmallVec;
|
||||
@@ -99,7 +115,6 @@ pub mod element {
|
||||
pub focus_handle: FocusHandle,
|
||||
pub(super) elements: SmallVec<[PrepaintElement; 3]>,
|
||||
pub scroll_offset: Point<Pixels>,
|
||||
pub display_text: SharedString,
|
||||
pub caret_visible: bool,
|
||||
}
|
||||
}
|
||||
@@ -159,11 +174,33 @@ impl Element for TextInputElement {
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
) -> Self::PrepaintState {
|
||||
// TODO: no wrapping in single-line
|
||||
let wrap_width = Some(bounds.size.width);
|
||||
|
||||
let wrapping = TextLayoutWrapping::new(request_layout.text_style.clone(), wrap_width);
|
||||
let showing_placeholder = request_layout.state.update(cx, |state, _cx| {
|
||||
let text_value = state.storage().content_utf8();
|
||||
let is_empty = text_value.is_empty();
|
||||
let display_text = match is_empty {
|
||||
false => text_value,
|
||||
true => self
|
||||
.placeholder
|
||||
.as_ref()
|
||||
.map(SharedString::as_str)
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
state.layout_data_mut().bounds = bounds;
|
||||
state.apply_wrapping(wrapping, display_text, window);
|
||||
is_empty
|
||||
});
|
||||
|
||||
let input = request_layout.state.read(cx);
|
||||
|
||||
let focus_handle = input.focus_handle(cx);
|
||||
let caret_pos = input.caret_pos();
|
||||
let selection = input.selected_range();
|
||||
let ime_range = input.marked_range();
|
||||
// TODO: Cursor blinking
|
||||
let cursor_visible = true; // input.cursor_visible();
|
||||
|
||||
@@ -174,18 +211,7 @@ impl Element for TextInputElement {
|
||||
|
||||
let mut elements = SmallVec::new();
|
||||
|
||||
let text_value = input.storage().content_utf8();
|
||||
let is_empty = text_value.is_empty();
|
||||
|
||||
let (display_text, run_color) = match is_empty {
|
||||
// TODO: Can the SharedString allocation be avoided?
|
||||
false => (SharedString::new(text_value), text_color),
|
||||
true => {
|
||||
let value = self.placeholder.as_ref().cloned().unwrap_or_default();
|
||||
(value, placeholder_color)
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: how do we enable scrolling? overflow on interactivity?
|
||||
let (hitbox, scroll_offset) = self.interactivity.prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
@@ -200,55 +226,111 @@ impl Element for TextInputElement {
|
||||
},
|
||||
);
|
||||
|
||||
let style = window.text_style();
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let run = TextRun {
|
||||
len: display_text.len(),
|
||||
font: style.font(),
|
||||
color: run_color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
let line = window
|
||||
.text_system()
|
||||
.shape_line(display_text.clone(), font_size, &[run], None);
|
||||
let line_height = window.line_height();
|
||||
let is_range_contained_by_range =
|
||||
|text_range: &Range<usize>, containing_range: &Range<usize>| {
|
||||
if text_range.is_empty() {
|
||||
containing_range.start <= text_range.start
|
||||
&& containing_range.end > text_range.start
|
||||
} else {
|
||||
containing_range.end > text_range.start
|
||||
&& containing_range.start < text_range.end
|
||||
}
|
||||
};
|
||||
let mut carent_point = Point::default();
|
||||
for segment in input.line_segments() {
|
||||
let line_distance_from_top = segment.pos_y * line_height;
|
||||
let line_y = line_distance_from_top - scroll_offset.y;
|
||||
let line_bottom = line_y + line_height * segment.num_visual_lines as f32;
|
||||
let line_visible = line_bottom >= Pixels::ZERO && line_y <= bounds.size.height;
|
||||
if !line_visible {
|
||||
continue;
|
||||
}
|
||||
|
||||
let has_selection = !selection.is_empty() && !is_empty;
|
||||
if has_selection {
|
||||
let start_x = line.x_for_index(selection.start);
|
||||
let end_x = line.x_for_index(selection.end);
|
||||
let quad = fill(
|
||||
Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + start_x.min(end_x) - scroll_offset.x,
|
||||
bounds.top(),
|
||||
),
|
||||
point(
|
||||
bounds.left() + start_x.max(end_x) - scroll_offset.x,
|
||||
bounds.bottom(),
|
||||
),
|
||||
),
|
||||
selection_color,
|
||||
);
|
||||
elements.push(PrepaintElement::Quad(quad));
|
||||
// TODO: First render all lines (underlines for IME), then all selections, then cursor if no selection
|
||||
|
||||
if let Some(wrapped) = &segment.wrapped_line {
|
||||
let point = bounds.origin + point(Pixels::ZERO, line_y);
|
||||
elements.push(PrepaintElement::Line {
|
||||
line: wrapped.clone(),
|
||||
point,
|
||||
align: TextAlign::Left,
|
||||
});
|
||||
}
|
||||
|
||||
let segment_is_empty = segment.text_range.is_empty();
|
||||
|
||||
if is_range_contained_by_range(&segment.text_range, &selection) {
|
||||
if segment_is_empty {
|
||||
const EMPTY_LINE_SELECTION_WIDTH: Pixels = px(6.);
|
||||
elements.push(PrepaintElement::Quad(fill(
|
||||
Bounds::from_corners(
|
||||
bounds.origin + point(Pixels::ZERO, line_y),
|
||||
bounds.origin + point(EMPTY_LINE_SELECTION_WIDTH, line_y + line_height),
|
||||
),
|
||||
selection_color,
|
||||
)));
|
||||
} else {
|
||||
let offset_corners = build_quad_over_text(
|
||||
&selection,
|
||||
segment,
|
||||
line_y,
|
||||
line_height,
|
||||
Pixels::ZERO,
|
||||
);
|
||||
elements.extend(PrepaintElement::build_quads(
|
||||
offset_corners,
|
||||
bounds.origin,
|
||||
selection_color,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !segment_is_empty && let Some(ime_range) = &ime_range {
|
||||
if !ime_range.is_empty()
|
||||
&& is_range_contained_by_range(&segment.text_range, &ime_range)
|
||||
{
|
||||
const MARKED_TEXT_UNDERLINE_THICKNESS: f32 = 2.0;
|
||||
let underline_thickness = px(MARKED_TEXT_UNDERLINE_THICKNESS);
|
||||
let underline_offset = line_height - underline_thickness;
|
||||
|
||||
let offset_corners = build_quad_over_text(
|
||||
&ime_range,
|
||||
segment,
|
||||
line_y,
|
||||
line_height,
|
||||
underline_offset,
|
||||
);
|
||||
elements.extend(PrepaintElement::build_quads(
|
||||
offset_corners,
|
||||
bounds.origin,
|
||||
selection_color,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let is_cursor_in_line = if segment_is_empty {
|
||||
caret_pos == segment.text_range.start
|
||||
} else {
|
||||
segment.text_range.contains(&caret_pos) || caret_pos == segment.text_range.end
|
||||
};
|
||||
if is_cursor_in_line && let Some(wrapped) = &segment.wrapped_line {
|
||||
let local_offset = caret_pos.saturating_sub(segment.text_range.start);
|
||||
let caret_px = wrapped
|
||||
.position_for_index(local_offset, line_height)
|
||||
.unwrap_or_default();
|
||||
carent_point = caret_px + point(Pixels::ZERO, line_y);
|
||||
}
|
||||
}
|
||||
|
||||
let caret_x_line = line.x_for_index(caret_pos);
|
||||
elements.push(PrepaintElement::Line {
|
||||
line,
|
||||
point: bounds.origin - point(scroll_offset.x, gpui::px(0.)),
|
||||
align: TextAlign::Left,
|
||||
});
|
||||
|
||||
let has_selection = !selection.is_empty() && !showing_placeholder;
|
||||
let is_focused = focus_handle.is_focused(window);
|
||||
if !has_selection && is_focused && cursor_visible {
|
||||
let cursor_thickness = gpui::px(2.0);
|
||||
let cursor_paint_x = bounds.left() + caret_x_line - scroll_offset.x;
|
||||
const CURSOR_WIDTH: f32 = 2.0;
|
||||
let quad = fill(
|
||||
Bounds::new(
|
||||
point(cursor_paint_x, bounds.top()),
|
||||
size(cursor_thickness, bounds.bottom() - bounds.top()),
|
||||
bounds.origin + carent_point - scroll_offset,
|
||||
size(gpui::px(CURSOR_WIDTH), line_height),
|
||||
),
|
||||
caret_color,
|
||||
);
|
||||
@@ -260,7 +342,6 @@ impl Element for TextInputElement {
|
||||
focus_handle,
|
||||
elements,
|
||||
scroll_offset,
|
||||
display_text,
|
||||
caret_visible: cursor_visible,
|
||||
}
|
||||
}
|
||||
@@ -279,7 +360,6 @@ impl Element for TextInputElement {
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
}
|
||||
|
||||
let mut layout_data = TextInputLayoutData::default();
|
||||
let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| {
|
||||
if style.display == Display::None {
|
||||
return;
|
||||
@@ -392,13 +472,13 @@ impl Element for TextInputElement {
|
||||
}
|
||||
});
|
||||
|
||||
layout_data = window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||
let line_h = window.line_height();
|
||||
let mut lines = Vec::with_capacity(prepaint.elements.len());
|
||||
for element in prepaint.elements.drain(..) {
|
||||
match element {
|
||||
PrepaintElement::Line { line, point, align } => {
|
||||
let _ = line.paint(point, line_h, align, None, window, cx);
|
||||
let _ = line.paint(point, line_h, align, Some(bounds), window, cx);
|
||||
lines.push(line);
|
||||
}
|
||||
PrepaintElement::Quad(quad) => window.paint_quad(quad),
|
||||
@@ -406,8 +486,6 @@ impl Element for TextInputElement {
|
||||
}
|
||||
|
||||
// TODO: Render marked IME underlines
|
||||
|
||||
TextInputLayoutData { lines, bounds }
|
||||
});
|
||||
};
|
||||
self.interactivity.paint(
|
||||
@@ -419,9 +497,69 @@ impl Element for TextInputElement {
|
||||
cx,
|
||||
perform_paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
request_layout.state.update(cx, |state, _cx| {
|
||||
*state.layout_data_mut() = layout_data;
|
||||
fn build_quad_over_text(
|
||||
containing_range: &Range<usize>,
|
||||
segment: &TextLineSegment,
|
||||
line_y: Pixels,
|
||||
line_height: Pixels,
|
||||
offset_y: Pixels,
|
||||
) -> Vec<(Point<Pixels>, Point<Pixels>)> {
|
||||
let Some(wrapped) = &segment.wrapped_line else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let line_start = segment.text_range.start;
|
||||
let line_end = segment.text_range.end;
|
||||
|
||||
let subrange_start = containing_range.start.max(line_start) - line_start;
|
||||
let subrange_end = containing_range.end.min(line_end) - line_start;
|
||||
|
||||
let start_pos = wrapped
|
||||
.position_for_index(subrange_start, line_height)
|
||||
.unwrap_or_default();
|
||||
let end_pos = wrapped
|
||||
.position_for_index(subrange_end, line_height)
|
||||
.unwrap_or_else(|| {
|
||||
let last_line_y = line_height * (segment.num_visual_lines - 1) as f32;
|
||||
point(wrapped.width(), last_line_y)
|
||||
});
|
||||
|
||||
let start_visual_line = (start_pos.y / line_height).floor() as usize;
|
||||
let end_visual_line = (end_pos.y / line_height).floor() as usize;
|
||||
|
||||
if start_visual_line == end_visual_line {
|
||||
vec![(
|
||||
point(start_pos.x, line_y + start_pos.y + offset_y),
|
||||
point(end_pos.x, line_y + start_pos.y + line_height),
|
||||
)]
|
||||
} else {
|
||||
let line_width = wrapped.width();
|
||||
let middle_lines = (start_visual_line + 1)..end_visual_line;
|
||||
let mut quad_corners = Vec::with_capacity(middle_lines.end - middle_lines.start + 2);
|
||||
|
||||
quad_corners.push((
|
||||
point(start_pos.x, line_y + start_pos.y + offset_y),
|
||||
point(line_width, line_y + start_pos.y + line_height),
|
||||
));
|
||||
|
||||
// Middle visual lines
|
||||
for visual_line in (start_visual_line + 1)..end_visual_line {
|
||||
let y = line_height * visual_line as f32;
|
||||
quad_corners.push((
|
||||
point(Pixels::ZERO, line_y + y + offset_y),
|
||||
point(line_width, line_y + y + line_height),
|
||||
));
|
||||
}
|
||||
|
||||
// Last visual line
|
||||
quad_corners.push((
|
||||
point(Pixels::ZERO, line_y + end_pos.y + offset_y),
|
||||
point(end_pos.x, line_y + end_pos.y + line_height),
|
||||
));
|
||||
|
||||
quad_corners
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ use crate::editable_text::{
|
||||
notify::{TextChanged, TextHistoryPushed},
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, ClipboardItem, Entity, FocusHandle, Focusable, NavigationDirection, Pixels, Point,
|
||||
ShapedLine, UTF16Selection, Window,
|
||||
App, Bounds, ClipboardItem, Entity, FocusHandle, Focusable, Hsla, NavigationDirection, Pixels,
|
||||
Point, ShapedLine, SharedString, TextRun, TextStyle, UTF16Selection, Window, WrappedLine,
|
||||
};
|
||||
use std::ops::Range;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
pub trait TextStateNotifier {
|
||||
fn notify_changed(&mut self);
|
||||
@@ -54,18 +54,48 @@ pub struct TextInputStateBase {
|
||||
|
||||
focus_handle: FocusHandle,
|
||||
|
||||
layout_wrapping: TextLayoutWrapping,
|
||||
layout_data: TextInputLayoutData,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct TextLayoutWrapping {
|
||||
text_style: TextStyle,
|
||||
wrap_width: Option<Pixels>,
|
||||
dirty: bool,
|
||||
}
|
||||
impl TextLayoutWrapping {
|
||||
pub fn new(text_style: TextStyle, wrap_width: Option<Pixels>) -> Self {
|
||||
Self {
|
||||
text_style,
|
||||
wrap_width,
|
||||
dirty: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct TextInputLayoutData {
|
||||
/// The `ShapedLine` produced by the painter's `prepaint`.
|
||||
/// Cached so IME `bounds_for_range` / `character_index_for_point` can evaluate without re-shaping.
|
||||
pub lines: Vec<ShapedLine>,
|
||||
pub lines: Vec<TextLineSegment>,
|
||||
/// The bounds of the text area, in window coordinates.
|
||||
/// Cached for IME operations.
|
||||
pub bounds: Bounds<Pixels>,
|
||||
}
|
||||
pub(super) struct TextLineSegment {
|
||||
/// The utf8 byte range in the content string that this line covers.
|
||||
pub text_range: Range<usize>,
|
||||
/// The shaped and wrapped text for this line, if available.
|
||||
pub wrapped_line: Option<Arc<WrappedLine>>,
|
||||
|
||||
/// The y-coordinate of this segment which can be multiplied by the line_height
|
||||
/// to get its pixel location relative to the bounds of the text area.
|
||||
pub pos_y: usize,
|
||||
/// The number of segments up to and including this segment in the literal line that has been wrapped.
|
||||
/// There may be other segments after this one with a larger counter.
|
||||
pub num_visual_lines: usize,
|
||||
}
|
||||
|
||||
impl Focusable for TextInputStateBase {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
@@ -87,6 +117,10 @@ impl TextInputStateBase {
|
||||
|
||||
focus_handle: cx.focus_handle(),
|
||||
|
||||
layout_wrapping: TextLayoutWrapping {
|
||||
dirty: true,
|
||||
..TextLayoutWrapping::default()
|
||||
},
|
||||
layout_data: TextInputLayoutData::default(),
|
||||
}
|
||||
}
|
||||
@@ -118,6 +152,10 @@ impl TextInputStateBase {
|
||||
self.selected_range = range;
|
||||
}
|
||||
|
||||
pub fn marked_range(&self) -> Option<Range<usize>> {
|
||||
self.marked_range.clone()
|
||||
}
|
||||
|
||||
pub(super) fn layout_data(&self) -> &TextInputLayoutData {
|
||||
&self.layout_data
|
||||
}
|
||||
@@ -127,6 +165,115 @@ impl TextInputStateBase {
|
||||
}
|
||||
}
|
||||
|
||||
impl TextInputStateBase {
|
||||
pub fn apply_wrapping(
|
||||
&mut self,
|
||||
wrapping: TextLayoutWrapping,
|
||||
display_text: &str,
|
||||
window: &Window,
|
||||
) {
|
||||
let dirty = self.layout_wrapping.dirty
|
||||
|| self.layout_wrapping.wrap_width != wrapping.wrap_width
|
||||
|| self.layout_wrapping.text_style != wrapping.text_style;
|
||||
self.layout_wrapping = wrapping;
|
||||
if dirty {
|
||||
self.layout_data.lines = self.build_wrapped_lines(display_text, window);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_segments(&self) -> &Vec<TextLineSegment> {
|
||||
&self.layout_data.lines
|
||||
}
|
||||
|
||||
fn build_wrapped_lines(&self, content: &str, window: &Window) -> Vec<TextLineSegment> {
|
||||
let text_style = &self.layout_wrapping.text_style;
|
||||
let font_size = text_style.font_size.to_pixels(window.rem_size());
|
||||
let mut lines = Vec::new();
|
||||
|
||||
if content.is_empty() {
|
||||
lines.push(TextLineSegment {
|
||||
text_range: 0..0,
|
||||
wrapped_line: None,
|
||||
pos_y: 0,
|
||||
num_visual_lines: 1,
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
|
||||
let mut pos_y = 0;
|
||||
let mut current_pos = 0;
|
||||
|
||||
while current_pos < content.len() {
|
||||
let line_end = content[current_pos..]
|
||||
.find('\n')
|
||||
.map(|pos| current_pos + pos)
|
||||
.unwrap_or(content.len());
|
||||
|
||||
let line_slice = &content[current_pos..line_end];
|
||||
|
||||
if line_slice.is_empty() {
|
||||
lines.push(TextLineSegment {
|
||||
text_range: current_pos..current_pos,
|
||||
wrapped_line: None,
|
||||
pos_y,
|
||||
num_visual_lines: 1,
|
||||
});
|
||||
pos_y += 1;
|
||||
} else {
|
||||
let run = TextRun {
|
||||
len: line_slice.len(),
|
||||
font: text_style.font(),
|
||||
// TODO: This is the actual text color that is stored in WrappedLine,
|
||||
// needs to reflect the style color provided by paint
|
||||
color: Hsla::default(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
|
||||
let wrapped_lines = window
|
||||
.text_system()
|
||||
.shape_text(
|
||||
SharedString::from(line_slice.to_string()),
|
||||
font_size,
|
||||
&[run],
|
||||
self.layout_wrapping.wrap_width,
|
||||
None,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
for wrapped in wrapped_lines {
|
||||
let num_visual_lines = wrapped.wrap_boundaries().len() + 1;
|
||||
lines.push(TextLineSegment {
|
||||
text_range: current_pos..line_end,
|
||||
wrapped_line: Some(Arc::new(wrapped)),
|
||||
pos_y,
|
||||
num_visual_lines,
|
||||
});
|
||||
pos_y += num_visual_lines;
|
||||
}
|
||||
}
|
||||
|
||||
current_pos = if line_end < content.len() {
|
||||
line_end + 1
|
||||
} else {
|
||||
content.len()
|
||||
};
|
||||
}
|
||||
|
||||
if content.ends_with('\n') {
|
||||
lines.push(TextLineSegment {
|
||||
text_range: content.len()..content.len(),
|
||||
wrapped_line: None,
|
||||
pos_y,
|
||||
num_visual_lines: 1,
|
||||
});
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
impl TextInputStateBase {
|
||||
pub fn ime_text_for_range(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user