move element implementation details to shared trait for input and text-area
This commit is contained in:
@@ -2,6 +2,7 @@ pub mod actions;
|
||||
mod input_element;
|
||||
mod input_state;
|
||||
pub mod notify;
|
||||
mod shared_element;
|
||||
mod shared_state;
|
||||
mod storage;
|
||||
mod text_area_element;
|
||||
|
||||
@@ -2,6 +2,8 @@ use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{Action, AppContext, Context, InteractiveElement, WeakEntity, Window};
|
||||
|
||||
use crate::editable_text::StateBackedEditableText;
|
||||
|
||||
/// The key context used for input element keybindings.
|
||||
pub const DEFAULT_INPUT_CONTEXT: &str = "Input";
|
||||
|
||||
@@ -236,9 +238,7 @@ pub trait EditableTextActionHandler<'app>: Sized {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait EditableInputActionElement {
|
||||
type State: 'static;
|
||||
|
||||
pub(super) trait EditableInputActionElement: StateBackedEditableText {
|
||||
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<Self::State>>>;
|
||||
|
||||
fn register_action<ActionType>(
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use crate::editable_text::{
|
||||
InitStorage, TextInputState, TextInputStateBase, TextLayoutWrapping, TextLineSegment,
|
||||
InitStorage, StateBackedEditableText, TextInputState,
|
||||
actions::{DEFAULT_INPUT_CONTEXT, EditableInputActionElement},
|
||||
shared_element::{self, EditableTextElement},
|
||||
};
|
||||
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, SharedString, Style, StyleRefinement,
|
||||
Styled, TextAlign, TextStyle, WeakEntity, Window, WrappedLine, fill, point, px, size,
|
||||
App, Bounds, Element, ElementId, InteractiveElement, Interactivity, IntoElement, Pixels,
|
||||
SharedString, StyleRefinement, Styled, WeakEntity, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
#[track_caller]
|
||||
pub fn input(id: impl Into<ElementId>) -> TextInputElement {
|
||||
@@ -65,58 +62,33 @@ impl IntoElement for TextInputElement {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditableInputActionElement for TextInputElement {
|
||||
impl StateBackedEditableText for TextInputElement {
|
||||
type State = TextInputState;
|
||||
}
|
||||
|
||||
impl EditableInputActionElement for TextInputElement {
|
||||
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<Self::State>>> {
|
||||
&self.state_entity
|
||||
}
|
||||
}
|
||||
|
||||
enum PrepaintElement {
|
||||
Line {
|
||||
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 super::*;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct LayoutState {
|
||||
pub state: Entity<TextInputState>,
|
||||
pub text_style: TextStyle,
|
||||
impl EditableTextElement for TextInputElement {
|
||||
fn init_state(&self, cx: &mut gpui::prelude::Context<Self::State>) -> Self::State {
|
||||
Self::State::new(self.init_storage.exec(cx), cx)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct PrepaintState {
|
||||
pub hitbox: Option<Hitbox>,
|
||||
pub focus_handle: FocusHandle,
|
||||
pub(super) elements: SmallVec<[PrepaintElement; 3]>,
|
||||
pub scroll_offset: Point<Pixels>,
|
||||
pub caret_visible: bool,
|
||||
fn placeholder(&self) -> &Option<SharedString> {
|
||||
&self.placeholder
|
||||
}
|
||||
|
||||
fn should_wrap(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TextInputElement {
|
||||
type RequestLayoutState = element::LayoutState;
|
||||
type PrepaintState = element::PrepaintState;
|
||||
type RequestLayoutState = shared_element::LayoutState<TextInputState>;
|
||||
type PrepaintState = shared_element::PrepaintState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
@@ -130,464 +102,42 @@ impl Element for TextInputElement {
|
||||
&mut self,
|
||||
global_id: Option<&gpui::GlobalElementId>,
|
||||
inspector_id: Option<&gpui::InspectorElementId>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
// Fetches or initializes the internal state of the field
|
||||
let state = match &self.interactivity.element_id {
|
||||
None => unimplemented!("all input elements must be assigned an id"),
|
||||
Some(element_id) => {
|
||||
let state = window.use_keyed_state(element_id.clone(), cx, |_window, cx| {
|
||||
println!("init input state {:?}", cx.weak_entity());
|
||||
TextInputState::new(self.init_storage.exec(cx), cx)
|
||||
});
|
||||
// store a reference to the entity owned by the element for access in action handlers
|
||||
*self.state_entity.borrow_mut() = state.downgrade();
|
||||
state
|
||||
}
|
||||
};
|
||||
|
||||
let mut resolved_text_style = None;
|
||||
|
||||
self.interactivity
|
||||
.track_focus(state.read(cx).focus_handle(cx));
|
||||
let layout_id = self.interactivity.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx,
|
||||
|element_style, window, cx| {
|
||||
window.with_text_style(element_style.text_style().cloned(), |window| {
|
||||
resolved_text_style = Some(window.text_style());
|
||||
|
||||
let style = element_style.clone();
|
||||
// TODO: Does this need to propagate the line_height as the element's height?
|
||||
window.request_layout(style, None, cx)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let layout_state = Self::RequestLayoutState {
|
||||
state,
|
||||
text_style: resolved_text_style.unwrap_or_else(|| window.text_style()),
|
||||
};
|
||||
(layout_id, layout_state)
|
||||
self.shared_request_layout(global_id, inspector_id, window, cx)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&gpui::GlobalElementId>,
|
||||
inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
struct InteractivityPrepaint {
|
||||
hitbox: Option<Hitbox>,
|
||||
scroll_offset: Point<Pixels>,
|
||||
}
|
||||
// TODO: how do we enable scrolling? overflow on interactivity?
|
||||
let prepaint = self.interactivity.prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
bounds.size,
|
||||
window,
|
||||
cx,
|
||||
|_style, scroll_offset, hitbox, window, _cx| {
|
||||
let hitbox =
|
||||
hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal)));
|
||||
InteractivityPrepaint {
|
||||
hitbox,
|
||||
scroll_offset,
|
||||
}
|
||||
},
|
||||
);
|
||||
let InteractivityPrepaint {
|
||||
hitbox,
|
||||
scroll_offset,
|
||||
} = prepaint;
|
||||
|
||||
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
|
||||
|
||||
// TODO: no wrapping in single-line
|
||||
let wrap_width = Some(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 = 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 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();
|
||||
|
||||
let mut elements = SmallVec::new();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 is_focused = focus_handle.is_focused(window);
|
||||
if !showing_placeholder && is_focused && cursor_visible {
|
||||
const CURSOR_WIDTH: f32 = 2.0;
|
||||
let quad = fill(
|
||||
Bounds::new(
|
||||
bounds.origin + carent_point - scroll_offset,
|
||||
size(gpui::px(CURSOR_WIDTH), line_height),
|
||||
),
|
||||
caret_color,
|
||||
);
|
||||
elements.push(PrepaintElement::Quad(quad));
|
||||
}
|
||||
|
||||
Self::PrepaintState {
|
||||
hitbox,
|
||||
focus_handle,
|
||||
elements,
|
||||
scroll_offset,
|
||||
caret_visible: cursor_visible,
|
||||
}
|
||||
self.shared_prepaint(global_id, inspector_id, bounds, request_layout, window, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&gpui::GlobalElementId>,
|
||||
inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if let Some(hitbox) = &prepaint.hitbox {
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
}
|
||||
|
||||
let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| {
|
||||
if style.display == Display::None {
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: Skip when disabled
|
||||
let ime_handler = ElementInputHandler::new(bounds, request_layout.state.clone());
|
||||
window.handle_input(&prepaint.focus_handle, ime_handler, cx);
|
||||
|
||||
use super::actions::EditableTextActionHandler;
|
||||
let get_relative_position = {
|
||||
let bounds = bounds.clone();
|
||||
move |position: Point<Pixels>| {
|
||||
// Converts a screen position to a position relative to the text area origin,
|
||||
// adjusted for scroll offset.
|
||||
let scroll_distance = gpui::px(0.); // TODO: STUB
|
||||
(position - bounds.origin)
|
||||
.apply_along(Axis::Horizontal, |pos| pos + scroll_distance)
|
||||
}
|
||||
};
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
if !bounds.contains(&event.position) {
|
||||
return;
|
||||
}
|
||||
if event.button != MouseButton::Left {
|
||||
return;
|
||||
}
|
||||
|
||||
let text_position = get_relative_position(event.position);
|
||||
state.update(cx, |state, cx| {
|
||||
state.on_mouse_down(event, text_position, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
move |event: &MouseUpEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
if event.button != MouseButton::Left {
|
||||
return;
|
||||
}
|
||||
|
||||
state.update(cx, |state, cx| {
|
||||
state.on_mouse_up(event, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
move |event: &MouseMoveEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
|
||||
let text_position = get_relative_position(event.position);
|
||||
state.update(cx, |state, cx| {
|
||||
state.on_mouse_move(event, text_position, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
/*
|
||||
let content_size = match axis {
|
||||
gpui::Axis::Horizontal => {
|
||||
let state = input.read(cx);
|
||||
let line = state.lines().first();
|
||||
let line = line.and_then(|l| l.wrapped_line.as_ref());
|
||||
line.map(|w| w.width()).unwrap_or(px(0.))
|
||||
}
|
||||
gpui::Axis::Vertical => input.read(cx).total_content_height(),
|
||||
};
|
||||
let max_scroll = (content_size - bounds.size.along(axis)).max(px(0.));
|
||||
*/
|
||||
// TODO: Scroll mouse wheel
|
||||
move |event: &ScrollWheelEvent, phase, _window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
if !bounds.contains(&event.position) {
|
||||
return;
|
||||
}
|
||||
|
||||
// use shift to alter horizontal scroll on text area
|
||||
//event.modifiers.shift;
|
||||
|
||||
/*
|
||||
let pixel_delta = event.delta.pixel_delta(px(20.));
|
||||
state.update(cx, |state, cx| {
|
||||
let delta = match axis {
|
||||
gpui::Axis::Horizontal => pixel_delta.y,
|
||||
gpui::Axis::Vertical => {
|
||||
if pixel_delta.x.abs() > pixel_delta.y.abs() {
|
||||
pixel_delta.x
|
||||
} else {
|
||||
pixel_delta.y
|
||||
}
|
||||
}
|
||||
};
|
||||
state.apply_scroll_delta(delta, max_scroll);
|
||||
cx.notify();
|
||||
});
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
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, Some(bounds), window, cx);
|
||||
lines.push(line);
|
||||
}
|
||||
PrepaintElement::Quad(quad) => window.paint_quad(quad),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Render marked IME underlines
|
||||
});
|
||||
};
|
||||
self.interactivity.paint(
|
||||
self.shared_paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds.clone(),
|
||||
prepaint.hitbox.as_ref(),
|
||||
bounds,
|
||||
request_layout,
|
||||
prepaint,
|
||||
window,
|
||||
cx,
|
||||
perform_paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
use crate::editable_text::{
|
||||
TextInputStateBase, TextLayoutWrapping, 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,
|
||||
WrappedLine, fill, point, px, size,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
pub enum PrepaintElement {
|
||||
Line {
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct LayoutState<State> {
|
||||
pub state: Entity<State>,
|
||||
pub text_style: TextStyle,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct PrepaintState {
|
||||
pub hitbox: Option<Hitbox>,
|
||||
pub focus_handle: FocusHandle,
|
||||
pub elements: SmallVec<[PrepaintElement; 3]>,
|
||||
pub scroll_offset: Point<Pixels>,
|
||||
pub caret_visible: bool,
|
||||
}
|
||||
|
||||
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,
|
||||
global_id: Option<&gpui::GlobalElementId>,
|
||||
inspector_id: Option<&gpui::InspectorElementId>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
) -> (gpui::LayoutId, LayoutState<Self::State>) {
|
||||
// Fetches or initializes the internal state of the field
|
||||
let state = match &self.interactivity().element_id {
|
||||
None => unimplemented!("all input elements must be assigned an id"),
|
||||
Some(element_id) => {
|
||||
let state = window
|
||||
.use_keyed_state(element_id.clone(), cx, |_window, cx| self.init_state(cx));
|
||||
// store a reference to the entity owned by the element for access in action handlers
|
||||
*self.state_entity_rc().borrow_mut() = state.downgrade();
|
||||
state
|
||||
}
|
||||
};
|
||||
|
||||
let mut resolved_text_style = None;
|
||||
|
||||
self.interactivity()
|
||||
.track_focus(state.read(cx).focus_handle(cx));
|
||||
let layout_id = self.interactivity().request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx,
|
||||
|element_style, window, cx| {
|
||||
window.with_text_style(element_style.text_style().cloned(), |window| {
|
||||
resolved_text_style = Some(window.text_style());
|
||||
|
||||
let style = element_style.clone();
|
||||
// TODO: Does this need to propagate the line_height as the element's height?
|
||||
window.request_layout(style, None, cx)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let layout_state = LayoutState {
|
||||
state,
|
||||
text_style: resolved_text_style.unwrap_or_else(|| window.text_style()),
|
||||
};
|
||||
(layout_id, layout_state)
|
||||
}
|
||||
|
||||
fn shared_prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&gpui::GlobalElementId>,
|
||||
inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
request_layout: &mut LayoutState<Self::State>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
) -> PrepaintState {
|
||||
struct InteractivityPrepaint {
|
||||
hitbox: Option<Hitbox>,
|
||||
scroll_offset: Point<Pixels>,
|
||||
}
|
||||
// TODO: how do we enable scrolling? overflow on interactivity?
|
||||
let prepaint = self.interactivity().prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
bounds.size,
|
||||
window,
|
||||
cx,
|
||||
|_style, scroll_offset, hitbox, window, _cx| {
|
||||
let hitbox =
|
||||
hitbox.or_else(|| Some(window.insert_hitbox(bounds, HitboxBehavior::Normal)));
|
||||
InteractivityPrepaint {
|
||||
hitbox,
|
||||
scroll_offset,
|
||||
}
|
||||
},
|
||||
);
|
||||
let InteractivityPrepaint {
|
||||
hitbox,
|
||||
scroll_offset,
|
||||
} = prepaint;
|
||||
|
||||
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(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 = 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 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();
|
||||
|
||||
let mut elements = SmallVec::new();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 is_focused = focus_handle.is_focused(window);
|
||||
if !showing_placeholder && is_focused && cursor_visible {
|
||||
const CURSOR_WIDTH: f32 = 2.0;
|
||||
let quad = fill(
|
||||
Bounds::new(
|
||||
bounds.origin + carent_point - scroll_offset,
|
||||
size(gpui::px(CURSOR_WIDTH), line_height),
|
||||
),
|
||||
caret_color,
|
||||
);
|
||||
elements.push(PrepaintElement::Quad(quad));
|
||||
}
|
||||
|
||||
PrepaintState {
|
||||
hitbox,
|
||||
focus_handle,
|
||||
elements,
|
||||
scroll_offset,
|
||||
caret_visible: cursor_visible,
|
||||
}
|
||||
}
|
||||
|
||||
fn shared_paint(
|
||||
&mut self,
|
||||
global_id: Option<&gpui::GlobalElementId>,
|
||||
inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
request_layout: &mut LayoutState<Self::State>,
|
||||
prepaint: &mut PrepaintState,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
) where
|
||||
Self::State:
|
||||
for<'app> EditableTextActionHandler<'app, Context = Context<'app, Self::State>>,
|
||||
{
|
||||
if let Some(hitbox) = &prepaint.hitbox {
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
}
|
||||
|
||||
let perform_paint = |style: &Style, window: &mut Window, cx: &mut App| {
|
||||
if style.display == Display::None {
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: Skip when disabled
|
||||
let ime_handler = ElementInputHandler::new(bounds, request_layout.state.clone());
|
||||
window.handle_input(&prepaint.focus_handle, ime_handler, cx);
|
||||
|
||||
let get_relative_position = {
|
||||
let bounds = bounds.clone();
|
||||
move |position: Point<Pixels>| {
|
||||
// Converts a screen position to a position relative to the text area origin,
|
||||
// adjusted for scroll offset.
|
||||
let scroll_distance = gpui::px(0.); // TODO: STUB
|
||||
(position - bounds.origin)
|
||||
.apply_along(Axis::Horizontal, |pos| pos + scroll_distance)
|
||||
}
|
||||
};
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
if !bounds.contains(&event.position) {
|
||||
return;
|
||||
}
|
||||
if event.button != MouseButton::Left {
|
||||
return;
|
||||
}
|
||||
|
||||
let text_position = get_relative_position(event.position);
|
||||
state.update(cx, |state, cx| {
|
||||
state.on_mouse_down(event, text_position, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
move |event: &MouseUpEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
if event.button != MouseButton::Left {
|
||||
return;
|
||||
}
|
||||
|
||||
state.update(cx, |state, cx| {
|
||||
state.on_mouse_up(event, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
move |event: &MouseMoveEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
|
||||
let text_position = get_relative_position(event.position);
|
||||
state.update(cx, |state, cx| {
|
||||
state.on_mouse_move(event, text_position, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
window.on_mouse_event({
|
||||
let state = request_layout.state.clone();
|
||||
/*
|
||||
let content_size = match axis {
|
||||
gpui::Axis::Horizontal => {
|
||||
let state = input.read(cx);
|
||||
let line = state.lines().first();
|
||||
let line = line.and_then(|l| l.wrapped_line.as_ref());
|
||||
line.map(|w| w.width()).unwrap_or(px(0.))
|
||||
}
|
||||
gpui::Axis::Vertical => input.read(cx).total_content_height(),
|
||||
};
|
||||
let max_scroll = (content_size - bounds.size.along(axis)).max(px(0.));
|
||||
*/
|
||||
// TODO: Scroll mouse wheel
|
||||
move |event: &ScrollWheelEvent, phase, _window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
if !bounds.contains(&event.position) {
|
||||
return;
|
||||
}
|
||||
|
||||
// use shift to alter horizontal scroll on text area
|
||||
//event.modifiers.shift;
|
||||
|
||||
/*
|
||||
let pixel_delta = event.delta.pixel_delta(px(20.));
|
||||
state.update(cx, |state, cx| {
|
||||
let delta = match axis {
|
||||
gpui::Axis::Horizontal => pixel_delta.y,
|
||||
gpui::Axis::Vertical => {
|
||||
if pixel_delta.x.abs() > pixel_delta.y.abs() {
|
||||
pixel_delta.x
|
||||
} else {
|
||||
pixel_delta.y
|
||||
}
|
||||
}
|
||||
};
|
||||
state.apply_scroll_delta(delta, max_scroll);
|
||||
cx.notify();
|
||||
});
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
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, Some(bounds), window, cx);
|
||||
lines.push(line);
|
||||
}
|
||||
PrepaintElement::Quad(quad) => window.paint_quad(quad),
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
self.interactivity().paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds.clone(),
|
||||
prepaint.hitbox.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
perform_paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::editable_text::{
|
||||
TextBoundary, UnicodeTextStorage,
|
||||
actions::EditableTextActionHandler,
|
||||
notify::{TextChanged, TextHistoryPushed},
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, ClipboardItem, FocusHandle, Focusable, Hsla, NavigationDirection, Pixels, Point,
|
||||
SharedString, TextRun, TextStyle, UTF16Selection, Window, WrappedLine, point,
|
||||
App, Bounds, ClipboardItem, EntityInputHandler, FocusHandle, Focusable, Hsla,
|
||||
NavigationDirection, Pixels, Point, SharedString, TextRun, TextStyle, UTF16Selection, Window,
|
||||
WrappedLine, point,
|
||||
};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
@@ -14,6 +16,14 @@ pub trait TextStateNotifier {
|
||||
fn emit_history(&mut self, event: TextHistoryPushed);
|
||||
}
|
||||
|
||||
pub trait StateBackedEditableText {
|
||||
type State: 'static
|
||||
+ std::ops::Deref<Target = TextInputStateBase>
|
||||
+ std::ops::DerefMut
|
||||
+ EntityInputHandler
|
||||
+ for<'app> EditableTextActionHandler<'app>;
|
||||
}
|
||||
|
||||
pub struct TextInputStateBase {
|
||||
storage: Box<dyn UnicodeTextStorage>,
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::editable_text::{
|
||||
InitStorage, TextAreaState,
|
||||
InitStorage, StateBackedEditableText, TextAreaState,
|
||||
actions::{DEFAULT_INPUT_CONTEXT, EditableInputActionElement},
|
||||
shared_element::{self, EditableTextElement},
|
||||
};
|
||||
use gpui::{
|
||||
Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity, IntoElement,
|
||||
SharedString, StyleRefinement, Styled, TextStyle, WeakEntity,
|
||||
App, Bounds, Element, ElementId, Entity, Hitbox, InteractiveElement, Interactivity,
|
||||
IntoElement, Pixels, SharedString, StyleRefinement, Styled, TextStyle, WeakEntity, Window,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
@@ -34,6 +35,13 @@ pub struct TextAreaElement {
|
||||
placeholder: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl TextAreaElement {
|
||||
pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.placeholder = Some(text.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for TextAreaElement {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
@@ -53,31 +61,33 @@ impl IntoElement for TextAreaElement {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditableInputActionElement for TextAreaElement {
|
||||
impl StateBackedEditableText for TextAreaElement {
|
||||
type State = TextAreaState;
|
||||
}
|
||||
|
||||
impl EditableInputActionElement for TextAreaElement {
|
||||
fn state_entity_rc(&self) -> &Rc<RefCell<WeakEntity<Self::State>>> {
|
||||
&self.state_entity
|
||||
}
|
||||
}
|
||||
|
||||
pub mod element {
|
||||
use super::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct LayoutState {
|
||||
pub state: Entity<TextAreaState>,
|
||||
pub text_style: TextStyle,
|
||||
impl EditableTextElement for TextAreaElement {
|
||||
fn init_state(&self, cx: &mut gpui::prelude::Context<Self::State>) -> Self::State {
|
||||
Self::State::new(self.init_storage.exec(cx), cx)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct PrepaintState {
|
||||
pub hitbox: Option<Hitbox>,
|
||||
fn placeholder(&self) -> &Option<SharedString> {
|
||||
&self.placeholder
|
||||
}
|
||||
|
||||
fn should_wrap(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TextAreaElement {
|
||||
type RequestLayoutState = element::LayoutState;
|
||||
type PrepaintState = element::PrepaintState;
|
||||
type RequestLayoutState = shared_element::LayoutState<TextAreaState>;
|
||||
type PrepaintState = shared_element::PrepaintState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
@@ -91,74 +101,42 @@ impl Element for TextAreaElement {
|
||||
&mut self,
|
||||
global_id: Option<&gpui::GlobalElementId>,
|
||||
inspector_id: Option<&gpui::InspectorElementId>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
// Fetches or initializes the internal state of the field
|
||||
let state = match &self.interactivity.element_id {
|
||||
None => unimplemented!("all input elements must be assigned an id"),
|
||||
Some(element_id) => {
|
||||
let state = window.use_keyed_state(element_id.clone(), cx, |_window, cx| {
|
||||
TextAreaState::new(self.init_storage.exec(cx), cx)
|
||||
});
|
||||
// store a reference to the entity owned by the element for access in action handlers
|
||||
*self.state_entity.borrow_mut() = state.downgrade();
|
||||
state
|
||||
}
|
||||
};
|
||||
|
||||
let mut resolved_text_style = None;
|
||||
|
||||
let layout_id = self.interactivity.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx,
|
||||
|element_style, window, cx| {
|
||||
window.with_text_style(element_style.text_style().cloned(), |window| {
|
||||
resolved_text_style = Some(window.text_style());
|
||||
|
||||
let mut style = element_style.clone();
|
||||
if let gpui::Length::Auto = style.size.width {
|
||||
style.size.width = gpui::relative(1.).into();
|
||||
}
|
||||
if let gpui::Length::Auto = style.size.height {
|
||||
style.size.height = gpui::relative(1.).into();
|
||||
}
|
||||
window.request_layout(style, None, cx)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let layout_state = element::LayoutState {
|
||||
state,
|
||||
text_style: resolved_text_style.unwrap_or_else(|| window.text_style()),
|
||||
};
|
||||
(layout_id, layout_state)
|
||||
self.shared_request_layout(global_id, inspector_id, window, cx)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
id: Option<&gpui::GlobalElementId>,
|
||||
global_id: Option<&gpui::GlobalElementId>,
|
||||
inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
todo!()
|
||||
self.shared_prepaint(global_id, inspector_id, bounds, request_layout, window, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&gpui::GlobalElementId>,
|
||||
global_id: Option<&gpui::GlobalElementId>,
|
||||
inspector_id: Option<&gpui::InspectorElementId>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::App,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
todo!()
|
||||
self.shared_paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
request_layout,
|
||||
prepaint,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user