Unify Render and RenderOnce into View (#58087)
This allows us to build powerful and flexible Input and TextArea components Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c80020231b
commit
74b5207744
12 files changed
+1464
-298
No files matched your search
@@ -256,3 +256,7 @@ path = "examples/mouse_pressure.rs"
|
||||
[[example]]
|
||||
name = "a11y"
|
||||
path = "examples/a11y.rs"
|
||||
|
||||
[[example]]
|
||||
name = "view_example"
|
||||
path = "examples/view_example/view_example_main.rs"
|
||||
@@ -0,0 +1,549 @@
|
||||
//! `Editor` — the workhorse entity. It owns the cursor, blink, focus, keyboard
|
||||
//! handling, and the specialized text-shaping renderer. The *text itself* lives
|
||||
//! in a shared `Entity<String>` it's handed at construction, so the value is
|
||||
//! readable/writable from outside while the editing machinery stays in here.
|
||||
//!
|
||||
//! This is the piece that proves the point: a text input is genuinely
|
||||
//! complicated, and `View` lets all of that complexity live in one entity that
|
||||
//! anything can embed.
|
||||
|
||||
use std::ops::Range;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable,
|
||||
InteractiveElement, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Subscription, Task,
|
||||
TextRun, UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size,
|
||||
};
|
||||
use unicode_segmentation::*;
|
||||
|
||||
use crate::{Backspace, Delete, End, Home, Left, Right};
|
||||
|
||||
pub struct Editor {
|
||||
pub value: Entity<String>,
|
||||
pub focus_handle: FocusHandle,
|
||||
pub cursor: usize,
|
||||
pub cursor_visible: bool,
|
||||
_blink_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
/// An editor that owns its own string internally, seeded with `text`.
|
||||
/// Nothing to allocate or wire up at the call site.
|
||||
pub fn new(text: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let value = cx.new(|_| text.into());
|
||||
Self::over(value, window, cx)
|
||||
}
|
||||
|
||||
/// An editor over a string *you* own, so the value is shared in and out.
|
||||
pub fn over(value: Entity<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| {
|
||||
this.start_blink(cx);
|
||||
});
|
||||
let blur_sub = cx.on_blur(&focus_handle, window, |this, _window, cx| {
|
||||
this.stop_blink(cx);
|
||||
});
|
||||
|
||||
// The value is shared: anything can write it while we hold a cursor into
|
||||
// it. Observe it so external writes (a) clamp the cursor back onto a char
|
||||
// boundary before the next IME round-trip can slice out of bounds, and
|
||||
// (b) notify us, so an `editor.cached(..)` subtree re-renders — the cache
|
||||
// is keyed on *our* notify, not the value's.
|
||||
let value_sub = cx.observe(&value, |this, value, cx| {
|
||||
let content = value.read(cx);
|
||||
let mut cursor = this.cursor.min(content.len());
|
||||
while cursor > 0 && !content.is_char_boundary(cursor) {
|
||||
cursor -= 1;
|
||||
}
|
||||
this.cursor = cursor;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Self {
|
||||
value,
|
||||
focus_handle,
|
||||
cursor: 0,
|
||||
cursor_visible: false,
|
||||
_blink_task: Task::ready(()),
|
||||
_subscriptions: vec![focus_sub, blur_sub, value_sub],
|
||||
}
|
||||
}
|
||||
|
||||
/// The current text. Read this from anywhere to get the value out.
|
||||
pub fn text(&self, cx: &App) -> String {
|
||||
self.value.read(cx).clone()
|
||||
}
|
||||
|
||||
fn start_blink(&mut self, cx: &mut Context<Self>) {
|
||||
self.cursor_visible = true;
|
||||
self._blink_task = Self::spawn_blink_task(cx);
|
||||
}
|
||||
|
||||
fn stop_blink(&mut self, cx: &mut Context<Self>) {
|
||||
self.cursor_visible = false;
|
||||
self._blink_task = Task::ready(());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn spawn_blink_task(cx: &mut Context<Self>) -> Task<()> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(500))
|
||||
.await;
|
||||
let result = this.update(cx, |editor, cx| {
|
||||
editor.cursor_visible = !editor.cursor_visible;
|
||||
cx.notify();
|
||||
});
|
||||
if result.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn reset_blink(&mut self, cx: &mut Context<Self>) {
|
||||
self.cursor_visible = true;
|
||||
self._blink_task = Self::spawn_blink_task(cx);
|
||||
}
|
||||
|
||||
pub fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let content = self.text(cx);
|
||||
if self.cursor > 0 {
|
||||
self.cursor = previous_boundary(&content, self.cursor);
|
||||
}
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let content = self.text(cx);
|
||||
if self.cursor < content.len() {
|
||||
self.cursor = next_boundary(&content, self.cursor);
|
||||
}
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cursor = 0;
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cursor = self.text(cx).len();
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let content = self.text(cx);
|
||||
if self.cursor > 0 {
|
||||
let prev = previous_boundary(&content, self.cursor);
|
||||
let cursor = self.cursor;
|
||||
self.value.update(cx, |s, cx| {
|
||||
s.drain(prev..cursor);
|
||||
cx.notify();
|
||||
});
|
||||
self.cursor = prev;
|
||||
}
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let content = self.text(cx);
|
||||
if self.cursor < content.len() {
|
||||
let next = next_boundary(&content, self.cursor);
|
||||
let cursor = self.cursor;
|
||||
self.value.update(cx, |s, cx| {
|
||||
s.drain(cursor..next);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn insert_newline(&mut self, cx: &mut Context<Self>) {
|
||||
let cursor = self.cursor;
|
||||
self.value.update(cx, |s, cx| {
|
||||
s.insert(cursor, '\n');
|
||||
cx.notify();
|
||||
});
|
||||
self.cursor += 1;
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn previous_boundary(content: &str, offset: usize) -> usize {
|
||||
content
|
||||
.grapheme_indices(true)
|
||||
.rev()
|
||||
.find_map(|(idx, _)| (idx < offset).then_some(idx))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn next_boundary(content: &str, offset: usize) -> usize {
|
||||
content
|
||||
.grapheme_indices(true)
|
||||
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||
.unwrap_or(content.len())
|
||||
}
|
||||
|
||||
fn offset_from_utf16(content: &str, offset: usize) -> usize {
|
||||
let mut utf8_offset = 0;
|
||||
let mut utf16_count = 0;
|
||||
for ch in content.chars() {
|
||||
if utf16_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf16_count += ch.len_utf16();
|
||||
utf8_offset += ch.len_utf8();
|
||||
}
|
||||
utf8_offset
|
||||
}
|
||||
|
||||
fn offset_to_utf16(content: &str, offset: usize) -> usize {
|
||||
let mut utf16_offset = 0;
|
||||
let mut utf8_count = 0;
|
||||
for ch in content.chars() {
|
||||
if utf8_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf8_count += ch.len_utf8();
|
||||
utf16_offset += ch.len_utf16();
|
||||
}
|
||||
utf16_offset
|
||||
}
|
||||
|
||||
fn range_to_utf16(content: &str, range: &Range<usize>) -> Range<usize> {
|
||||
offset_to_utf16(content, range.start)..offset_to_utf16(content, range.end)
|
||||
}
|
||||
|
||||
fn range_from_utf16(content: &str, range_utf16: &Range<usize>) -> Range<usize> {
|
||||
offset_from_utf16(content, range_utf16.start)..offset_from_utf16(content, range_utf16.end)
|
||||
}
|
||||
|
||||
impl Focusable for Editor {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityInputHandler for Editor {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
actual_range: &mut Option<Range<usize>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<String> {
|
||||
let content = self.text(cx);
|
||||
let range = range_from_utf16(&content, &range_utf16);
|
||||
actual_range.replace(range_to_utf16(&content, &range));
|
||||
Some(content[range].to_string())
|
||||
}
|
||||
|
||||
fn selected_text_range(
|
||||
&mut self,
|
||||
_ignore_disabled_input: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<UTF16Selection> {
|
||||
let content = self.text(cx);
|
||||
let utf16_cursor = offset_to_utf16(&content, self.cursor);
|
||||
Some(UTF16Selection {
|
||||
range: utf16_cursor..utf16_cursor,
|
||||
reversed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn marked_text_range(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Range<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let content = self.text(cx);
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|r| range_from_utf16(&content, r))
|
||||
.unwrap_or(self.cursor..self.cursor);
|
||||
|
||||
let new_content = content[..range.start].to_owned() + new_text + &content[range.end..];
|
||||
self.cursor = range.start + new_text.len();
|
||||
self.value.update(cx, |s, cx| {
|
||||
*s = new_content;
|
||||
cx.notify();
|
||||
});
|
||||
self.reset_blink(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
_new_selected_range_utf16: Option<Range<usize>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.replace_text_in_range(range_utf16, new_text, window, cx);
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
_range_utf16: Range<usize>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn character_index_for_point(
|
||||
&mut self,
|
||||
_point: gpui::Point<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Render for Editor {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Editor>) -> impl IntoElement {
|
||||
EditorText {
|
||||
editor: cx.entity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EditorText — the specialized renderer: shapes the text and paints the cursor.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct EditorText {
|
||||
editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
struct EditorTextPrepaint {
|
||||
lines: Vec<ShapedLine>,
|
||||
cursor: Option<PaintQuad>,
|
||||
}
|
||||
|
||||
impl IntoElement for EditorText {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for EditorText {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = EditorTextPrepaint;
|
||||
|
||||
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 editor = self.editor.read(cx);
|
||||
let content = editor.value.read(cx);
|
||||
let line_count = 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.value.read(cx).clone();
|
||||
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 text_color = style.color;
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let line_height = window.line_height();
|
||||
|
||||
let is_placeholder = content.is_empty();
|
||||
|
||||
let 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: 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(lines.len().saturating_sub(1));
|
||||
let cursor_x = 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),
|
||||
),
|
||||
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),
|
||||
),
|
||||
text_color,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
EditorTextPrepaint { 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)
|
||||
}
|
||||
|
||||
pub fn standard_actions<E: InteractiveElement>(editor: Entity<Editor>) -> impl FnOnce(E) -> E {
|
||||
move |element| {
|
||||
element
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &Left, window, cx| editor.update(cx, |e, cx| e.left(a, window, cx))
|
||||
})
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &Right, window, cx| editor.update(cx, |e, cx| e.right(a, window, cx))
|
||||
})
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &Home, window, cx| editor.update(cx, |e, cx| e.home(a, window, cx))
|
||||
})
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &End, window, cx| editor.update(cx, |e, cx| e.end(a, window, cx))
|
||||
})
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |a: &Backspace, window, cx| {
|
||||
editor.update(cx, |e, cx| e.backspace(a, window, cx))
|
||||
}
|
||||
})
|
||||
.on_action(move |a: &Delete, window, cx| {
|
||||
editor.update(cx, |e, cx| e.delete(a, window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! `Input` — a single-line text input. The shaping layer over `Editor`.
|
||||
//!
|
||||
//! Construct it two ways, depending on how much state you want to own:
|
||||
//! * `Input::new(value: Entity<String>)` — you hold just the string; the input
|
||||
//! allocates the `Editor` internally via `use_state`. Value readable, cursor hidden.
|
||||
//! * `Input::editor(editor: Entity<Editor>)` — you hold the editor; cursor/selection
|
||||
//! are now yours to read and drive too.
|
||||
//!
|
||||
//! Either way the chrome is identical. Because the string (or editor) is the
|
||||
//! input's *identity*, the internal `use_state(Editor)` is collision-safe across
|
||||
//! any number of inputs.
|
||||
|
||||
use gpui::{
|
||||
App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, Pixels, StyleRefinement,
|
||||
Window, div, hsla, point, prelude::*, px, white,
|
||||
};
|
||||
|
||||
use crate::example_editor::{Editor, standard_actions};
|
||||
|
||||
enum Source {
|
||||
Value(Entity<String>),
|
||||
Editor(Entity<Editor>),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Input {
|
||||
source: Source,
|
||||
width: Option<Pixels>,
|
||||
color: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// Backed by a bare string; the editor is allocated internally.
|
||||
pub fn new(value: Entity<String>) -> Self {
|
||||
Self {
|
||||
source: Source::Value(value),
|
||||
width: None,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Backed by an editor you own (so you can read/drive its cursor).
|
||||
pub fn editor(editor: Entity<Editor>) -> Self {
|
||||
Self {
|
||||
source: Source::Editor(editor),
|
||||
width: None,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: Pixels) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Hsla) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::View for Input {
|
||||
fn entity_id(&self) -> Option<EntityId> {
|
||||
Some(match &self.source {
|
||||
Source::Value(value) => value.entity_id(),
|
||||
Source::Editor(editor) => editor.entity_id(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
// Get the editor: use the one we were handed, or allocate it under our
|
||||
// own (string-derived) identity so it persists and never collides.
|
||||
let editor = match self.source {
|
||||
Source::Value(value) => {
|
||||
window.use_state(cx, move |window, cx| Editor::over(value, window, cx))
|
||||
}
|
||||
Source::Editor(editor) => editor,
|
||||
};
|
||||
|
||||
let focus_handle = editor.read(cx).focus_handle.clone();
|
||||
let is_focused = focus_handle.is_focused(window);
|
||||
let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.));
|
||||
let box_width = self.width.unwrap_or(px(300.));
|
||||
|
||||
let border = if is_focused {
|
||||
hsla(220. / 360., 0.8, 0.5, 1.)
|
||||
} else {
|
||||
hsla(0., 0., 0.75, 1.)
|
||||
};
|
||||
|
||||
div()
|
||||
.id("input")
|
||||
.key_context("TextInput")
|
||||
.track_focus(&focus_handle)
|
||||
.cursor(CursorStyle::IBeam)
|
||||
.map(standard_actions(editor.clone()))
|
||||
.w(box_width)
|
||||
.h(px(36.))
|
||||
.px(px(8.))
|
||||
.bg(white())
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.when(is_focused, |this| {
|
||||
this.shadow(vec![BoxShadow {
|
||||
color: hsla(220. / 360., 0.8, 0.5, 0.3),
|
||||
offset: point(px(0.), px(0.)),
|
||||
blur_radius: px(4.),
|
||||
spread_radius: px(1.),
|
||||
inset: false,
|
||||
}])
|
||||
})
|
||||
.rounded(px(4.))
|
||||
.overflow_hidden()
|
||||
.flex()
|
||||
.items_center()
|
||||
.line_height(px(20.))
|
||||
.text_size(px(14.))
|
||||
.text_color(text_color)
|
||||
.child(editor.cached(StyleRefinement::default().size_full()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Tests for the input composition. Require the `test-support` feature:
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo test -p gpui --example view_example --features test-support
|
||||
//! ```
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{Context, Entity, KeyBinding, TestAppContext, Window, prelude::*};
|
||||
|
||||
use crate::example_editor::Editor;
|
||||
use crate::example_input::Input;
|
||||
use crate::{Backspace, Delete, End, Home, Left, Right};
|
||||
|
||||
/// Two inputs, each backed by an editor we own (so the test can focus and
|
||||
/// read them). Proves data flows through the shared `String` and that
|
||||
/// sibling inputs stay isolated.
|
||||
struct Harness {
|
||||
a: Entity<Editor>,
|
||||
b: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl Render for Harness {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
gpui::div()
|
||||
.child(Input::editor(self.a.clone()))
|
||||
.child(Input::editor(self.b.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_keys(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("backspace", Backspace, None),
|
||||
KeyBinding::new("delete", Delete, None),
|
||||
KeyBinding::new("left", Left, None),
|
||||
KeyBinding::new("right", Right, None),
|
||||
KeyBinding::new("home", Home, None),
|
||||
KeyBinding::new("end", End, None),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(
|
||||
cx: &mut TestAppContext,
|
||||
) -> (
|
||||
Entity<Editor>,
|
||||
Entity<String>,
|
||||
Entity<String>,
|
||||
&mut gpui::VisualTestContext,
|
||||
) {
|
||||
bind_keys(cx);
|
||||
|
||||
let (harness, cx) = cx.add_window_view(|window, cx| {
|
||||
let a_value = cx.new(|_| String::new());
|
||||
let b_value = cx.new(|_| String::new());
|
||||
let a = cx.new(|cx| Editor::over(a_value, window, cx));
|
||||
let b = cx.new(|cx| Editor::over(b_value, window, cx));
|
||||
Harness { a, b }
|
||||
});
|
||||
|
||||
let a = cx.read_entity(&harness, |h, _| h.a.clone());
|
||||
let b = cx.read_entity(&harness, |h, _| h.b.clone());
|
||||
let a_value = cx.read_entity(&a, |e, _| e.value.clone());
|
||||
let b_value = cx.read_entity(&b, |e, _| e.value.clone());
|
||||
|
||||
// Focus the first input's editor.
|
||||
cx.update(|window, cx| {
|
||||
let focus_handle = a.read(cx).focus_handle.clone();
|
||||
window.focus(&focus_handle, cx);
|
||||
});
|
||||
|
||||
(a, a_value, b_value, cx)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn typing_updates_the_shared_string(cx: &mut TestAppContext) {
|
||||
let (editor, a_value, _b_value, cx) = setup(cx);
|
||||
|
||||
cx.simulate_input("hello");
|
||||
|
||||
cx.read_entity(&a_value, |value, _| assert_eq!(value, "hello"));
|
||||
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn sibling_inputs_are_isolated(cx: &mut TestAppContext) {
|
||||
let (_editor, a_value, b_value, cx) = setup(cx);
|
||||
|
||||
cx.simulate_input("x");
|
||||
|
||||
cx.read_entity(&a_value, |value, _| assert_eq!(value, "x"));
|
||||
cx.read_entity(&b_value, |value, _| {
|
||||
assert_eq!(value, "", "typing in input A must not touch input B")
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn external_writes_clamp_the_cursor(cx: &mut TestAppContext) {
|
||||
let (editor, a_value, _b_value, cx) = setup(cx);
|
||||
|
||||
cx.simulate_input("hello");
|
||||
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5));
|
||||
|
||||
// Write the shared value from outside the editor. The old cursor (5)
|
||||
// now points into the middle of a multi-byte character; the editor's
|
||||
// observation must clamp it back onto a boundary.
|
||||
cx.update(|_, cx| {
|
||||
a_value.update(cx, |value, cx| {
|
||||
*value = "日本".to_string();
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
|
||||
cx.read_entity(&a_value, |value, _| assert_eq!(value, "日本"));
|
||||
cx.read_entity(&editor, |editor, _| {
|
||||
assert_eq!(editor.cursor, 3, "cursor must clamp to a char boundary");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn arrows_move_the_cursor(cx: &mut TestAppContext) {
|
||||
let (editor, _a_value, _b_value, cx) = setup(cx);
|
||||
|
||||
cx.simulate_input("abc");
|
||||
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 3));
|
||||
|
||||
cx.simulate_keystrokes("left left");
|
||||
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! `TextArea` — a multi-line text box. Same `Editor` workhorse, taller chrome,
|
||||
//! and `Enter` inserts a newline instead of being ignored. Constructible from a
|
||||
//! string or an editor, exactly like [`Input`](crate::example_input::Input).
|
||||
|
||||
use gpui::{
|
||||
App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, StyleRefinement, Window, div,
|
||||
hsla, point, prelude::*, px, white,
|
||||
};
|
||||
|
||||
use crate::Enter;
|
||||
use crate::example_editor::{Editor, standard_actions};
|
||||
|
||||
enum Source {
|
||||
Value(Entity<String>),
|
||||
Editor(Entity<Editor>),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct TextArea {
|
||||
source: Source,
|
||||
rows: usize,
|
||||
color: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl TextArea {
|
||||
pub fn new(value: Entity<String>, rows: usize) -> Self {
|
||||
Self {
|
||||
source: Source::Value(value),
|
||||
rows,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor(editor: Entity<Editor>, rows: usize) -> Self {
|
||||
Self {
|
||||
source: Source::Editor(editor),
|
||||
rows,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Hsla) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::View for TextArea {
|
||||
fn entity_id(&self) -> Option<EntityId> {
|
||||
Some(match &self.source {
|
||||
Source::Value(value) => value.entity_id(),
|
||||
Source::Editor(editor) => editor.entity_id(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let editor = match self.source {
|
||||
Source::Value(value) => {
|
||||
window.use_state(cx, move |window, cx| Editor::over(value, window, cx))
|
||||
}
|
||||
Source::Editor(editor) => editor,
|
||||
};
|
||||
|
||||
let focus_handle = editor.read(cx).focus_handle.clone();
|
||||
let is_focused = focus_handle.is_focused(window);
|
||||
let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.));
|
||||
let row_height = px(20.);
|
||||
let box_height = row_height * self.rows as f32 + px(16.);
|
||||
|
||||
let border = if is_focused {
|
||||
hsla(220. / 360., 0.8, 0.5, 1.)
|
||||
} else {
|
||||
hsla(0., 0., 0.75, 1.)
|
||||
};
|
||||
|
||||
div()
|
||||
.id("text-area")
|
||||
.key_context("TextInput")
|
||||
.track_focus(&focus_handle)
|
||||
.cursor(CursorStyle::IBeam)
|
||||
.map(standard_actions(editor.clone()))
|
||||
// Enter is the one binding that differs from a single-line input.
|
||||
.on_action({
|
||||
let editor = editor.clone();
|
||||
move |_: &Enter, _window, cx| editor.update(cx, |e, cx| e.insert_newline(cx))
|
||||
})
|
||||
.w(px(400.))
|
||||
.h(box_height)
|
||||
.p(px(8.))
|
||||
.bg(white())
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.when(is_focused, |this| {
|
||||
this.shadow(vec![BoxShadow {
|
||||
color: hsla(220. / 360., 0.8, 0.5, 0.3),
|
||||
offset: point(px(0.), px(0.)),
|
||||
blur_radius: px(4.),
|
||||
spread_radius: px(1.),
|
||||
inset: false,
|
||||
}])
|
||||
})
|
||||
.rounded(px(4.))
|
||||
.overflow_hidden()
|
||||
.line_height(row_height)
|
||||
.text_size(px(14.))
|
||||
.text_color(text_color)
|
||||
// The cache style is computed from the `rows` prop: change `rows` and
|
||||
// the editor's cached bounds change, busting its cache and re-laying
|
||||
// out the text. (`Input` just uses `size_full()` — nothing to vary.)
|
||||
.child(
|
||||
editor.cached(
|
||||
StyleRefinement::default()
|
||||
.w_full()
|
||||
.h(row_height * self.rows as f32),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
|
||||
//! View example — composing a text input from the `View` primitives.
|
||||
//!
|
||||
//! The whole point: a text input is deceptively complicated, and `View` makes it
|
||||
//! easy to compose one. Three pieces, each shown in its own section:
|
||||
//!
|
||||
//! * `Editor` — the workhorse entity: cursor, blink, focus, keyboard, and a
|
||||
//! specialized text renderer. All the hard parts live here.
|
||||
//! * `String` — the data plane. `editor.text(cx)` / `value.read(cx)` get it out.
|
||||
//! * `Input` / `TextArea` — the shaping layer. Each takes a `String` (and grows
|
||||
//! the editor internally) OR an `Editor` (so you can read the cursor).
|
||||
//!
|
||||
//! Run: `cargo run -p gpui --example view_example`
|
||||
|
||||
mod example_editor;
|
||||
mod example_input;
|
||||
mod example_text_area;
|
||||
|
||||
#[cfg(test)]
|
||||
mod example_tests;
|
||||
|
||||
use example_editor::Editor;
|
||||
use example_input::Input;
|
||||
use example_text_area::TextArea;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Div, Entity, IntoElement, KeyBinding, Render, SharedString, Window,
|
||||
WindowBounds, WindowOptions, actions, div, hsla, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
actions!(
|
||||
view_example,
|
||||
[Backspace, Delete, Left, Right, Home, End, Enter, Quit]
|
||||
);
|
||||
|
||||
/// A tiny stateless view that reads an editor's cursor and is composed *beside*
|
||||
/// the thing editing it — two views over one entity, zero wiring.
|
||||
#[derive(IntoElement)]
|
||||
struct CursorReadout {
|
||||
editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl CursorReadout {
|
||||
fn new(editor: Entity<Editor>) -> Self {
|
||||
Self { editor }
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::RenderOnce for CursorReadout {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let cursor = self.editor.read(cx).cursor;
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(hsla(0., 0., 0.45, 1.))
|
||||
.child(SharedString::from(format!("cursor @ {cursor}")))
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewExample;
|
||||
|
||||
impl ViewExample {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ViewExample {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// The data plane: plain strings, allocated at the top by the hook.
|
||||
let name = window.use_state(cx, |_, _| String::new());
|
||||
let email = window.use_state(cx, |_, _| String::from("me@example.com"));
|
||||
let bio = window.use_state(cx, |_, _| String::new());
|
||||
// Editors that own their own string internally — no extra wiring up top.
|
||||
let notes = window.use_state(cx, |window, cx| Editor::new("multi\nline", window, cx));
|
||||
let owned = window.use_state(cx, |window, cx| Editor::new("editable", window, cx));
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(rgb(0xf0f0f0))
|
||||
.p(px(24.))
|
||||
.gap(px(24.))
|
||||
.child(
|
||||
section("Inputs — from a String (cursor stays internal)")
|
||||
.child(Input::new(name).width(px(320.)))
|
||||
.child(
|
||||
Input::new(email)
|
||||
.width(px(320.))
|
||||
.color(hsla(0., 0., 0.3, 1.)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Input — from an Editor (read its cursor beside it)").child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(12.))
|
||||
.child(Input::editor(owned.clone()).width(px(320.)))
|
||||
.child(CursorReadout::new(owned)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Text areas — from a String, or from an Editor")
|
||||
.child(TextArea::new(bio, 3))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap(px(12.))
|
||||
.child(TextArea::editor(notes.clone(), 3).color(hsla(
|
||||
250. / 360.,
|
||||
0.7,
|
||||
0.4,
|
||||
1.,
|
||||
)))
|
||||
.child(CursorReadout::new(notes)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A labeled vertical section.
|
||||
fn section(title: &str) -> Div {
|
||||
div().flex().flex_col().gap(px(8.)).child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(hsla(0., 0., 0.3, 1.))
|
||||
.child(SharedString::from(title.to_string())),
|
||||
)
|
||||
}
|
||||
|
||||
fn run_example() {
|
||||
application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(560.0), px(480.0)), cx);
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("backspace", Backspace, None),
|
||||
KeyBinding::new("delete", Delete, None),
|
||||
KeyBinding::new("left", Left, None),
|
||||
KeyBinding::new("right", Right, None),
|
||||
KeyBinding::new("home", Home, None),
|
||||
KeyBinding::new("end", End, None),
|
||||
KeyBinding::new("enter", Enter, None),
|
||||
KeyBinding::new("cmd-q", Quit, None),
|
||||
]);
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| ViewExample::new()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
+2
-112
@@ -33,12 +33,12 @@
|
||||
|
||||
use crate::{
|
||||
A11ySubtreeBuilder, App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ElementId,
|
||||
FocusHandle, InspectorElementId, LayoutId, Pixels, Point, SharedString, Size, Style, Window,
|
||||
FocusHandle, InspectorElementId, LayoutId, Pixels, Point, Size, Style, Window,
|
||||
util::FluentBuilder, window::with_element_arena,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use std::{
|
||||
any::{Any, type_name},
|
||||
any::Any,
|
||||
fmt::{self, Debug, Display},
|
||||
mem, panic,
|
||||
sync::Arc,
|
||||
@@ -208,116 +208,6 @@ pub trait ParentElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro
|
||||
/// for [`RenderOnce`]
|
||||
#[doc(hidden)]
|
||||
pub struct Component<C: RenderOnce> {
|
||||
component: Option<C>,
|
||||
#[cfg(debug_assertions)]
|
||||
source: &'static core::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<C: RenderOnce> Component<C> {
|
||||
/// Create a new component from the given RenderOnce type.
|
||||
#[track_caller]
|
||||
pub fn new(component: C) -> Self {
|
||||
Component {
|
||||
component: Some(component),
|
||||
#[cfg(debug_assertions)]
|
||||
source: core::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepaint_component(
|
||||
(element, name): &mut (AnyElement, &'static str),
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.with_id(ElementId::Name(SharedString::new_static(name)), |window| {
|
||||
element.prepaint(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn paint_component(
|
||||
(element, name): &mut (AnyElement, &'static str),
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.with_id(ElementId::Name(SharedString::new_static(name)), |window| {
|
||||
element.paint(window, cx);
|
||||
})
|
||||
}
|
||||
impl<C: RenderOnce> Element for Component<C> {
|
||||
type RequestLayoutState = (AnyElement, &'static str);
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
return Some(self.source);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return None;
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
window.with_id(ElementId::Name(type_name::<C>().into()), |window| {
|
||||
let mut element = self
|
||||
.component
|
||||
.take()
|
||||
.unwrap()
|
||||
.render(window, cx)
|
||||
.into_any_element();
|
||||
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
(layout_id, (element, type_name::<C>()))
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
state: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
prepaint_component(state, window, cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
state: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
paint_component(state, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: RenderOnce> IntoElement for Component<C> {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A globally unique identifier for an element, used to track state across frames.
|
||||
#[derive(Deref, DerefMut, Clone, Default, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct GlobalElementId(pub(crate) Arc<[ElementId]>);
|
||||
|
||||
+357
-177
@@ -1,36 +1,24 @@
|
||||
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, RenderOnce, Style, StyleRefinement, TextStyle, WeakEntity,
|
||||
};
|
||||
use crate::{Empty, Window};
|
||||
use anyhow::Result;
|
||||
use collections::FxHashSet;
|
||||
use refineable::Refineable;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::{any::TypeId, fmt, ops::Range};
|
||||
|
||||
struct AnyViewState {
|
||||
prepaint_range: Range<PrepaintStateIndex>,
|
||||
paint_range: Range<PaintIndex>,
|
||||
cache_key: ViewCacheKey,
|
||||
accessed_entities: FxHashSet<EntityId>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ViewCacheKey {
|
||||
bounds: Bounds<Pixels>,
|
||||
content_mask: ContentMask<Pixels>,
|
||||
text_style: TextStyle,
|
||||
}
|
||||
|
||||
/// A dynamically-typed handle to a view, which can be downcast to a [Entity] for a specific type.
|
||||
/// A dynamically-typed view handle that can be downcast to a specific `Entity<V>`.
|
||||
///
|
||||
/// This is the type-erased counterpart to [`ViewElement`]: it holds an entity plus
|
||||
/// a function pointer to its render, and is itself a [`View`], so embedding it as an
|
||||
/// element goes through the same [`ViewElement`] machinery as any other view.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnyView {
|
||||
entity: AnyEntity,
|
||||
render: fn(&AnyView, &mut Window, &mut App) -> AnyElement,
|
||||
cached_style: Option<Rc<StyleRefinement>>,
|
||||
}
|
||||
|
||||
impl<V: Render> From<Entity<V>> for AnyView {
|
||||
@@ -38,18 +26,18 @@ impl<V: Render> From<Entity<V>> for AnyView {
|
||||
AnyView {
|
||||
entity: value.into_any(),
|
||||
render: any_view::render::<V>,
|
||||
cached_style: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyView {
|
||||
/// Indicate that this view should be cached when using it as an element.
|
||||
/// When using this method, the view's previous layout and paint will be recycled from the previous frame if [Context::notify] has not been called since it was rendered.
|
||||
/// The one exception is when [Window::refresh] is called, in which case caching is ignored.
|
||||
pub fn cached(mut self, style: StyleRefinement) -> Self {
|
||||
self.cached_style = Some(style.into());
|
||||
self
|
||||
/// Embed this view as a cached [`ViewElement`] laid out at `style`.
|
||||
///
|
||||
/// The rendered subtree is recycled from the previous frame unless
|
||||
/// [Context::notify] was called on the backing entity since it was rendered
|
||||
/// (or [Window::refresh] is called, which ignores caching).
|
||||
pub fn cached(self, style: StyleRefinement) -> ViewElement<AnyView> {
|
||||
ViewElement::new(self).cached(style)
|
||||
}
|
||||
|
||||
/// Convert this to a weak handle.
|
||||
@@ -68,7 +56,6 @@ impl AnyView {
|
||||
Err(entity) => Err(Self {
|
||||
entity,
|
||||
render: self.render,
|
||||
cached_style: self.cached_style,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -78,7 +65,7 @@ impl AnyView {
|
||||
self.entity.entity_type
|
||||
}
|
||||
|
||||
/// Gets the entity id of this handle.
|
||||
/// The [`EntityId`] of this view.
|
||||
pub fn entity_id(&self) -> EntityId {
|
||||
self.entity.entity_id()
|
||||
}
|
||||
@@ -92,184 +79,48 @@ impl PartialEq for AnyView {
|
||||
|
||||
impl Eq for AnyView {}
|
||||
|
||||
impl Element for AnyView {
|
||||
type RequestLayoutState = Option<AnyElement>;
|
||||
type PrepaintState = Option<AnyElement>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(ElementId::View(self.entity_id()))
|
||||
/// `AnyView` is the type-erased [`View`]: its `render` is a function pointer rather
|
||||
/// than a concrete type, but it participates in the reactive graph exactly like any
|
||||
/// other view via [`ViewElement`].
|
||||
impl View for AnyView {
|
||||
fn entity_id(&self) -> Option<EntityId> {
|
||||
Some(self.entity.entity_id())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
window.with_rendered_view(self.entity_id(), |window| {
|
||||
// Disable caching when inspecting so that mouse_hit_test has all hitboxes.
|
||||
let caching_disabled = window.is_inspector_picking(cx);
|
||||
match self.cached_style.as_ref() {
|
||||
Some(style) if !caching_disabled => {
|
||||
let mut root_style = Style::default();
|
||||
root_style.refine(style);
|
||||
let layout_id = window.request_layout(root_style, None, cx);
|
||||
(layout_id, None)
|
||||
}
|
||||
_ => {
|
||||
let mut element = (self.render)(self, window, cx);
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
(layout_id, Some(element))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
window.set_view_id(self.entity_id());
|
||||
window.with_rendered_view(self.entity_id(), |window| {
|
||||
if let Some(mut element) = element.take() {
|
||||
element.prepaint(window, cx);
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
window.with_element_state::<AnyViewState, _>(
|
||||
global_id.unwrap(),
|
||||
|element_state, window| {
|
||||
let content_mask = window.content_mask();
|
||||
let text_style = window.text_style();
|
||||
|
||||
if let Some(mut element_state) = element_state
|
||||
&& element_state.cache_key.bounds == bounds
|
||||
&& element_state.cache_key.content_mask == content_mask
|
||||
&& element_state.cache_key.text_style == text_style
|
||||
&& !window.dirty_views.contains(&self.entity_id())
|
||||
&& !window.refreshing
|
||||
{
|
||||
let prepaint_start = window.prepaint_index();
|
||||
window.reuse_prepaint(element_state.prepaint_range.clone());
|
||||
cx.entities
|
||||
.extend_accessed(&element_state.accessed_entities);
|
||||
let prepaint_end = window.prepaint_index();
|
||||
element_state.prepaint_range = prepaint_start..prepaint_end;
|
||||
|
||||
return (None, element_state);
|
||||
}
|
||||
|
||||
let refreshing = mem::replace(&mut window.refreshing, true);
|
||||
let prepaint_start = window.prepaint_index();
|
||||
let (mut element, accessed_entities) = cx.detect_accessed_entities(|cx| {
|
||||
let mut element = (self.render)(self, window, cx);
|
||||
element.layout_as_root(bounds.size.into(), window, cx);
|
||||
element.prepaint_at(bounds.origin, window, cx);
|
||||
element
|
||||
});
|
||||
|
||||
let prepaint_end = window.prepaint_index();
|
||||
window.refreshing = refreshing;
|
||||
|
||||
(
|
||||
Some(element),
|
||||
AnyViewState {
|
||||
accessed_entities,
|
||||
prepaint_range: prepaint_start..prepaint_end,
|
||||
paint_range: PaintIndex::default()..PaintIndex::default(),
|
||||
cache_key: ViewCacheKey {
|
||||
bounds,
|
||||
content_mask,
|
||||
text_style,
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
element: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.with_rendered_view(self.entity_id(), |window| {
|
||||
let caching_disabled = window.is_inspector_picking(cx);
|
||||
if self.cached_style.is_some() && !caching_disabled {
|
||||
window.with_element_state::<AnyViewState, _>(
|
||||
global_id.unwrap(),
|
||||
|element_state, window| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
|
||||
let paint_start = window.paint_index();
|
||||
|
||||
if let Some(element) = element {
|
||||
let refreshing = mem::replace(&mut window.refreshing, true);
|
||||
element.paint(window, cx);
|
||||
window.refreshing = refreshing;
|
||||
} else {
|
||||
window.reuse_paint(element_state.paint_range.clone());
|
||||
}
|
||||
|
||||
let paint_end = window.paint_index();
|
||||
element_state.paint_range = paint_start..paint_end;
|
||||
|
||||
((), element_state)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
element.as_mut().unwrap().paint(window, cx);
|
||||
}
|
||||
});
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
(self.render)(&self, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static + Render> IntoElement for Entity<V> {
|
||||
type Element = AnyView;
|
||||
type Element = ViewElement<Entity<V>>;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self.into()
|
||||
ViewElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for AnyView {
|
||||
type Element = Self;
|
||||
type Element = ViewElement<AnyView>;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
ViewElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A weak, dynamically-typed view handle that does not prevent the view from being released.
|
||||
/// A weak, dynamically-typed view handle.
|
||||
pub struct AnyWeakView {
|
||||
entity: AnyWeakEntity,
|
||||
render: fn(&AnyView, &mut Window, &mut App) -> AnyElement,
|
||||
}
|
||||
|
||||
impl AnyWeakView {
|
||||
/// Convert to a strongly-typed handle if the referenced view has not yet been released.
|
||||
/// Upgrade to a strong `AnyView` handle, if the view is still alive.
|
||||
pub fn upgrade(&self) -> Option<AnyView> {
|
||||
let entity = self.entity.upgrade()?;
|
||||
Some(AnyView {
|
||||
entity,
|
||||
render: self.render,
|
||||
cached_style: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -310,6 +161,335 @@ mod any_view {
|
||||
}
|
||||
}
|
||||
|
||||
/// A renderable that participates in GPUI's reactive graph — the unifying model
|
||||
/// behind [`Render`] and [`RenderOnce`].
|
||||
///
|
||||
/// When `entity_id()` returns `Some`, that id becomes the view's identity: it gets
|
||||
/// a unique element-id space (so internal `use_state` / `.id(..)` never collide
|
||||
/// across siblings) and `cx.notify()` on that entity re-renders only this view's
|
||||
/// subtree. `None` behaves like a stateless component.
|
||||
///
|
||||
/// You rarely implement `View` directly. `Entity<T: Render>` and any `T: RenderOnce`
|
||||
/// get a blanket impl below; implement it by hand only when a component needs both
|
||||
/// parent-supplied props *and* a backing entity for identity.
|
||||
pub trait View: 'static + Sized {
|
||||
/// This view's identity, if it has one. A view typically holds the backing
|
||||
/// entity as a field and returns its [`EntityId`] here.
|
||||
///
|
||||
/// The id becomes this view's [`ElementId`], so two views keyed on the same
|
||||
/// entity must not be rendered at the same position in the element tree
|
||||
/// (e.g. as siblings under the same parent): their internal element state
|
||||
/// (`use_state`, scroll offsets, etc.) would silently collide. Nesting is
|
||||
/// fine — the id is scoped by the parent path.
|
||||
fn entity_id(&self) -> Option<EntityId>;
|
||||
|
||||
/// Render this view into an element tree, consuming `self`.
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement;
|
||||
}
|
||||
|
||||
/// A stateless component (`RenderOnce`) is a `View` with no identity.
|
||||
impl<T: RenderOnce> View for T {
|
||||
fn entity_id(&self) -> Option<EntityId> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
RenderOnce::render(self, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// An entity that renders itself (`Render`) is a `View` keyed on its own id.
|
||||
impl<T: Render> View for Entity<T> {
|
||||
fn entity_id(&self) -> Option<EntityId> {
|
||||
Some(Entity::entity_id(self))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
self.update(cx, |this, cx| {
|
||||
Render::render(this, window, cx).into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Render> Entity<T> {
|
||||
/// Embed this entity as a cached [`ViewElement`] laid out at `style`.
|
||||
///
|
||||
/// The rendered subtree is reused until the entity is notified (or the
|
||||
/// cached bounds / text style change). Caching requires a definite size:
|
||||
/// a cached view is laid out from `style` and is *not* measured from its
|
||||
/// contents. Use [`ViewElement::new`] (or `.child(entity)`) for the
|
||||
/// uncached case.
|
||||
#[track_caller]
|
||||
pub fn cached(self, style: StyleRefinement) -> ViewElement<Entity<T>> {
|
||||
ViewElement::new(self).cached(style)
|
||||
}
|
||||
}
|
||||
|
||||
/// The element type for [`View`] implementations. Wraps a `View` and hooks it
|
||||
/// into layout, prepaint, and paint. Constructed via [`ViewElement::new`].
|
||||
#[doc(hidden)]
|
||||
pub struct ViewElement<V: View> {
|
||||
view: Option<V>,
|
||||
entity_id: Option<EntityId>,
|
||||
cached_style: Option<StyleRefinement>,
|
||||
#[cfg(debug_assertions)]
|
||||
source: &'static core::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<V: View> ViewElement<V> {
|
||||
/// Wrap a [`View`] as an element.
|
||||
#[track_caller]
|
||||
pub fn new(view: V) -> Self {
|
||||
let entity_id = view.entity_id();
|
||||
ViewElement {
|
||||
entity_id,
|
||||
cached_style: None,
|
||||
view: Some(view),
|
||||
#[cfg(debug_assertions)]
|
||||
source: core::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable caching of this view's rendered subtree, laid out at `style`.
|
||||
/// The composer supplies the layout style because caching skips rendering
|
||||
/// the contents to measure them.
|
||||
///
|
||||
/// Crate-private on purpose: caching is only sound for entity-backed views,
|
||||
/// where [`Context::notify`] is the contract that busts the cache. A stateless
|
||||
/// view has no such contract, so a frozen subtree could never be invalidated.
|
||||
/// Reach this through [`Entity::cached`] or [`AnyView::cached`], which are
|
||||
/// entity-backed by construction.
|
||||
pub(crate) fn cached(mut self, style: StyleRefinement) -> Self {
|
||||
self.cached_style = Some(style);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> IntoElement for ViewElement<V> {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewElementState {
|
||||
prepaint_range: Range<PrepaintStateIndex>,
|
||||
paint_range: Range<PaintIndex>,
|
||||
cache_key: ViewElementCacheKey,
|
||||
accessed_entities: FxHashSet<EntityId>,
|
||||
}
|
||||
|
||||
struct ViewElementCacheKey {
|
||||
bounds: Bounds<Pixels>,
|
||||
content_mask: ContentMask<Pixels>,
|
||||
text_style: TextStyle,
|
||||
}
|
||||
|
||||
impl<V: View> Element for ViewElement<V> {
|
||||
type RequestLayoutState = Option<AnyElement>;
|
||||
type PrepaintState = Option<AnyElement>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.entity_id.map(ElementId::View)
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
return Some(self.source);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return None;
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
if let Some(entity_id) = self.entity_id {
|
||||
// Stateful path: create a reactive boundary.
|
||||
window.with_rendered_view(entity_id, |window| {
|
||||
let caching_disabled = window.is_inspector_picking(cx);
|
||||
match self.cached_style.as_ref() {
|
||||
Some(style) if !caching_disabled => {
|
||||
let mut root_style = Style::default();
|
||||
root_style.refine(style);
|
||||
let layout_id = window.request_layout(root_style, None, cx);
|
||||
(layout_id, None)
|
||||
}
|
||||
_ => {
|
||||
let mut element = self
|
||||
.view
|
||||
.take()
|
||||
.unwrap()
|
||||
.render(window, cx)
|
||||
.into_any_element();
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
(layout_id, Some(element))
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Stateless path: isolate subtree via type name (no entity identity).
|
||||
window.with_id(
|
||||
ElementId::Name(std::any::type_name::<V>().into()),
|
||||
|window| {
|
||||
let mut element = self
|
||||
.view
|
||||
.take()
|
||||
.unwrap()
|
||||
.render(window, cx)
|
||||
.into_any_element();
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
(layout_id, Some(element))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
if let Some(entity_id) = self.entity_id {
|
||||
// Stateful path.
|
||||
window.set_view_id(entity_id);
|
||||
window.with_rendered_view(entity_id, |window| {
|
||||
if let Some(mut element) = element.take() {
|
||||
element.prepaint(window, cx);
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
window.with_element_state::<ViewElementState, _>(
|
||||
global_id.unwrap(),
|
||||
|element_state, window| {
|
||||
let content_mask = window.content_mask();
|
||||
let text_style = window.text_style();
|
||||
|
||||
if let Some(mut element_state) = element_state
|
||||
&& element_state.cache_key.bounds == bounds
|
||||
&& element_state.cache_key.content_mask == content_mask
|
||||
&& element_state.cache_key.text_style == text_style
|
||||
&& !window.dirty_views.contains(&entity_id)
|
||||
&& !window.refreshing
|
||||
{
|
||||
let prepaint_start = window.prepaint_index();
|
||||
window.reuse_prepaint(element_state.prepaint_range.clone());
|
||||
cx.entities
|
||||
.extend_accessed(&element_state.accessed_entities);
|
||||
let prepaint_end = window.prepaint_index();
|
||||
element_state.prepaint_range = prepaint_start..prepaint_end;
|
||||
|
||||
return (None, element_state);
|
||||
}
|
||||
|
||||
let refreshing = mem::replace(&mut window.refreshing, true);
|
||||
let prepaint_start = window.prepaint_index();
|
||||
let (mut element, accessed_entities) = cx.detect_accessed_entities(|cx| {
|
||||
let mut element = self
|
||||
.view
|
||||
.take()
|
||||
.unwrap()
|
||||
.render(window, cx)
|
||||
.into_any_element();
|
||||
element.layout_as_root(bounds.size.into(), window, cx);
|
||||
element.prepaint_at(bounds.origin, window, cx);
|
||||
element
|
||||
});
|
||||
|
||||
let prepaint_end = window.prepaint_index();
|
||||
window.refreshing = refreshing;
|
||||
|
||||
(
|
||||
Some(element),
|
||||
ViewElementState {
|
||||
accessed_entities,
|
||||
prepaint_range: prepaint_start..prepaint_end,
|
||||
paint_range: PaintIndex::default()..PaintIndex::default(),
|
||||
cache_key: ViewElementCacheKey {
|
||||
bounds,
|
||||
content_mask,
|
||||
text_style,
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// Stateless path: just prepaint the element.
|
||||
window.with_id(
|
||||
ElementId::Name(std::any::type_name::<V>().into()),
|
||||
|window| {
|
||||
element.as_mut().unwrap().prepaint(window, cx);
|
||||
},
|
||||
);
|
||||
Some(element.take().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
element: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if let Some(entity_id) = self.entity_id {
|
||||
// Stateful path.
|
||||
window.with_rendered_view(entity_id, |window| {
|
||||
let caching_disabled = window.is_inspector_picking(cx);
|
||||
if self.cached_style.is_some() && !caching_disabled {
|
||||
window.with_element_state::<ViewElementState, _>(
|
||||
global_id.unwrap(),
|
||||
|element_state, window| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
|
||||
let paint_start = window.paint_index();
|
||||
|
||||
if let Some(element) = element {
|
||||
let refreshing = mem::replace(&mut window.refreshing, true);
|
||||
element.paint(window, cx);
|
||||
window.refreshing = refreshing;
|
||||
} else {
|
||||
window.reuse_paint(element_state.paint_range.clone());
|
||||
}
|
||||
|
||||
let paint_end = window.paint_index();
|
||||
element_state.paint_range = paint_start..paint_end;
|
||||
|
||||
((), element_state)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
element.as_mut().unwrap().paint(window, cx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Stateless path: just paint the element.
|
||||
window.with_id(
|
||||
ElementId::Name(std::any::type_name::<V>().into()),
|
||||
|window| {
|
||||
element.as_mut().unwrap().paint(window, cx);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A view that renders nothing
|
||||
pub struct EmptyView;
|
||||
|
||||
|
||||
@@ -2824,7 +2824,7 @@ impl Window {
|
||||
};
|
||||
|
||||
// Layout all root elements.
|
||||
let mut root_element = self.root.as_ref().unwrap().clone().into_any();
|
||||
let mut root_element = self.root.as_ref().unwrap().clone().into_any_element();
|
||||
root_element.prepaint_as_root(Point::default(), root_size.into(), self, cx);
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
@@ -2836,12 +2836,12 @@ impl Window {
|
||||
let mut active_drag_element = None;
|
||||
let mut tooltip_element = None;
|
||||
if let Some(prompt) = self.prompt.take() {
|
||||
let mut element = prompt.view.any_view().into_any();
|
||||
let mut element = prompt.view.any_view().into_any_element();
|
||||
element.prepaint_as_root(Point::default(), root_size.into(), self, cx);
|
||||
prompt_element = Some(element);
|
||||
self.prompt = Some(prompt);
|
||||
} else if let Some(active_drag) = cx.active_drag.take() {
|
||||
let mut element = active_drag.view.clone().into_any();
|
||||
let mut element = active_drag.view.clone().into_any_element();
|
||||
let offset = self.mouse_position() - active_drag.cursor_offset;
|
||||
element.prepaint_as_root(offset, AvailableSpace::min_size(), self, cx);
|
||||
active_drag_element = Some(element);
|
||||
@@ -2905,7 +2905,7 @@ impl Window {
|
||||
log::error!("Unexpectedly absent TooltipRequest");
|
||||
continue;
|
||||
};
|
||||
let mut element = tooltip_request.tooltip.view.clone().into_any();
|
||||
let mut element = tooltip_request.tooltip.view.clone().into_any_element();
|
||||
let mouse_position = tooltip_request.tooltip.mouse_position;
|
||||
let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self, cx);
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||
impl #impl_generics gpui::IntoElement for #type_name #type_generics
|
||||
#where_clause
|
||||
{
|
||||
type Element = gpui::Component<Self>;
|
||||
type Element = gpui::ViewElement<Self>;
|
||||
|
||||
#[track_caller]
|
||||
fn into_element(self) -> Self::Element {
|
||||
gpui::Component::new(self)
|
||||
gpui::ViewElement::new(self)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,8 +29,8 @@ pub fn register_action(ident: TokenStream) -> TokenStream {
|
||||
register_action::register_action(ident)
|
||||
}
|
||||
|
||||
/// #[derive(IntoElement)] is used to create a Component out of anything that implements
|
||||
/// the `RenderOnce` trait.
|
||||
/// #[derive(IntoElement)] generates an `IntoElement` impl for any `RenderOnce`
|
||||
/// type, wrapping it in a `ViewElement` so it can be used as a child.
|
||||
#[proc_macro_derive(IntoElement)]
|
||||
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||
derive_into_element::derive_into_element(input)
|
||||
|
||||
@@ -6294,7 +6294,7 @@ impl Workspace {
|
||||
.children(
|
||||
self.notifications
|
||||
.iter()
|
||||
.map(|(_, notification)| notification.clone().into_any()),
|
||||
.map(|(_, notification)| notification.clone().into_any_element()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user