Skip to main content

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}