use gpui::{
App, Bounds, Element, ElementId, ElementInputHandler, Entity, GlobalElementId, IntoElement,
LayoutId, PaintQuad, Pixels, Style, TextRun, UnderlineStyle, Window, fill, point, px, relative,
size,
};
use gpui_kit_theme::ActiveTheme;
use super::TextArea;
use super::layout::Layout;
pub struct TextAreaElement {
area: Entity<TextArea>,
}
impl TextAreaElement {
pub fn new(area: Entity<TextArea>) -> Self {
Self { area }
}
}
pub struct PrepaintState {
layout: Option<Layout>,
source_text: gpui::SharedString,
cursor: Option<PaintQuad>,
selection: Vec<PaintQuad>,
scroll_offset: Pixels,
visible_rows: usize,
}
impl IntoElement for TextAreaElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for TextAreaElement {
type RequestLayoutState = ();
type PrepaintState = PrepaintState;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let rows = self.area.read(cx).visible_rows();
let mut style = Style::default();
style.size.width = relative(1.0).into();
style.size.height = (window.line_height() * rows as f32).into();
(window.request_layout(style, [], cx), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
let theme = cx.theme().clone();
let area = self.area.read(cx);
let content = area.value().clone();
let source_text = content.clone();
let selected = area.selected_range();
let cursor = area.cursor_offset();
let marked = area.marked_range();
let (min_rows, max_rows) = area.row_limits();
let empty = content.is_empty();
let style = window.text_style();
let (display_text, text_color) = if empty {
(area.placeholder_text().clone(), theme.colors.text_faint)
} else {
(content, style.color)
};
let run = TextRun {
len: display_text.len(),
font: style.font(),
color: text_color,
background_color: None,
underline: None,
strikethrough: None,
};
let runs = match marked {
Some(marked) if marked.end <= display_text.len() => vec![
TextRun {
len: marked.start,
..run.clone()
},
TextRun {
len: marked.end - marked.start,
underline: Some(UnderlineStyle {
color: Some(run.color),
thickness: px(1.0),
wavy: false,
}),
..run.clone()
},
TextRun {
len: display_text.len() - marked.end,
..run
},
]
.into_iter()
.filter(|run| run.len > 0)
.collect(),
_ => vec![run],
};
let font_size = style.font_size.to_pixels(window.rem_size());
let line_height = window.line_height();
let lines = window
.text_system()
.shape_text(
display_text,
font_size,
&runs,
Some(bounds.size.width),
None,
)
.map(|lines| lines.into_iter().collect::<Vec<_>>())
.unwrap_or_default();
let layout = Layout::new(lines, line_height);
let visible_rows = if empty {
min_rows
} else {
layout.total_rows().clamp(min_rows, max_rows)
};
let caret = layout.position_for_offset(cursor);
let mut scroll_offset = self
.area
.read(cx)
.scroll_offset()
.min((layout.height() - bounds.size.height).max(px(0.0)))
.max(px(0.0));
if caret.y < scroll_offset {
scroll_offset = caret.y;
}
if caret.y + line_height > scroll_offset + bounds.size.height {
scroll_offset = caret.y + line_height - bounds.size.height;
}
let origin = point(bounds.left(), bounds.top() - scroll_offset);
let cursor = (selected.is_empty()).then(|| {
fill(
Bounds::new(
point(origin.x + caret.x, origin.y + caret.y),
size(px(1.5), line_height),
),
theme.colors.accent,
)
});
let mut selection = Vec::new();
if !selected.is_empty() {
let first = layout.row_for_offset(selected.start);
let last = layout.row_for_offset(selected.end);
for row in first..=last {
let span = layout.row_range(row);
let start = selected.start.max(span.start);
let end = selected.end.min(span.end);
if start > end {
continue;
}
let left = layout.position_for_offset(start);
let right = layout.position_for_offset(end);
let top = line_height * row as f32;
selection.push(fill(
Bounds::from_corners(
point(origin.x + left.x, origin.y + top),
point(origin.x + right.x, origin.y + top + line_height),
),
theme.colors.selected,
));
}
}
PrepaintState {
layout: Some(layout),
source_text,
cursor,
selection,
scroll_offset,
visible_rows,
}
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let (focus_handle, disabled) = {
let area = self.area.read(cx);
(area.focus_handle.clone(), area.disabled)
};
if !disabled {
window.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.area.clone()),
cx,
);
}
let scroll_offset = prepaint.scroll_offset;
let visible_rows = prepaint.visible_rows;
window.with_content_mask(Some(gpui::ContentMask { bounds }), |window| {
for selection in prepaint.selection.drain(..) {
window.paint_quad(selection);
}
if let Some(layout) = prepaint.layout.take() {
for (line, top) in layout.painted_lines() {
line.paint(
point(bounds.origin.x, bounds.origin.y + top - scroll_offset),
window.line_height(),
gpui::TextAlign::Left,
None,
window,
cx,
)
.ok();
}
self.area.update(cx, |area, cx| {
let grew = area.visible_rows() != visible_rows;
area.set_visible_rows(visible_rows);
area.set_scroll_offset(scroll_offset);
let accessibility_layout_changed =
area.set_last_layout(layout, prepaint.source_text.clone(), bounds);
if grew || accessibility_layout_changed {
cx.notify();
}
});
}
if !disabled
&& focus_handle.is_focused(window)
&& let Some(cursor) = prepaint.cursor.take()
{
window.paint_quad(cursor);
}
});
}
}