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