reimplement text layout wrapping using the existing gpui TextLayout logic
This commit is contained in:
@@ -85,10 +85,6 @@ impl EditableTextElement for TextInputElement {
|
||||
fn placeholder(&self) -> &Option<SharedString> {
|
||||
&self.placeholder
|
||||
}
|
||||
|
||||
fn should_wrap(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TextInputElement {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::editable_text::{
|
||||
TextInputStateBase, TextLayoutWrapping, TextLineSegment,
|
||||
TextInputLayoutData, TextLineSegment,
|
||||
actions::{EditableInputActionElement, EditableTextActionHandler},
|
||||
};
|
||||
use gpui::{
|
||||
Along, App, Axis, Bounds, ContentMask, Context, CursorStyle, DispatchPhase, Display,
|
||||
ElementInputHandler, Entity, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla,
|
||||
InteractiveElement, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
Pixels, Point, ScrollWheelEvent, SharedString, Style, TextAlign, TextStyle, Window,
|
||||
Pixels, Point, ScrollWheelEvent, SharedString, Size, Style, TextAlign, TextLayout, Window,
|
||||
WrappedLine, fill, point, px, size,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
@@ -52,7 +52,6 @@ pub struct PrepaintState {
|
||||
pub trait EditableTextElement: InteractiveElement + EditableInputActionElement {
|
||||
fn init_state(&self, cx: &mut Context<Self::State>) -> Self::State;
|
||||
fn placeholder(&self) -> &Option<SharedString>;
|
||||
fn should_wrap(&self) -> bool;
|
||||
|
||||
fn shared_request_layout(
|
||||
&mut self,
|
||||
@@ -73,21 +72,156 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: This required a gpui api change in order to sync the focus handle between Interactivity and TextInputStateBase
|
||||
self.interactivity()
|
||||
.track_focus(state.read(cx).focus_handle(cx));
|
||||
let focus_handle;
|
||||
let show_placeholder;
|
||||
let storage_version;
|
||||
{
|
||||
let state = state.read(cx);
|
||||
focus_handle = state.focus_handle(cx);
|
||||
show_placeholder = state.storage().content_utf8().is_empty();
|
||||
storage_version = state.storage().version();
|
||||
}
|
||||
|
||||
// TODO: This required a gpui api change in order to sync the focus handle between Interactivity and TextInputStateBase
|
||||
self.interactivity().track_focus(focus_handle);
|
||||
|
||||
let placeholder = self.placeholder().clone();
|
||||
let placeholder_color = Hsla::white().opacity(0.5); // TODO: as an element param
|
||||
let layout_id = self.interactivity().request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx,
|
||||
|style, window, cx| {
|
||||
let state = state.clone();
|
||||
window.with_text_style(style.text_style().cloned(), |window| {
|
||||
//let text_style = window.text_style();
|
||||
// NOTE: Loosely mirrors TextLayout::layout
|
||||
let text_layout_id = window.request_measured_layout(Default::default(), {
|
||||
let text_style = window.text_style();
|
||||
let font_size = text_style.font_size.to_pixels(window.rem_size());
|
||||
let line_height = window.pixel_snap(
|
||||
text_style
|
||||
.line_height
|
||||
.to_pixels(font_size.into(), window.rem_size()),
|
||||
);
|
||||
move |known_dimensions, available_space, window, cx| {
|
||||
let text: SharedString;
|
||||
let color: Hsla;
|
||||
let prev_wrap_width: Option<Pixels>;
|
||||
let prev_size: Option<Size<Pixels>>;
|
||||
let last_seen_storage_version: u16;
|
||||
|
||||
// TODO: allocate the interior text layout and provide it as a child to the interactivity layout
|
||||
window.request_layout(style.clone(), None, cx)
|
||||
{
|
||||
let state = state.read(cx);
|
||||
match show_placeholder {
|
||||
false => {
|
||||
text = SharedString::from(state.storage().content_utf8());
|
||||
color = text_style.color;
|
||||
}
|
||||
true => {
|
||||
text = placeholder.clone().unwrap_or_default();
|
||||
color = placeholder_color;
|
||||
}
|
||||
}
|
||||
prev_wrap_width = state.layout_data.wrap_width;
|
||||
prev_size = state.layout_data.size;
|
||||
last_seen_storage_version =
|
||||
state.layout_data.last_seen_storage_version;
|
||||
}
|
||||
|
||||
let runs = vec![gpui::TextRun {
|
||||
len: text.len(),
|
||||
font: text_style.font(),
|
||||
color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}];
|
||||
|
||||
let wrap_width = TextLayout::evaluate_wrap_width(
|
||||
&text_style.white_space,
|
||||
known_dimensions,
|
||||
available_space,
|
||||
);
|
||||
|
||||
let truncation = TextLayout::evaluate_overflow(
|
||||
&text_style,
|
||||
known_dimensions,
|
||||
available_space,
|
||||
);
|
||||
|
||||
if let Some(size) = prev_size
|
||||
&& (wrap_width.is_none() || wrap_width == prev_wrap_width)
|
||||
&& truncation.width.is_none()
|
||||
&& storage_version == last_seen_storage_version
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
let (text, runs) = TextLayout::apply_truncation(
|
||||
text,
|
||||
&text_style,
|
||||
font_size,
|
||||
wrap_width,
|
||||
&truncation,
|
||||
&runs,
|
||||
cx,
|
||||
);
|
||||
|
||||
let wrapped_lines = window
|
||||
.text_system()
|
||||
.shape_text(
|
||||
text,
|
||||
font_size,
|
||||
&runs,
|
||||
wrap_width,
|
||||
text_style.line_clamp,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build the size of the text and convert the wrapped_lines into
|
||||
// lines that will be cached in state and painted.
|
||||
let mut size: Size<Pixels> = Size::default();
|
||||
let mut pos_y = 0;
|
||||
let mut line_start = 0;
|
||||
let mut lines = Vec::with_capacity(wrapped_lines.len());
|
||||
for line in wrapped_lines {
|
||||
let line_size = line.size(line_height);
|
||||
size.height += line_size.height;
|
||||
size.width = size.width.max(line_size.width).ceil();
|
||||
|
||||
let num_visual_lines = line.wrap_boundaries().len() + 1;
|
||||
let line_len = line.len();
|
||||
lines.push(TextLineSegment {
|
||||
text_range: line_start..line_start + line_len,
|
||||
wrapped_line: Some(Arc::new(line)),
|
||||
pos_y,
|
||||
num_visual_lines,
|
||||
});
|
||||
line_start += line_len;
|
||||
pos_y += num_visual_lines;
|
||||
}
|
||||
|
||||
let layout_data = TextInputLayoutData {
|
||||
wrap_width,
|
||||
size: Some(size),
|
||||
last_seen_storage_version,
|
||||
lines,
|
||||
lines_represent_placeholder: show_placeholder,
|
||||
};
|
||||
|
||||
// Update the state for use in prepaint, paint, and action handlers.
|
||||
// request_measured_layout caches this scope for processing later
|
||||
// between layout and prepaint, so we cant just copy/move these values to the outer scope.
|
||||
state.update(cx, move |state, _cx| {
|
||||
state.layout_data = layout_data;
|
||||
});
|
||||
|
||||
size
|
||||
}
|
||||
});
|
||||
|
||||
window.request_layout(style.clone(), Some(text_layout_id), cx)
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -144,48 +278,15 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement {
|
||||
bounds
|
||||
};
|
||||
|
||||
//let text_color = request_layout.text_style.color;
|
||||
let placeholder_color = Hsla::white().opacity(0.5); // TODO: as an element param
|
||||
let selection_color = Hsla::blue().opacity(0.5); // TODO: as an element param
|
||||
let caret_color = Hsla::white(); // TODO: as an element param
|
||||
|
||||
/*
|
||||
let wrap_width = self.should_wrap().then_some(inner_bounds.size.width);
|
||||
let showing_placeholder = request_layout.state.update(cx, |state, _cx| {
|
||||
let wrapping = TextLayoutWrapping::new(
|
||||
request_layout.text_style.clone(),
|
||||
wrap_width,
|
||||
state.storage().version(),
|
||||
);
|
||||
let show_placeholder = state.storage().content_utf8().is_empty();
|
||||
state.layout_data.bounds = inner_bounds;
|
||||
if state.layout_wrapping.integrate(wrapping) {
|
||||
let (display_text, color) = match show_placeholder {
|
||||
false => (state.storage().content_utf8(), text_color),
|
||||
true => {
|
||||
let value = self.placeholder().as_ref();
|
||||
let value = value.map(SharedString::as_str).unwrap_or_default();
|
||||
(value, placeholder_color)
|
||||
}
|
||||
};
|
||||
state.layout_data.lines = TextInputStateBase::build_wrapped_lines(
|
||||
display_text,
|
||||
&state.layout_wrapping,
|
||||
window,
|
||||
color,
|
||||
);
|
||||
}
|
||||
show_placeholder
|
||||
});
|
||||
*/
|
||||
let showing_placeholder = false;
|
||||
let state = request_layout.state.read(cx);
|
||||
|
||||
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();
|
||||
let focus_handle = state.focus_handle(cx);
|
||||
let caret_pos = state.caret_pos();
|
||||
let selection = state.selected_range();
|
||||
let ime_range = state.marked_range();
|
||||
// TODO: Cursor blinking
|
||||
let cursor_visible = true; // input.cursor_visible();
|
||||
|
||||
@@ -203,7 +304,7 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement {
|
||||
}
|
||||
};
|
||||
let mut carent_point = Point::default();
|
||||
for segment in input.line_segments() {
|
||||
for segment in &state.layout_data.lines {
|
||||
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;
|
||||
@@ -290,7 +391,7 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement {
|
||||
}
|
||||
|
||||
let is_focused = focus_handle.is_focused(window);
|
||||
if !showing_placeholder && is_focused && cursor_visible {
|
||||
if !state.layout_data.lines_represent_placeholder && is_focused && cursor_visible {
|
||||
const CURSOR_WIDTH: f32 = 2.0;
|
||||
let quad = fill(
|
||||
Bounds::new(
|
||||
|
||||
@@ -4,9 +4,8 @@ use crate::editable_text::{
|
||||
notify::{TextChanged, TextHistoryPushed},
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, ClipboardItem, EntityInputHandler, FocusHandle, Focusable, Hsla,
|
||||
NavigationDirection, Pixels, Point, SharedString, TextRun, TextStyle, UTF16Selection, Window,
|
||||
WrappedLine, point,
|
||||
App, Bounds, ClipboardItem, EntityInputHandler, FocusHandle, Focusable, NavigationDirection,
|
||||
Pixels, Point, Size, UTF16Selection, Window, WrappedLine, point,
|
||||
};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
@@ -47,49 +46,21 @@ pub struct TextInputStateBase {
|
||||
|
||||
focus_handle: FocusHandle,
|
||||
|
||||
pub(super) layout_wrapping: TextLayoutWrapping,
|
||||
pub(super) layout_data: TextInputLayoutData,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub(super) struct TextLayoutWrapping {
|
||||
text_style: TextStyle,
|
||||
wrap_width: Option<Pixels>,
|
||||
last_seen_storage_version: u16,
|
||||
}
|
||||
impl Default for TextLayoutWrapping {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text_style: Default::default(),
|
||||
wrap_width: Default::default(),
|
||||
last_seen_storage_version: u16::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TextLayoutWrapping {
|
||||
pub fn new(text_style: TextStyle, wrap_width: Option<Pixels>, storage_version: u16) -> Self {
|
||||
Self {
|
||||
text_style,
|
||||
wrap_width,
|
||||
last_seen_storage_version: storage_version,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn integrate(&mut self, other: Self) -> bool {
|
||||
let dirty = *self != other;
|
||||
*self = other;
|
||||
dirty
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct TextInputLayoutData {
|
||||
/// The last known width at which the lines were wrapped.
|
||||
pub wrap_width: Option<Pixels>,
|
||||
/// The last known size of the text, as generated during layout.
|
||||
pub size: Option<Size<Pixels>>,
|
||||
/// The last seen version of `storage` (for tracking when lines need to be reprocessed during layout)
|
||||
pub last_seen_storage_version: u16,
|
||||
/// 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<TextLineSegment>,
|
||||
/// The bounds of the text area, in window coordinates.
|
||||
/// Cached for IME operations.
|
||||
pub bounds: Bounds<Pixels>,
|
||||
pub lines_represent_placeholder: bool,
|
||||
}
|
||||
pub(super) struct TextLineSegment {
|
||||
/// The utf8 byte range in the content string that this line covers.
|
||||
@@ -125,7 +96,6 @@ impl TextInputStateBase {
|
||||
|
||||
focus_handle: cx.focus_handle(),
|
||||
|
||||
layout_wrapping: TextLayoutWrapping::default(),
|
||||
layout_data: TextInputLayoutData::default(),
|
||||
}
|
||||
}
|
||||
@@ -163,101 +133,6 @@ impl TextInputStateBase {
|
||||
}
|
||||
|
||||
impl TextInputStateBase {
|
||||
pub(super) fn line_segments(&self) -> &Vec<TextLineSegment> {
|
||||
&self.layout_data.lines
|
||||
}
|
||||
|
||||
pub(super) fn build_wrapped_lines(
|
||||
content: &str,
|
||||
wrapping: &TextLayoutWrapping,
|
||||
window: &Window,
|
||||
color: Hsla,
|
||||
) -> Vec<TextLineSegment> {
|
||||
let text_style = &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(),
|
||||
color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
|
||||
let wrapped_lines = window
|
||||
.text_system()
|
||||
.shape_text(
|
||||
SharedString::from(line_slice.to_string()),
|
||||
font_size,
|
||||
&[run],
|
||||
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
|
||||
}
|
||||
|
||||
/// Returns the utf-8 character position of the start of the line that contains the provided pixel-point.
|
||||
pub fn index_for_pixel_point(&self, point: Point<Pixels>, line_height: Pixels) -> usize {
|
||||
let storage_len_utf8 = self.storage.content_utf8().len();
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::editable_text::{
|
||||
shared_element::{self, EditableTextElement},
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity,
|
||||
IntoElement, Pixels, SharedString, StyleRefinement, Styled, TextStyle, WeakEntity, Window,
|
||||
App, Bounds, Element, ElementId, InteractiveElement, Interactivity, IntoElement, Pixels,
|
||||
SharedString, StyleRefinement, Styled, WeakEntity, Window,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
@@ -84,10 +84,6 @@ impl EditableTextElement for TextAreaElement {
|
||||
fn placeholder(&self) -> &Option<SharedString> {
|
||||
&self.placeholder
|
||||
}
|
||||
|
||||
fn should_wrap(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TextAreaElement {
|
||||
|
||||
Reference in New Issue
Block a user