1pub use crate::{
3 BlockEditorEvent, BlockEditorState, CapturedRange, CapturedRangeError, ClipboardPolicy,
4 ImagePolicy, PasteRequest, SourceSnapshot, SubmitPolicy,
5};
6use gpui::prelude::FluentBuilder as _;
7use gpui::{
8 App, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce,
9 StatefulInteractiveElement, Styled, Window, div, rems,
10};
11use gpui_kit::component::ActiveTheme;
12
13const INPUT_PADDING_X: f32 = 0.625;
16const INPUT_PADDING_Y: f32 = 0.5;
17const ROW_HEIGHT: f32 = 1.5;
18
19#[derive(IntoElement)]
20pub struct BlockEditor {
21 state: Entity<BlockEditorState>,
22 min_rows: usize,
23 max_rows: usize,
24}
25impl BlockEditor {
26 pub fn new(state: &Entity<BlockEditorState>) -> Self {
27 Self {
28 state: state.clone(),
29 min_rows: 2,
30 max_rows: 16,
31 }
32 }
33 pub fn auto_grow(mut self, min: usize, max: usize) -> Self {
37 self.min_rows = min.max(1);
38 self.max_rows = max.max(self.min_rows);
39 self
40 }
41}
42impl RenderOnce for BlockEditor {
43 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
44 let scroll = self.state.read(cx).scroll_handle();
45 let min_height = rems(self.min_rows as f32 * ROW_HEIGHT + INPUT_PADDING_Y * 2.);
46 let max_height = rems(self.max_rows as f32 * ROW_HEIGHT + INPUT_PADDING_Y * 2.);
47 div()
48 .id(("block-editor-viewport", self.state.entity_id()))
49 .w_full()
50 .min_w_0()
51 .min_h(min_height)
52 .max_h(max_height)
53 .overflow_y_scroll()
54 .when_some(scroll, |this, scroll| this.track_scroll(&scroll))
55 .text_color(cx.theme().foreground)
56 .child(
58 crate::BlockEditor::new(&self.state)
59 .min_h(min_height)
60 .px(rems(INPUT_PADDING_X))
61 .py(rems(INPUT_PADDING_Y)),
62 )
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::{BlockPosition, BlockSelection};
70 use gpui::{
71 AppContext as _, Bounds, Context, EntityInputHandler as _, Focusable as _, MouseButton,
72 Pixels, Render, TestAppContext, VisualTestContext, point, px,
73 };
74
75 struct Harness(Entity<BlockEditorState>, f32);
76
77 impl Render for Harness {
78 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
79 div()
80 .w(rems(self.1))
81 .text_size(rems(1.))
82 .child(BlockEditor::new(&self.0).auto_grow(2, 4))
83 }
84 }
85
86 fn draw(cx: &mut VisualTestContext) {
87 cx.run_until_parked();
88 cx.update(|window, cx| window.draw(cx).clear(cx));
89 }
90
91 fn caret(editor: &Entity<BlockEditorState>, cx: &mut VisualTestContext) -> Bounds<Pixels> {
92 cx.update(|window, cx| {
93 editor.update(cx, |state, cx| {
94 let offset = state.model().caret().unwrap().offset;
95 state
96 .bounds_for_range(offset..offset, Bounds::default(), window, cx)
97 .expect("the selected leaf is laid out")
98 })
99 })
100 }
101
102 #[gpui::test]
103 fn inset_pointer_edit_and_reveal_survive_zoom_and_rewrap(cx: &mut TestAppContext) {
104 cx.update(|cx| {
105 cx.set_http_client(gpui::http_client::FakeHttpClient::create(|_| async {
106 Err(anyhow::anyhow!(
107 "outbound requests forbidden in editor test"
108 ))
109 }));
110 crate::init_for_tests(cx);
111 });
112 let (root, cx) = cx.add_window_view(|window, cx| {
113 Harness(
114 cx.new(|cx| BlockEditorState::new(window, cx).with_source("alpha beta")),
115 20.,
116 )
117 });
118 let editor = root.read_with(cx, |root, _| root.0.clone());
119 draw(cx);
120 let scroll = editor.read_with(cx, |state, _| state.scroll_handle().unwrap());
121 let inset = cx.update(|window, _| {
122 let view = scroll.bounds();
123 point(
124 view.left() + window.rem_size() / 4.,
125 view.top() + window.rem_size() / 4.,
126 )
127 });
128 cx.simulate_click(inset, Default::default());
129 cx.simulate_input("X");
130 draw(cx);
131 assert_eq!(
132 editor.read_with(cx, |state, _| state.source()),
133 "Xalpha beta"
134 );
135 assert!(cx.update(|window, cx| editor.read(cx).focus_handle(cx).is_focused(window)));
136
137 let start = caret(&editor, cx).center();
140 let end = cx.update(|window, cx| {
141 editor.update(cx, |state, cx| {
142 state
143 .bounds_for_range(6..6, Bounds::default(), window, cx)
144 .unwrap()
145 .center()
146 })
147 });
148 cx.simulate_mouse_down(start, MouseButton::Left, Default::default());
149 cx.simulate_mouse_move(end, Some(MouseButton::Left), Default::default());
150 cx.simulate_mouse_up(end, MouseButton::Left, Default::default());
151 cx.simulate_input("word");
152 draw(cx);
153 assert_eq!(
154 editor.read_with(cx, |state, _| state.source()),
155 "Xword beta"
156 );
157
158 let trailing_inset = cx.update(|window, _| {
159 let view = scroll.bounds();
160 point(
161 view.right() - window.rem_size() / 4.,
162 view.bottom() - window.rem_size() / 4.,
163 )
164 });
165 cx.simulate_click(trailing_inset, Default::default());
166 cx.simulate_input("Y");
167 draw(cx);
168 assert_eq!(
169 editor.read_with(cx, |state, _| state.source()),
170 "Xword betaY"
171 );
172
173 let source = (0..80)
174 .map(|_| "one two three four five six seven eight nine ten")
175 .collect::<Vec<_>>()
176 .join("\n\n");
177 editor.update(cx, |state, cx| state.set_source(&source, cx).unwrap());
178 draw(cx);
179 editor.update(cx, |state, cx| {
180 let last = state.model().document().leaves().last().unwrap();
181 state
182 .set_selection(
183 BlockSelection::caret(BlockPosition::new(
184 last.id(),
185 last.text().unwrap().len(),
186 )),
187 cx,
188 )
189 .unwrap();
190 });
191 draw(cx);
192 assert!(
193 scroll.offset().y < Pixels::ZERO,
194 "the retained owner scrolls to the last leaf"
195 );
196 assert!(scroll.bounds().contains(&caret(&editor, cx).center()));
197
198 cx.update(|window, _| window.set_rem_size(px(24.)));
201 root.update(cx, |root, cx| {
202 root.1 = 12.;
203 cx.notify();
204 });
205 draw(cx);
207 draw(cx);
208 assert!(scroll.bounds().contains(&caret(&editor, cx).center()));
209 editor.update(cx, |state, cx| {
210 let first = state.model().document().leaves().next().unwrap().id();
211 state
212 .set_selection(BlockSelection::caret(BlockPosition::new(first, 0)), cx)
213 .unwrap();
214 });
215 draw(cx);
216 let inset = cx.update(|window, _| {
217 let view = scroll.bounds();
218 point(
219 view.left() + window.rem_size() / 4.,
220 view.top() + window.rem_size() / 4.,
221 )
222 });
223 cx.simulate_click(inset, Default::default());
224 cx.simulate_input("Z");
225 draw(cx);
226 assert_eq!(
227 editor.read_with(cx, |state, _| state.source()),
228 format!("Z{source}")
229 );
230 }
231}