editor/editor/render.rs
1use super::*;
2use crate::AppExt as _;
3use std::rc::Rc;
4use ui::AppExt as _;
5
6impl Render for Editor {
7 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
8 if let Some(mention) = &mut self.mention {
9 mention.refresh(cx);
10 }
11 let theme = Theme::of(cx).clone();
12 let layout = cx.editor_layout();
13 let focused = self.focus_handle.is_focused(window);
14 // The only place the blink starts: `caret_moved` drops the task, so the
15 // next render brings it back in phase, lit beat first.
16 if focused && cx.caret_blink() {
17 if self.blink.is_none() {
18 self.start_blink(cx);
19 }
20 } else {
21 self.blink = None;
22 self.caret_on = true;
23 }
24 let selection = focused.then_some(self.selection);
25 // The source line a reveal owed in source mode lands on.
26 let reveal_line = match self.mode {
27 Mode::Source => self
28 .source_text()
29 .get(..self.selection.head.offset)
30 .map_or(0, |before| before.matches('\n').count()),
31 Mode::Blocks => 0,
32 };
33 // gpui ends an outside file drag — left the window or released
34 // elsewhere — without a drop here, so the indicator goes with it.
35 if !cx.has_active_drag() {
36 self.dropping = None;
37 }
38
39 // Typed text and IME reach an entity only through an input handler
40 // registered during *paint*, against the bounds it should be anchored
41 // to. There is no custom element here to do that from, so a zero-cost
42 // canvas over the document supplies the paint phase. Without this the
43 // key bindings still fire and nothing types.
44 let handle = self.focus_handle.clone();
45 let entity = cx.entity();
46 let in_drag = self.in_drag();
47 let input = canvas(
48 |_, _, _| (),
49 move |bounds, _, window, cx| {
50 // `on_mouse_move` hears the pointer only over this box, and a
51 // drag goes on past it. Registering it is paint's alone.
52 if in_drag {
53 let entity = entity.clone();
54 window.on_mouse_event(move |event: &gpui::MouseMoveEvent, phase, _, cx| {
55 if phase == gpui::DispatchPhase::Bubble && event.dragging() {
56 entity.update(cx, |this, cx| this.drag_to(event.position, cx));
57 }
58 });
59 }
60 // The gutter handle is placed from positions recorded in window
61 // coordinates, so the box they have to be measured against is
62 // taken here — the one place that knows it.
63 entity.update(cx, |this, _| {
64 this.origin = bounds.origin;
65 this.width = bounds.size.width;
66 });
67 window.handle_input(
68 &handle,
69 ElementInputHandler::new(bounds, entity.clone()),
70 cx,
71 );
72 },
73 )
74 .absolute()
75 .size_full();
76
77 // A tab stop, so the editor is reachable the same way every other
78 // control in the library is.
79 let handle = self.focus_handle.clone().tab_stop(true);
80
81 div()
82 // Stateful only so the pointer leaving can be heard: `on_hover` is
83 // what tells the gutter handle to stop pointing at a block the
84 // pointer left behind.
85 .id("bezel-editor")
86 // The mark is what keeps `tab`: without it traversal answers the
87 // key first and the caret never sees it.
88 .key_context(key_context())
89 .track_focus(&handle)
90 // Tracking focus does not take it. Without this, clicking into the
91 // document blurs the editor instead of putting a caret in it, and
92 // the caret vanishes on the first click.
93 .on_mouse_down(
94 MouseButton::Left,
95 cx.listener(|this, event: &gpui::MouseDownEvent, window, cx| {
96 // The handle's own listener runs first and claims the
97 // press; without the flag this would close the menu it
98 // just opened. `ui::popover::Popup` solves it the same way.
99 if std::mem::take(&mut this.press_claimed) {
100 this.focus_handle.clone().focus(window, cx);
101 } else {
102 this.pressed(
103 event.position,
104 event.click_count,
105 event.modifiers,
106 window,
107 cx,
108 );
109 }
110 // What `press` reads to skip a press this box already
111 // took. It also stops gpui's own focus transfer, which runs
112 // after this listener, so both arms focus by hand.
113 window.prevent_default();
114 }),
115 )
116 .on_mouse_down(
117 MouseButton::Right,
118 cx.listener(|this, event: &gpui::MouseDownEvent, window, cx| {
119 if this.blocks()
120 && let Some((block, Part::Cell { row, column })) =
121 this.layouts.cell_at(event.position)
122 {
123 this.table_menu.open(Dropped::new(
124 (block, TableTarget::Cell { row, column }),
125 event.position,
126 ));
127 this.focus_handle.focus(window, cx);
128 } else {
129 this.right_pressed(event.position, window, cx);
130 }
131 window.prevent_default();
132 cx.stop_propagation();
133 cx.notify();
134 }),
135 )
136 // The drag has to be tracked from the container rather than from a
137 // payload: a text selection has nothing to carry, and gpui's drag
138 // payload is for things being dropped somewhere.
139 .on_hover(cx.listener(|this, hovered: &bool, _, cx| {
140 if !*hovered && (this.hovered.take().is_some() | this.hovered_cell.take().is_some())
141 {
142 cx.notify();
143 }
144 }))
145 .on_mouse_move(cx.listener(|this, event: &gpui::MouseMoveEvent, _, cx| {
146 // Ahead of every drag branch below, because the pointer's shape
147 // is about where it *is* rather than about what it is doing.
148 let over_text = this.layouts.over_text(event.position);
149 if over_text != this.over_text {
150 this.over_text = over_text;
151 cx.notify();
152 }
153 // A drag in flight is followed by the window-wide listener
154 // `render` registers; otherwise the pointer only decides which
155 // block wears the handle.
156 if this.in_drag() {
157 return;
158 }
159 let hovered = this.layouts.block_at(event.position);
160 let cell = this.layouts.cell_at(event.position).or_else(|| {
161 // Keep the target while crossing its control lanes.
162 this.hovered_cell.filter(|(block, _)| {
163 this.layouts
164 .block_bounds(*block)
165 .is_some_and(|bounds| bounds.contains(&event.position))
166 })
167 });
168 if hovered != this.hovered || cell != this.hovered_cell {
169 this.hovered = hovered;
170 this.hovered_cell = cell;
171 cx.notify();
172 }
173 }))
174 // Both, because a release can land anywhere on screen and only the
175 // first fires over the editor. A resize left running would leave
176 // its stand-in picture painted over the document for good.
177 .on_mouse_up_out(
178 MouseButton::Left,
179 cx.listener(|this, event: &gpui::MouseUpEvent, window, cx| {
180 this.drag_table_to(event.position, cx);
181 this.dragging = None;
182 this.drop_resize(window, cx);
183 this.drop_height(cx);
184 this.drop_table_drag(cx);
185 }),
186 )
187 .on_mouse_up(
188 MouseButton::Left,
189 cx.listener(|this, event: &gpui::MouseUpEvent, window, cx| {
190 this.drag_table_to(event.position, cx);
191 this.dragging = None;
192 if this.drop_table_drag(cx) {
193 return;
194 }
195 if this.drop_resize(window, cx) {
196 return;
197 }
198 if this.drop_height(cx) {
199 return;
200 }
201 let Some((from, to)) = this.lifted.take() else {
202 return;
203 };
204 if from == to {
205 // A press that never moved is a click, and a click on
206 // the handle is what opens the menu — unless that same
207 // press is what dismissed it, which the note taken on
208 // the way down is the only way to tell.
209 if !this.block_menu.take_press_was_open() {
210 let rows = crate::slash::Rows::new(crate::block_menu::installed(cx));
211 this.block_menu
212 .open(Dropped::new((from, rows), event.position));
213 }
214 return cx.notify();
215 }
216 this.edit(EditKind::Structure, cx, |this| {
217 // `move_block` steps one sibling at a time, so a drop
218 // several blocks away is that many steps. Bounded by
219 // the block count, which no drag can exceed.
220 let delta = if to > from { 1 } else { -1 };
221 let mut at = from;
222 // Each step is its own move, so each is its own delta —
223 // folding them into one would have to compose the
224 // hops, and they are already in order.
225 let mut deltas = Vec::new();
226 for _ in 0..this.doc.blocks.len() {
227 let span = this.doc.subtree(at);
228 let Some(next) = this.doc.move_block(at, delta) else {
229 break;
230 };
231 deltas.push(Delta::Moved {
232 at: span,
233 to: Some(next),
234 });
235 at = next;
236 if (delta > 0 && at >= to) || (delta < 0 && at <= to) {
237 break;
238 }
239 }
240 this.selection =
241 Selection::at(Cursor::new(at, Part::Body, 0).clamp(&this.doc));
242 deltas
243 });
244 }),
245 )
246 .on_action(cx.listener(Self::backspace))
247 .on_action(cx.listener(Self::delete))
248 .on_action(cx.listener(|this, _: &KillLine, _, cx| this.delete_to(true, line_end, cx)))
249 .on_action(cx.listener(|this, _: &DeleteWordLeft, _, cx| {
250 this.delete_to(false, Cursor::word_left, cx)
251 }))
252 .on_action(cx.listener(|this, _: &DeleteWordRight, _, cx| {
253 this.delete_to(true, Cursor::word_right, cx)
254 }))
255 .on_action(cx.listener(|this, _: &DeleteToHome, _, cx| {
256 this.delete_to(false, |at, _| at.home(), cx)
257 }))
258 .on_action(cx.listener(Self::split_block))
259 .on_action(cx.listener(Self::soft_break))
260 .on_action(cx.listener(Self::insert_paragraph))
261 .on_action(cx.listener(Self::indent))
262 .on_action(cx.listener(Self::increase_text_size))
263 .on_action(cx.listener(Self::decrease_text_size))
264 .on_action(cx.listener(Self::reset_text_size))
265 .on_action(cx.listener(Self::outdent))
266 .on_action(cx.listener(Self::dismiss))
267 .on_action(cx.listener(Self::select_all))
268 .on_action(cx.listener(Self::copy))
269 .on_action(cx.listener(Self::cut))
270 .on_action(cx.listener(Self::paste))
271 .on_action(cx.listener(Self::undo))
272 .on_action(cx.listener(Self::redo))
273 .on_action(cx.listener(Self::confirm_url))
274 .on_action(cx.listener(Self::cancel_url))
275 // A file crossing the document lights the same indicator a lifted
276 // block does, so a drop from outside lands where it looks like it
277 // will.
278 .on_drag_move(cx.listener(
279 |this, event: &gpui::DragMoveEvent<gpui::ExternalPaths>, _, cx| {
280 let at = event.event.position;
281 let over = event
282 .bounds
283 .contains(&at)
284 .then(|| this.layouts.block_at(at))
285 .flatten();
286 if over != this.dropping {
287 this.dropping = over;
288 cx.notify();
289 }
290 },
291 ))
292 .on_drop(
293 cx.listener(|this, paths: &gpui::ExternalPaths, window, cx| {
294 this.focus_handle.clone().focus(window, cx);
295 this.drop_paths(paths, cx);
296 }),
297 )
298 .on_action(cx.listener(|this, _: &ToggleBold, _, cx| this.toggle_mark(Mark::Bold, cx)))
299 .on_action(
300 cx.listener(|this, _: &ToggleItalic, _, cx| this.toggle_mark(Mark::Italic, cx)),
301 )
302 .on_action(
303 cx.listener(|this, _: &ToggleStrike, _, cx| this.toggle_mark(Mark::Strike, cx)),
304 )
305 .on_action(cx.listener(|this, _: &ToggleCode, _, cx| this.toggle_mark(Mark::Code, cx)))
306 .on_action(cx.listener(|this, _: &ToggleHighlight, _, cx| {
307 if this.marks.delimiter(HIGHLIGHT_MARK).is_some() {
308 this.toggle_mark(Mark::Custom(HIGHLIGHT_MARK.into()), cx);
309 }
310 }))
311 .on_action(cx.listener(|this, _: &MoveBlockUp, _, cx| {
312 this.move_block(this.cursor().block, -1, cx)
313 }))
314 .on_action(cx.listener(|this, _: &MoveBlockDown, _, cx| {
315 this.move_block(this.cursor().block, 1, cx)
316 }))
317 .on_action(cx.listener(|this, _: &DuplicateBlock, _, cx| {
318 this.duplicate_block(this.cursor().block, cx)
319 }))
320 .on_action(cx.listener(|this, _: &RemoveBlock, _, cx| {
321 this.remove_block(this.cursor().block, cx)
322 }))
323 // Motion is one method with a `Cursor` function and an "extend"
324 // flag, so a shift variant cannot drift from the key it shadows.
325 .on_action(cx.listener(|this, _: &Left, _, cx| {
326 if !this.menu_side(false, cx) {
327 this.horizontal(false, false, cx)
328 }
329 }))
330 .on_action(cx.listener(|this, _: &Right, _, cx| {
331 if !this.menu_side(true, cx) {
332 this.horizontal(true, false, cx)
333 }
334 }))
335 .on_action(cx.listener(|this, _: &Up, _, cx| this.vertical(false, false, cx)))
336 .on_action(cx.listener(|this, _: &Down, _, cx| this.vertical(true, false, cx)))
337 .on_action(cx.listener(|this, _: &Home, _, cx| this.row_edge(false, false, cx)))
338 .on_action(cx.listener(|this, _: &End, _, cx| this.row_edge(true, false, cx)))
339 .on_action(cx.listener(|this, _: &DocumentStart, _, cx| {
340 this.moved(false, |_, doc| Selection::all(doc).anchor, cx)
341 }))
342 .on_action(cx.listener(|this, _: &DocumentEnd, _, cx| {
343 this.moved(false, |_, doc| Selection::all(doc).head, cx)
344 }))
345 .on_action(
346 cx.listener(|this, _: &WordLeft, _, cx| this.moved(false, Cursor::word_left, cx)),
347 )
348 .on_action(
349 cx.listener(|this, _: &WordRight, _, cx| this.moved(false, Cursor::word_right, cx)),
350 )
351 .on_action(cx.listener(|this, _: &SelectLeft, _, cx| this.horizontal(false, true, cx)))
352 .on_action(cx.listener(|this, _: &SelectRight, _, cx| this.horizontal(true, true, cx)))
353 .on_action(cx.listener(|this, _: &SelectUp, _, cx| this.vertical(false, true, cx)))
354 .on_action(cx.listener(|this, _: &SelectDown, _, cx| this.vertical(true, true, cx)))
355 .on_action(cx.listener(|this, _: &SelectHome, _, cx| this.row_edge(false, true, cx)))
356 .on_action(cx.listener(|this, _: &SelectEnd, _, cx| this.row_edge(true, true, cx)))
357 .on_action(cx.listener(|this, _: &SelectDocumentStart, _, cx| {
358 this.moved(true, |_, doc| Selection::all(doc).anchor, cx)
359 }))
360 .on_action(cx.listener(|this, _: &SelectDocumentEnd, _, cx| {
361 this.moved(true, |_, doc| Selection::all(doc).head, cx)
362 }))
363 .on_action(cx.listener(|this, _: &SelectWordLeft, _, cx| {
364 this.moved(true, Cursor::word_left, cx)
365 }))
366 .on_action(cx.listener(|this, _: &SelectWordRight, _, cx| {
367 this.moved(true, Cursor::word_right, cx)
368 }))
369 .w_full()
370 // Text under the pointer, so the pointer says so — and only there,
371 // or while a drag is still sweeping one out. The editor's box
372 // reaches over its gutter, the margin beside a short line, a rule,
373 // an image and a card, none of which a caret can be put into.
374 // Where it has nothing to say it stays quiet rather than
375 // overriding the page with an arrow of its own.
376 .when(self.over_text || self.dragging.is_some(), |el| {
377 el.cursor(CursorStyle::IBeam)
378 })
379 // No focus ring. A ring says *widget*, and a document is not one —
380 // the caret already paints only while focused, so a box around the
381 // whole page is a second, louder signal for the same fact.
382 .relative()
383 .child(input)
384 // A height drag holds the resize cursor wherever the pointer
385 // runs, over the editor's text and the card's own cursors alike.
386 .when(self.sizing.is_some(), |el| {
387 el.child(
388 canvas(
389 |_, _, _| (),
390 |_, _, window, _| window.set_window_cursor_style(CursorStyle::ResizeUpDown),
391 )
392 .absolute()
393 .size_0(),
394 )
395 })
396 // The document is inset by the gutter so the handle has somewhere
397 // to sit *inside* the editor. Outside it the handle is clipped by
398 // any scrolling ancestor, and a pointer over it never reaches
399 // `on_mouse_move`, which fires only while this element is the one
400 // under the pointer.
401 .child(
402 div()
403 .w_full()
404 .pl(gpui::px(layout.text_inset))
405 .child(match self.mode {
406 // The source is one text, so it paints as one text —
407 // the same caret, the same clicks, no block chrome to
408 // suppress a piece at a time.
409 Mode::Source => markdown::render_source(
410 self.source_text(),
411 markdown::Editing {
412 selection,
413 caret_on: self.caret_on,
414 layouts: Some(&self.layouts),
415 annotations: &self.annotations(),
416 keep: if self.reveal.is_some() {
417 std::slice::from_ref(&reveal_line)
418 } else {
419 &[]
420 },
421 scroll: self.scroll.as_ref(),
422 typography: Some(cx.typography().scaled(
423 text_size::resolve(self.text_size, cx)
424 / theme::base_text_size(),
425 )),
426 ..Default::default()
427 },
428 cx,
429 ),
430 Mode::Blocks => markdown::render_with(
431 &self.doc,
432 markdown::Editing {
433 selection,
434 caret_on: self.caret_on,
435 layouts: Some(&self.layouts),
436 annotations: &self.annotations(),
437 placeholder: focused.then(|| PLACEHOLDER.into()),
438 // A caret goes into the caption here, so it is always
439 // painted — an editor that could hide it would be
440 // hiding a place you can already be typing.
441 caption: markdown::Caption::Shown,
442 // The size is absolute, so the factor the ladder
443 // is already scaled by comes back out of it —
444 // otherwise the app's size and this one multiply.
445 typography: Some(cx.typography().scaled(
446 text_size::resolve(self.text_size, cx)
447 / theme::base_text_size(),
448 )),
449 // The editor's own press hit-tests
450 // `checkbox_bounds`, which is what keeps a
451 // toggle in the undo history.
452 toggle: Some(markdown::Toggle::HitTested),
453 table_controls: true,
454 image_overlay: self.image_overlay.clone(),
455 image_overlay_corner: self.image_overlay_corner,
456 sizing: self.held_height(),
457 fence: Some(markdown::FenceHost {
458 rewrite: {
459 let editor = cx.entity().downgrade();
460 Rc::new(move |ix, code, _, cx| {
461 editor
462 .update(cx, |this, cx| this.set_code(ix, code, cx))
463 .ok();
464 })
465 },
466 resize: Some({
467 let editor = cx.entity().downgrade();
468 Rc::new(move |ix, y, _, cx| {
469 editor
470 .update(cx, |this, cx| this.start_height(ix, y, cx))
471 .ok();
472 })
473 }),
474 leave: {
475 let editor = cx.entity().downgrade();
476 Rc::new(move |ix, window, cx| {
477 editor
478 .update(cx, |this, cx| {
479 this.leave_block(ix, window, cx)
480 })
481 .ok();
482 })
483 },
484 }),
485 base: self.base.as_deref(),
486 // A reveal owed to a caret nobody is focused on
487 // still needs its block built to find it.
488 keep: if self.reveal.is_some() {
489 std::slice::from_ref(&self.selection.head.block)
490 } else {
491 &[]
492 },
493 scroll: self.scroll.as_ref(),
494 jump: Some({
495 let editor = cx.entity().downgrade();
496 Rc::new(move |ix, _, cx| {
497 let at = Selection::at(Cursor::new(ix, Part::Body, 0));
498 editor
499 .update(cx, |this, cx| this.select_to_top(at, cx))
500 .ok();
501 })
502 }),
503 ..Default::default()
504 },
505 window,
506 cx,
507 ),
508 }),
509 )
510 // Last, so the layouts it reads are this frame's rather than the
511 // one before — children paint in order.
512 .child(
513 canvas(|_, _, _| (), {
514 let entity = cx.entity();
515 move |_, _, window, cx| {
516 entity.update(cx, |this, cx| {
517 this.reveal_caret(window);
518 this.settle_handle(window, cx);
519 });
520 }
521 })
522 .absolute()
523 .size(gpui::px(0.0)),
524 )
525 .children(self.slash_menu(&theme, window, cx))
526 .children(self.mention_menu(&theme, window, cx))
527 .children(self.paste_menu(&theme, window, cx))
528 .children(self.url_prompt(&theme, cx))
529 .children(self.image_target(cx))
530 .children(self.resize_preview())
531 .children(self.handle(focused, &theme, cx))
532 .children(self.resize_handle(&theme, cx))
533 .children(self.drop_indicator(&theme))
534 .children(self.table_strips(&theme, cx))
535 .children(self.table_handles(&theme, cx))
536 .children(self.table_drop_indicator(&theme))
537 .children(self.dropdown(menu::Dropdown::Table, &theme, window, cx))
538 .children(self.language_chip(&theme, cx))
539 .children(self.dropdown(menu::Dropdown::Block, &theme, window, cx))
540 .children(self.dropdown(menu::Dropdown::Language, &theme, window, cx))
541 .children(self.dropdown(menu::Dropdown::Text, &theme, window, cx))
542 .children(self.dropdown(menu::Dropdown::Image, &theme, window, cx))
543 }
544}