Detailed changes
@@ -1,16 +1,18 @@
//! The `Editor` entity — owns the truth about text content, cursor position,
//! blink state, and keyboard handling.
//!
-//! This is pure state with no rendering. It implements `EntityInputHandler` so
-//! the platform can deliver typed characters, and `Focusable` so the window
-//! knows where keyboard focus lives.
+//! Also contains `EditorText`, the low-level custom `Element` that shapes text
+//! and paints the cursor, and `EditorView`, a cached `View` wrapper that
+//! automatically pairs an `Editor` entity with its `EditorText` element.
+use std::hash::Hash;
use std::ops::Range;
use std::time::Duration;
use gpui::{
- App, Bounds, Context, EntityInputHandler, FocusHandle, Focusable, Pixels, Task, UTF16Selection,
- Window,
+ App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable,
+ Hsla, IntoViewElement, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Task, TextRun,
+ UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size,
};
use unicode_segmentation::*;
@@ -254,3 +256,232 @@ impl EntityInputHandler for Editor {
None
}
}
+
+// ---------------------------------------------------------------------------
+// EditorText — custom Element that shapes text & paints the cursor
+// ---------------------------------------------------------------------------
+
+struct EditorText {
+ editor: Entity<Editor>,
+ text_color: Hsla,
+}
+
+struct EditorTextPrepaintState {
+ lines: Vec<ShapedLine>,
+ cursor: Option<PaintQuad>,
+}
+
+impl EditorText {
+ pub fn new(editor: Entity<Editor>, text_color: Hsla) -> Self {
+ Self { editor, text_color }
+ }
+}
+
+impl IntoElement for EditorText {
+ type Element = Self;
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Element for EditorText {
+ type RequestLayoutState = ();
+ type PrepaintState = EditorTextPrepaintState;
+
+ fn id(&self) -> Option<gpui::ElementId> {
+ None
+ }
+
+ fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+ None
+ }
+
+ fn request_layout(
+ &mut self,
+ _id: Option<&gpui::GlobalElementId>,
+ _inspector_id: Option<&gpui::InspectorElementId>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> (LayoutId, Self::RequestLayoutState) {
+ let line_count = self.editor.read(cx).content.split('\n').count().max(1);
+ let line_height = window.line_height();
+ let mut style = gpui::Style::default();
+ style.size.width = relative(1.).into();
+ style.size.height = (line_height * line_count as f32).into();
+ (window.request_layout(style, [], cx), ())
+ }
+
+ fn prepaint(
+ &mut self,
+ _id: Option<&gpui::GlobalElementId>,
+ _inspector_id: Option<&gpui::InspectorElementId>,
+ bounds: Bounds<Pixels>,
+ _request_layout: &mut Self::RequestLayoutState,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self::PrepaintState {
+ let editor = self.editor.read(cx);
+ let content = &editor.content;
+ let cursor_offset = editor.cursor;
+ let cursor_visible = editor.cursor_visible;
+ let is_focused = editor.focus_handle.is_focused(window);
+
+ let style = window.text_style();
+ let font_size = style.font_size.to_pixels(window.rem_size());
+ let line_height = window.line_height();
+
+ let is_placeholder = content.is_empty();
+
+ let shaped_lines: Vec<ShapedLine> = if is_placeholder {
+ let placeholder: SharedString = "Type here...".into();
+ let run = TextRun {
+ len: placeholder.len(),
+ font: style.font(),
+ color: hsla(0., 0., 0.5, 0.5),
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ };
+ vec![
+ window
+ .text_system()
+ .shape_line(placeholder, font_size, &[run], None),
+ ]
+ } else {
+ content
+ .split('\n')
+ .map(|line_str| {
+ let text: SharedString = SharedString::from(line_str.to_string());
+ let run = TextRun {
+ len: text.len(),
+ font: style.font(),
+ color: self.text_color,
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ };
+ window
+ .text_system()
+ .shape_line(text, font_size, &[run], None)
+ })
+ .collect()
+ };
+
+ let cursor = if is_focused && cursor_visible && !is_placeholder {
+ let (cursor_line, offset_in_line) = cursor_line_and_offset(content, cursor_offset);
+ let cursor_line = cursor_line.min(shaped_lines.len().saturating_sub(1));
+ let cursor_x = shaped_lines[cursor_line].x_for_index(offset_in_line);
+
+ Some(fill(
+ Bounds::new(
+ point(
+ bounds.left() + cursor_x,
+ bounds.top() + line_height * cursor_line as f32,
+ ),
+ size(px(1.5), line_height),
+ ),
+ self.text_color,
+ ))
+ } else if is_focused && cursor_visible && is_placeholder {
+ Some(fill(
+ Bounds::new(
+ point(bounds.left(), bounds.top()),
+ size(px(1.5), line_height),
+ ),
+ self.text_color,
+ ))
+ } else {
+ None
+ };
+
+ EditorTextPrepaintState {
+ lines: shaped_lines,
+ cursor,
+ }
+ }
+
+ fn paint(
+ &mut self,
+ _id: Option<&gpui::GlobalElementId>,
+ _inspector_id: Option<&gpui::InspectorElementId>,
+ bounds: Bounds<Pixels>,
+ _request_layout: &mut Self::RequestLayoutState,
+ prepaint: &mut Self::PrepaintState,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ let focus_handle = self.editor.read(cx).focus_handle.clone();
+
+ window.handle_input(
+ &focus_handle,
+ ElementInputHandler::new(bounds, self.editor.clone()),
+ cx,
+ );
+
+ let line_height = window.line_height();
+ for (i, line) in prepaint.lines.iter().enumerate() {
+ let origin = point(bounds.left(), bounds.top() + line_height * i as f32);
+ line.paint(origin, line_height, gpui::TextAlign::Left, None, window, cx)
+ .unwrap();
+ }
+
+ if let Some(cursor) = prepaint.cursor.take() {
+ window.paint_quad(cursor);
+ }
+ }
+}
+
+fn cursor_line_and_offset(content: &str, cursor: usize) -> (usize, usize) {
+ let mut line_index = 0;
+ let mut line_start = 0;
+ for (i, ch) in content.char_indices() {
+ if i >= cursor {
+ break;
+ }
+ if ch == '\n' {
+ line_index += 1;
+ line_start = i + 1;
+ }
+ }
+ (line_index, cursor - line_start)
+}
+
+// ---------------------------------------------------------------------------
+// EditorView — a cached View that pairs an Editor entity with EditorText
+// ---------------------------------------------------------------------------
+
+/// A simple cached view that renders an `Editor` entity via the `EditorText`
+/// custom element. Use this when you want a bare editor display with automatic
+/// caching and no extra chrome.
+#[derive(IntoViewElement, Hash)]
+pub struct EditorView {
+ editor: Entity<Editor>,
+ text_color: Hsla,
+}
+
+impl EditorView {
+ pub fn new(editor: Entity<Editor>) -> Self {
+ Self {
+ editor,
+ text_color: hsla(0., 0., 0.1, 1.),
+ }
+ }
+
+ pub fn text_color(mut self, color: Hsla) -> Self {
+ self.text_color = color;
+ self
+ }
+}
+
+impl gpui::View for EditorView {
+ type State = Editor;
+
+ fn entity(&self) -> &Entity<Editor> {
+ &self.editor
+ }
+
+ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ EditorText::new(self.editor, self.text_color)
+ }
+}
@@ -1,200 +0,0 @@
-//! The `EditorText` element — turns `Editor` state into pixels.
-//!
-//! This is a custom `Element` implementation that handles the low-level work:
-//! - Shapes text into `ShapedLine`s during prepaint (one per hard line break)
-//! - Computes the cursor quad position across multiple lines
-//! - Paints the shaped text and cursor during paint
-//! - Calls `window.handle_input()` during paint to wire platform text input
-
-use gpui::{
- App, Bounds, ElementInputHandler, Entity, Hsla, LayoutId, PaintQuad, Pixels, ShapedLine,
- SharedString, TextRun, Window, fill, hsla, point, prelude::*, px, relative, size,
-};
-
-use crate::editor::Editor;
-
-pub struct EditorText {
- editor: Entity<Editor>,
- text_color: Hsla,
-}
-
-pub struct PrepaintState {
- lines: Vec<ShapedLine>,
- cursor: Option<PaintQuad>,
-}
-
-impl EditorText {
- pub fn new(editor: Entity<Editor>, text_color: Hsla) -> Self {
- Self { editor, text_color }
- }
-}
-
-impl IntoElement for EditorText {
- type Element = Self;
-
- fn into_element(self) -> Self::Element {
- self
- }
-}
-
-impl Element for EditorText {
- type RequestLayoutState = ();
- type PrepaintState = PrepaintState;
-
- fn id(&self) -> Option<gpui::ElementId> {
- None
- }
-
- fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
- None
- }
-
- fn request_layout(
- &mut self,
- _id: Option<&gpui::GlobalElementId>,
- _inspector_id: Option<&gpui::InspectorElementId>,
- window: &mut Window,
- cx: &mut App,
- ) -> (LayoutId, Self::RequestLayoutState) {
- let line_count = self.editor.read(cx).content.split('\n').count().max(1);
- let line_height = window.line_height();
- let mut style = gpui::Style::default();
- style.size.width = relative(1.).into();
- style.size.height = (line_height * line_count as f32).into();
- (window.request_layout(style, [], cx), ())
- }
-
- fn prepaint(
- &mut self,
- _id: Option<&gpui::GlobalElementId>,
- _inspector_id: Option<&gpui::InspectorElementId>,
- bounds: Bounds<Pixels>,
- _request_layout: &mut Self::RequestLayoutState,
- window: &mut Window,
- cx: &mut App,
- ) -> Self::PrepaintState {
- let editor = self.editor.read(cx);
- let content = &editor.content;
- let cursor_offset = editor.cursor;
- let cursor_visible = editor.cursor_visible;
- let is_focused = editor.focus_handle.is_focused(window);
-
- let style = window.text_style();
- let font_size = style.font_size.to_pixels(window.rem_size());
- let line_height = window.line_height();
-
- let is_placeholder = content.is_empty();
-
- let shaped_lines: Vec<ShapedLine> = if is_placeholder {
- let placeholder: SharedString = "Type here...".into();
- let run = TextRun {
- len: placeholder.len(),
- font: style.font(),
- color: hsla(0., 0., 0.5, 0.5),
- background_color: None,
- underline: None,
- strikethrough: None,
- };
- vec![
- window
- .text_system()
- .shape_line(placeholder, font_size, &[run], None),
- ]
- } else {
- content
- .split('\n')
- .map(|line_str| {
- let text: SharedString = SharedString::from(line_str.to_string());
- let run = TextRun {
- len: text.len(),
- font: style.font(),
- color: self.text_color,
- background_color: None,
- underline: None,
- strikethrough: None,
- };
- window
- .text_system()
- .shape_line(text, font_size, &[run], None)
- })
- .collect()
- };
-
- let cursor = if is_focused && cursor_visible && !is_placeholder {
- let (cursor_line, offset_in_line) = cursor_line_and_offset(content, cursor_offset);
- let cursor_line = cursor_line.min(shaped_lines.len().saturating_sub(1));
- let cursor_x = shaped_lines[cursor_line].x_for_index(offset_in_line);
-
- Some(fill(
- Bounds::new(
- point(
- bounds.left() + cursor_x,
- bounds.top() + line_height * cursor_line as f32,
- ),
- size(px(1.5), line_height),
- ),
- self.text_color,
- ))
- } else if is_focused && cursor_visible && is_placeholder {
- Some(fill(
- Bounds::new(
- point(bounds.left(), bounds.top()),
- size(px(1.5), line_height),
- ),
- self.text_color,
- ))
- } else {
- None
- };
-
- PrepaintState {
- lines: shaped_lines,
- cursor,
- }
- }
-
- fn paint(
- &mut self,
- _id: Option<&gpui::GlobalElementId>,
- _inspector_id: Option<&gpui::InspectorElementId>,
- bounds: Bounds<Pixels>,
- _request_layout: &mut Self::RequestLayoutState,
- prepaint: &mut Self::PrepaintState,
- window: &mut Window,
- cx: &mut App,
- ) {
- let focus_handle = self.editor.read(cx).focus_handle.clone();
-
- window.handle_input(
- &focus_handle,
- ElementInputHandler::new(bounds, self.editor.clone()),
- cx,
- );
-
- let line_height = window.line_height();
- for (i, line) in prepaint.lines.iter().enumerate() {
- let origin = point(bounds.left(), bounds.top() + line_height * i as f32);
- line.paint(origin, line_height, gpui::TextAlign::Left, None, window, cx)
- .unwrap();
- }
-
- if let Some(cursor) = prepaint.cursor.take() {
- window.paint_quad(cursor);
- }
- }
-}
-
-fn cursor_line_and_offset(content: &str, cursor: usize) -> (usize, usize) {
- let mut line_index = 0;
- let mut line_start = 0;
- for (i, ch) in content.char_indices() {
- if i >= cursor {
- break;
- }
- if ch == '\n' {
- line_index += 1;
- line_start = i + 1;
- }
- }
- (line_index, cursor - line_start)
-}
@@ -8,20 +8,20 @@
use std::time::Duration;
use gpui::{
- Animation, AnimationExt as _, App, BoxShadow, CursorStyle, Entity, Hsla, Pixels, SharedString,
- StyleRefinement, ViewElement, Window, bounce, div, ease_in_out, hsla, point, prelude::*, px,
- white,
+ Animation, AnimationExt as _, App, BoxShadow, CursorStyle, Entity, Hsla, IntoViewElement,
+ Pixels, SharedString, StyleRefinement, Window, bounce, div, ease_in_out, hsla, point,
+ prelude::*, px, white,
};
use crate::editor::Editor;
-use crate::editor_text::EditorText;
+use crate::editor::EditorView;
use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
struct FlashState {
count: usize,
}
-#[derive(Hash)]
+#[derive(Hash, IntoViewElement)]
pub struct Input {
editor: Entity<Editor>,
width: Option<Pixels>,
@@ -55,6 +55,15 @@ impl gpui::View for Input {
&self.editor
}
+ fn style(&self) -> Option<StyleRefinement> {
+ let mut style = StyleRefinement::default();
+ if let Some(w) = self.width {
+ style.size.width = Some(w.into());
+ }
+ style.size.height = Some(px(36.).into());
+ Some(style)
+ }
+
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let flash_state = window.use_state(cx, |_window, _cx| FlashState { count: 0 });
let count = flash_state.read(cx).count;
@@ -145,7 +154,7 @@ impl gpui::View for Input {
.line_height(px(20.))
.text_size(px(14.))
.text_color(text_color)
- .child(EditorText::new(editor, text_color));
+ .child(EditorView::new(editor).text_color(text_color));
if count > 0 {
base.with_animation(
@@ -164,16 +173,3 @@ impl gpui::View for Input {
}
}
}
-
-impl IntoElement for Input {
- type Element = ViewElement<Self>;
-
- fn into_element(self) -> Self::Element {
- let mut style = StyleRefinement::default();
- if let Some(w) = self.width {
- style.size.width = Some(w.into());
- }
- style.size.height = Some(px(36.).into());
- ViewElement::new(self).cached(style)
- }
-}
@@ -28,7 +28,6 @@
//! ```
mod editor;
-mod editor_text;
mod input;
mod text_area;
@@ -5,15 +5,15 @@
//! components with different props and layouts.
use gpui::{
- App, BoxShadow, CursorStyle, Entity, Hsla, StyleRefinement, ViewElement, Window, div, hsla,
+ App, BoxShadow, CursorStyle, Entity, Hsla, IntoViewElement, StyleRefinement, Window, div, hsla,
point, prelude::*, px, white,
};
use crate::editor::Editor;
-use crate::editor_text::EditorText;
+use crate::editor::EditorView;
use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
-#[derive(Hash)]
+#[derive(Hash, IntoViewElement)]
pub struct TextArea {
editor: Entity<Editor>,
rows: usize,
@@ -42,6 +42,15 @@ impl gpui::View for TextArea {
&self.editor
}
+ fn style(&self) -> Option<StyleRefinement> {
+ let row_height = px(20.);
+ let box_height = row_height * self.rows as f32 + px(16.);
+ let mut style = StyleRefinement::default();
+ style.size.width = Some(px(400.).into());
+ style.size.height = Some(box_height.into());
+ Some(style)
+ }
+
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let focus_handle = self.editor.read(cx).focus_handle.clone();
let is_focused = focus_handle.is_focused(window);
@@ -120,19 +129,6 @@ impl gpui::View for TextArea {
.line_height(row_height)
.text_size(px(14.))
.text_color(text_color)
- .child(EditorText::new(editor, text_color))
- }
-}
-
-impl IntoElement for TextArea {
- type Element = ViewElement<Self>;
-
- fn into_element(self) -> Self::Element {
- let row_height = px(20.);
- let box_height = row_height * self.rows as f32 + px(16.);
- let mut style = StyleRefinement::default();
- style.size.width = Some(px(400.).into());
- style.size.height = Some(box_height.into());
- ViewElement::new(self).cached(style)
+ .child(EditorView::new(editor).text_color(text_color))
}
}
@@ -90,7 +90,8 @@ pub use executor::*;
pub use geometry::*;
pub use global::*;
pub use gpui_macros::{
- AppContext, IntoElement, Render, VisualContext, property_test, register_action, test,
+ AppContext, IntoElement, IntoViewElement, Render, VisualContext, property_test,
+ register_action, test,
};
pub use gpui_util::arc_cow::ArcCow;
pub use http_client;
@@ -1,7 +1,7 @@
use crate::{
AnyElement, AnyEntity, AnyWeakEntity, App, Bounds, ContentMask, Context, Element, ElementId,
Entity, EntityId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, PaintIndex,
- Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity,
+ Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity, relative,
};
use crate::{Empty, Window};
use anyhow::Result;
@@ -372,6 +372,17 @@ pub trait View: 'static + Sized + Hash {
/// Render this view into an element tree. Takes ownership of self,
/// consuming the component props. The entity state persists across frames.
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement;
+
+ /// Returns the style to use for caching this view.
+ /// When `Some`, the view element will be cached using the given style for its outer layout.
+ /// The default returns a full-size style refinement (`width: 100%, height: 100%`).
+ /// Return `None` to disable caching.
+ fn style(&self) -> Option<StyleRefinement> {
+ let mut style = StyleRefinement::default();
+ style.size.width = Some(relative(1.).into());
+ style.size.height = Some(relative(1.).into());
+ Some(style)
+ }
}
/// An element that wraps a [`View`], creating a reactive boundary in the element tree.
@@ -0,0 +1,28 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{DeriveInput, parse_macro_input};
+
+pub fn derive_into_view_element(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as DeriveInput);
+ let type_name = &ast.ident;
+ let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
+
+ let r#gen = quote! {
+ impl #impl_generics gpui::IntoElement for #type_name #type_generics
+ #where_clause
+ {
+ type Element = gpui::ViewElement<Self>;
+
+ fn into_element(self) -> Self::Element {
+ let style = gpui::View::style(&self);
+ let element = gpui::ViewElement::new(self);
+ match style {
+ Some(s) => element.cached(s),
+ None => element,
+ }
+ }
+ }
+ };
+
+ r#gen.into()
+}
@@ -1,6 +1,7 @@
mod derive_action;
mod derive_app_context;
mod derive_into_element;
+mod derive_into_view_element;
mod derive_render;
mod derive_visual_context;
mod property_test;
@@ -35,6 +36,14 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream {
derive_into_element::derive_into_element(input)
}
+/// #[derive(IntoViewElement)] generates an `IntoElement` implementation for types
+/// that implement the `View` trait. It creates a `ViewElement` wrapper and
+/// automatically enables caching based on the `View::style()` method.
+#[proc_macro_derive(IntoViewElement)]
+pub fn derive_into_view_element(input: TokenStream) -> TokenStream {
+ derive_into_view_element::derive_into_view_element(input)
+}
+
#[proc_macro_derive(Render)]
#[doc(hidden)]
pub fn derive_render(input: TokenStream) -> TokenStream {