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 => 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}