Skip to main content

markdown_editor/
element.rs

1//! `BlockEditor` view (design §7).
2//!
3//! `render` takes the cached projection and renders it with gpui-kit's own
4//! `BlockNode` renderer, so the editor looks exactly like `TextView`. The
5//! [`EditorRoot`] element installs a frame-local [`FrameSink`] while its
6//! children prepaint: keyed text leaves report their layouts into it and take
7//! their selection backgrounds from it (`text::editor_seam`, D3). The root
8//! then paints the caret and the composition underline from that geometry.
9//! Paint never updates the editor entity.
10
11use std::{cell::Cell, collections::HashMap, ops::Range, rc::Rc};
12
13use gpui::prelude::FluentBuilder as _;
14use gpui::{
15    AnyElement, App, Bounds, CursorStyle, DefiniteLength, DispatchPhase, Edges, Element, ElementId,
16    ElementInputHandler, Entity, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
17    InteractiveElement, IntoElement, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent,
18    MouseUpEvent, ParentElement, Pixels, Refineable, RenderOnce, Role, StatefulInteractiveElement,
19    Style, StyleRefinement, Styled, TextStyle, Window, canvas, div, fill, point, px, size,
20};
21
22use super::{cull::Measured, id::BlockId, projection::Snapshot, state::*, style::BlockEditorStyle};
23use gpui_kit::base::StyledExt as _;
24use gpui_kit::base::text::editor_host::{
25    BlockNode, FlowPosition, RenderContext, collect_leaf_geometry,
26};
27
28/// Items per cull unit of a long top-level list.
29const LIST_CHUNK: usize = 32;
30
31/// A block-based WYSIWYG Markdown editor (spike). Hold an
32/// `Entity<BlockEditorState>` and pass it here in `render`. Instance styling
33/// applies to the native input root; it installs no padding or height defaults.
34#[derive(IntoElement)]
35pub struct BlockEditor {
36    state: Entity<BlockEditorState>,
37    style: StyleRefinement,
38}
39
40impl BlockEditor {
41    pub fn new(state: &Entity<BlockEditorState>) -> Self {
42        Self {
43            state: state.clone(),
44            style: StyleRefinement::default(),
45        }
46    }
47}
48
49// Presentation is applied inside EditorRoot so padding is part of its
50// native hitbox and input bounds, not an unfocusable outer wrapper.
51impl Styled for BlockEditor {
52    fn style(&mut self) -> &mut StyleRefinement {
53        &mut self.style
54    }
55}
56
57/// The accessible role (and heading level) of a top-level block (Phase 5,
58/// per-block accessibility). `None` for blocks that are not exposed on
59/// their own (definitions, breaks).
60pub(crate) fn block_role(node: &BlockNode) -> Option<(Role, Option<usize>)> {
61    Some(match node {
62        BlockNode::Paragraph(_) => (Role::Paragraph, None),
63        BlockNode::Heading { level, .. } => (Role::Heading, Some(*level as usize)),
64        BlockNode::Blockquote { .. } => (Role::Blockquote, None),
65        BlockNode::List { .. } => (Role::List, None),
66        BlockNode::CodeBlock(_) => (Role::Code, None),
67        BlockNode::Table(_) => (Role::Table, None),
68        BlockNode::HorizontalRule { .. } => (Role::Splitter, None),
69        BlockNode::Custom { .. } => (Role::Group, None),
70        BlockNode::ListItem { .. } => (Role::ListItem, None),
71        BlockNode::Root { .. }
72        | BlockNode::Break { .. }
73        | BlockNode::Definition { .. }
74        | BlockNode::Unknown => return None,
75        _ => return None,
76    })
77}
78
79impl RenderOnce for BlockEditor {
80    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
81        let theme = gpui_kit::base::Theme::global(cx);
82        // Links are followed by the editor itself (Cmd/Ctrl-click); the shared
83        // renderer must never open a URL on its own.
84        let render_cx = RenderContext::from_app(cx).on_link_click(|_, _, _, _| {});
85        let text_style = render_cx.style().clone();
86        let mut style = self.state.read(cx).style.resolved(&theme.tokens);
87        if style.selection_is_default() {
88            style = style.with_selection(text_style.selection());
89        }
90        // Read the prior frame before request_layout can reset shared text
91        // layouts. Manual scrolling must not become a reflow reveal request.
92        let caret_was_visible = {
93            let state = self.state.read(cx);
94            state
95                .scroll_handle
96                .as_ref()
97                .zip(state.model.caret())
98                .is_some_and(|(handle, pos)| {
99                    state
100                        .frame
101                        .borrow()
102                        .caret_bounds(pos, style.caret_width())
103                        .is_some_and(|caret| handle.bounds().contains(&caret.center()))
104                })
105        };
106        let (focus, snapshot, backgrounds, extensions, keep, list_items) =
107            self.state.update(cx, |state, _| {
108                let snapshot = state.snapshot();
109                let mut backgrounds: HashMap<u64, Vec<(Range<usize>, Hsla)>> = HashMap::new();
110                for (block, range) in state.selection_ranges() {
111                    // Atoms paint whole (O(1) lookup, not a scan of the leaves).
112                    let range = match state.model.doc.get(block).map(|e| &e.content) {
113                        Some(super::document::BlockContent::Atom(_)) => {
114                            0..state.model.doc.leaf_len(block).unwrap_or(range.end)
115                        }
116                        _ => range,
117                    };
118                    backgrounds
119                        .entry(block.as_u64())
120                        .or_default()
121                        .push((range, style.selection()));
122                }
123                (
124                    state.focus_handle.clone(),
125                    snapshot,
126                    backgrounds,
127                    state.model.doc.extensions.clone(),
128                    state.pinned_blocks(),
129                    // Only needed to rebuild the cached cull units.
130                    (!state.cull.borrow().index.is_current(state.model.revision())).then(|| {
131                        let doc = &state.model.doc;
132                        doc.children(doc.root)
133                            .iter()
134                            .filter(|id| {
135                                matches!(
136                                    doc.kind(**id),
137                                    Some(super::kind::BlockKindData::List { .. })
138                                ) && doc.children(**id).len() > LIST_CHUNK
139                            })
140                            .map(|id| (*id, Rc::<[BlockId]>::from(doc.children(*id))))
141                            .collect::<HashMap<_, _>>()
142                    }),
143                )
144            });
145
146        // Match the native root's effective inherited/instance typography
147        // before deriving body-relative node metrics and culling geometry.
148        let mut typography = window.text_style();
149        typography.refine(&self.style.text);
150        let node_cx = render_cx
151            .with_markdown_extensions(extensions)
152            .with_body_font_size(typography.font_size.to_pixels(window.rem_size()));
153        let (cull, revision) = {
154            let state = self.state.read(cx);
155            (state.cull.clone(), state.model.revision())
156        };
157        let mut children: Vec<AnyElement> = Vec::new();
158        {
159            let mut cull_state = cull.borrow_mut();
160            cull_state.measured.clear();
161            let window_span = cull_state.visible.map(|(top, bottom)| {
162                // Overscan two viewports each way, so a fast scroll stays
163                // within laid-out blocks until the next frame catches up.
164                let margin = ((bottom - top) * 2.).max(px(600.));
165                (top - margin, bottom + margin)
166            });
167            let cull_state = &mut *cull_state;
168            if let Some(list_items) = list_items {
169                // Cull units: each top-level node, except that a long
170                // top-level list is split into chunks of `LIST_CHUNK` items,
171                // each rendered as a list of its own (continuing the
172                // numbering, no gap between chunks). Rebuilt per revision.
173                let nodes: Vec<&BlockNode> = snapshot
174                    .blocks
175                    .iter()
176                    .flat_map(|b| b.nodes.iter())
177                    .collect();
178                let mut units = Vec::with_capacity(nodes.len());
179                let mut node_ix = 0;
180                for (block_ix, block) in snapshot.blocks.iter().enumerate() {
181                    for (node_in_block, node) in block.nodes.iter().enumerate() {
182                        let flow = FlowPosition::at(&nodes, node_ix, false);
183                        let unit = |id, items| super::cull::UnitDesc {
184                            id,
185                            block: block_ix,
186                            node: node_in_block,
187                            node_ix,
188                            items,
189                            flow,
190                        };
191                        match (node, list_items.get(&block.id)) {
192                            (BlockNode::List { children, .. }, Some(ids))
193                                if ids.len() == children.len() =>
194                            {
195                                for start in (0..ids.len()).step_by(LIST_CHUNK) {
196                                    let range = start..(start + LIST_CHUNK).min(ids.len());
197                                    units.push(unit(ids[start], Some((range, ids.clone()))));
198                                }
199                            }
200                            _ => units.push(unit(block.id, None)),
201                        }
202                        node_ix += 1;
203                    }
204                }
205                cull_state.index.rebuild(
206                    revision,
207                    units.iter().map(|u| {
208                        let covered: &[BlockId] = match &u.items {
209                            Some((range, ids)) => &ids[range.clone()],
210                            None => &[],
211                        };
212                        (u.id, covered)
213                    }),
214                    &cull_state.heights,
215                );
216                cull_state.units = units;
217                cull_state.node_count = nodes.len();
218            }
219            let units = &cull_state.units;
220            let count = cull_state.node_count;
221            let index = &cull_state.index;
222            let n = units.len();
223            // Units to lay out: the visible range (binary search over the
224            // prefix sums) plus pinned units (selection ends, IME).
225            let (first, last) = match window_span {
226                None => (0, n),
227                Some((top, bottom)) => {
228                    let first = index.find(top.max(px(0.))).min(n);
229                    (first, (index.find(bottom) + 1).min(n).max(first))
230                }
231            };
232            let mut wanted: Vec<usize> = keep
233                .iter()
234                .filter_map(|id| index.unit_of.get(id).copied())
235                .filter(|ix| *ix < first || *ix >= last)
236                .collect();
237            wanted.extend(first..last);
238            wanted.sort_unstable();
239            wanted.dedup();
240            let mut cursor = px(0.);
241            let mut rendered = 0;
242            for unit_ix in wanted {
243                let unit = &units[unit_ix];
244                let node_ix = unit.node_ix;
245                let top = index.prefix(unit_ix);
246                if top > cursor {
247                    children.push(div().h(top - cursor).into_any_element());
248                }
249                cursor = top + index.height(unit_ix);
250                rendered += 1;
251                let flow = unit.flow;
252                let is_last_node = node_ix + 1 == count;
253                let node = &snapshot.blocks[unit.block].nodes[unit.node];
254                let element = match (&unit.items, node) {
255                    (
256                        Some((range, ids)),
257                        BlockNode::List {
258                            children: items,
259                            ordered,
260                            start,
261                            ..
262                        },
263                    ) => {
264                        let last_chunk = range.end == ids.len();
265                        let chunk = BlockNode::List {
266                            children: items[range.clone()].to_vec(),
267                            ordered: *ordered,
268                            start: if *ordered {
269                                Some(start.unwrap_or(1) + range.start as u32)
270                            } else {
271                                *start
272                            },
273                            span: None,
274                        };
275                        let flow = if range.start == 0 {
276                            flow
277                        } else {
278                            flow.first(false)
279                        };
280                        chunk.render(
281                            node_ix,
282                            // Inner chunks drop the list's bottom gap.
283                            flow.last(!last_chunk || is_last_node),
284                            &node_cx,
285                            window,
286                            cx,
287                        )
288                    }
289                    _ => node.render(node_ix, flow.last(is_last_node), &node_cx, window, cx),
290                };
291                // Per-block accessibility node around the rendered block.
292                let element = match block_role(node) {
293                    Some((role, level)) => div()
294                        .id(("block", unit.id.as_u64() as usize))
295                        .role(role)
296                        .when_some(level, |this, level| this.aria_level(level))
297                        .child(element)
298                        .into_any_element(),
299                    None => element,
300                };
301                children.push(
302                    Measured {
303                        id: unit.id,
304                        index: unit_ix,
305                        state: cull.clone(),
306                        child: Some(element),
307                    }
308                    .into_any_element(),
309                );
310            }
311            let total = index.prefix(n);
312            if total > cursor {
313                children.push(div().h(total - cursor).into_any_element());
314            }
315            cull_state.rendered = rendered;
316        }
317
318        let content_bounds = Rc::new(Cell::new(Bounds::default()));
319        let measured_content = content_bounds.clone();
320        let mut root_style = Style::default();
321        root_style.refine(&self.style);
322        let s = &self.state;
323        div()
324            .id("block-editor")
325            // D8: one multi-line text input over the document; the value is
326            // built only while accessibility is active (cached per revision).
327            .role(Role::MultilineTextInput)
328            .when(window.is_a11y_active() || cfg!(test), |this| {
329                this.aria_value(self.state.read(cx).accessible_value())
330            })
331            .key_context(self.state.read(cx).key_context())
332            .track_focus(&focus)
333            .cursor(if self.state.read(cx).grip_hover {
334                CursorStyle::OpenHand
335            } else if self.state.read(cx).pointer_hand {
336                CursorStyle::PointingHand
337            } else {
338                CursorStyle::IBeam
339            })
340            .w_full()
341            .on_action(window.listener_for(s, BlockEditorState::on_backspace))
342            .on_action(window.listener_for(s, BlockEditorState::on_delete))
343            .on_action(window.listener_for(s, BlockEditorState::on_delete_word_left))
344            .on_action(window.listener_for(s, BlockEditorState::on_delete_word_right))
345            .on_action(window.listener_for(s, BlockEditorState::on_delete_to_line_start))
346            .on_action(window.listener_for(s, BlockEditorState::on_delete_to_line_end))
347            .on_action(window.listener_for(s, BlockEditorState::on_move_paragraph_start))
348            .on_action(window.listener_for(s, BlockEditorState::on_move_paragraph_end))
349            .on_action(window.listener_for(s, BlockEditorState::on_select_paragraph_start))
350            .on_action(window.listener_for(s, BlockEditorState::on_select_paragraph_end))
351            .on_action(window.listener_for(s, BlockEditorState::on_enter))
352            .on_action(window.listener_for(s, BlockEditorState::on_structural_enter))
353            .on_action(window.listener_for(s, BlockEditorState::on_soft_break))
354            .on_action(window.listener_for(s, BlockEditorState::on_exit_block))
355            .on_action(window.listener_for(s, BlockEditorState::on_move_left))
356            .on_action(window.listener_for(s, BlockEditorState::on_move_right))
357            .on_action(window.listener_for(s, BlockEditorState::on_move_up))
358            .on_action(window.listener_for(s, BlockEditorState::on_move_down))
359            .on_action(window.listener_for(s, BlockEditorState::on_move_home))
360            .on_action(window.listener_for(s, BlockEditorState::on_move_end))
361            .on_action(window.listener_for(s, BlockEditorState::on_select_left))
362            .on_action(window.listener_for(s, BlockEditorState::on_move_word_left))
363            .on_action(window.listener_for(s, BlockEditorState::on_move_word_right))
364            .on_action(window.listener_for(s, BlockEditorState::on_select_word_left))
365            .on_action(window.listener_for(s, BlockEditorState::on_select_word_right))
366            .on_action(window.listener_for(s, BlockEditorState::on_select_right))
367            .on_action(window.listener_for(s, BlockEditorState::on_select_up))
368            .on_action(window.listener_for(s, BlockEditorState::on_select_down))
369            .on_action(window.listener_for(s, BlockEditorState::on_select_home))
370            .on_action(window.listener_for(s, BlockEditorState::on_select_end))
371            .on_action(window.listener_for(s, BlockEditorState::on_move_doc_start))
372            .on_action(window.listener_for(s, BlockEditorState::on_move_doc_end))
373            .on_action(window.listener_for(s, BlockEditorState::on_select_doc_start))
374            .on_action(window.listener_for(s, BlockEditorState::on_select_doc_end))
375            .on_action(window.listener_for(s, BlockEditorState::on_select_all))
376            .on_action(window.listener_for(s, BlockEditorState::on_undo))
377            .on_action(window.listener_for(s, BlockEditorState::on_redo))
378            .on_action(window.listener_for(s, BlockEditorState::on_normal_text))
379            .on_action(window.listener_for(s, BlockEditorState::on_table_insert_column_left))
380            .on_action(window.listener_for(s, BlockEditorState::on_table_insert_column_right))
381            .on_action(window.listener_for(s, BlockEditorState::on_table_insert_row_above))
382            .on_action(window.listener_for(s, BlockEditorState::on_table_insert_row_below))
383            .on_action(window.listener_for(s, BlockEditorState::on_table_delete_line))
384            .on_action(window.listener_for(s, BlockEditorState::on_close_table_menu))
385            .on_action(window.listener_for(s, BlockEditorState::on_toggle_bold))
386            .on_action(window.listener_for(s, BlockEditorState::on_toggle_italic))
387            .on_action(window.listener_for(s, BlockEditorState::on_edit_link))
388            .on_action(window.listener_for(s, BlockEditorState::on_toggle_code))
389            .on_action(window.listener_for(s, BlockEditorState::on_indent))
390            .on_action(window.listener_for(s, BlockEditorState::on_outdent))
391            .on_action(window.listener_for(s, BlockEditorState::on_copy))
392            .on_action(window.listener_for(s, BlockEditorState::on_cut))
393            .on_action(window.listener_for(s, BlockEditorState::on_paste))
394            .on_action(window.listener_for(s, BlockEditorState::on_native_paste))
395            .on_action(window.listener_for(s, BlockEditorState::on_paste_plain))
396            .children(super::table_ui::render_menu(&self.state, cx))
397            .child(EditorRoot {
398                state: self.state.clone(),
399                child: Some(
400                    div()
401                        .w_full()
402                        .refine_style(&self.style)
403                        .child(
404                            div().relative().w_full().children(children).child(
405                                canvas(
406                                    move |bounds, _, _| measured_content.set(bounds),
407                                    |_, _, _, _| {},
408                                )
409                                .absolute()
410                                .top_0()
411                                .left_0()
412                                .size_full(),
413                            ),
414                        )
415                        .into_any_element(),
416                ),
417                content_bounds,
418                padding: root_style.padding,
419                typography,
420                caret_was_visible,
421                backgrounds: Some(backgrounds),
422                snapshot,
423                style,
424            })
425    }
426}
427
428type LeafBackgrounds = HashMap<u64, Vec<(Range<usize>, Hsla)>>;
429
430struct EditorRoot {
431    state: Entity<BlockEditorState>,
432    child: Option<AnyElement>,
433    backgrounds: Option<LeafBackgrounds>,
434    snapshot: Rc<Snapshot>,
435    style: BlockEditorStyle,
436    content_bounds: Rc<Cell<Bounds<Pixels>>>,
437    padding: Edges<DefiniteLength>,
438    typography: TextStyle,
439    caret_was_visible: bool,
440}
441
442impl IntoElement for EditorRoot {
443    type Element = Self;
444    fn into_element(self) -> Self::Element {
445        self
446    }
447}
448
449pub(crate) struct Prepaint {
450    hitbox: Hitbox,
451}
452
453impl EditorRoot {
454    /// Scroll the caret into view in the owner's scroll container, once its
455    /// block has been laid out (a culled block is pinned, so it is laid out
456    /// in the frame after the caret moved there).
457    fn reveal_caret(&self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
458        let state = self.state.read(cx);
459        if !state.reveal.get() {
460            return;
461        }
462        let (Some(handle), Some(pos)) = (state.scroll_handle.clone(), state.model.caret()) else {
463            state.reveal.set(false);
464            return;
465        };
466        let Some(caret) = state
467            .frame
468            .borrow()
469            .caret_bounds(pos, self.style.caret_width())
470        else {
471            return;
472        };
473        state.reveal.set(false);
474        // The scroll owner stays at the region edge; text and the revealed
475        // caret stay inside the root's presentation inset at either scroll end.
476        let padding = self
477            .padding
478            .to_pixels(bounds.size.map(Into::into), window.rem_size());
479        let mut view = handle.bounds();
480        view.origin.y += padding.top;
481        view.size.height = (view.size.height - padding.top - padding.bottom).max(Pixels::ZERO);
482        let mut offset = handle.offset();
483        if caret.top() < view.top() {
484            offset.y += view.top() - caret.top();
485        } else if caret.bottom() > view.bottom() {
486            offset.y -= caret.bottom() - view.bottom();
487        } else {
488            return;
489        }
490        handle.set_offset(offset);
491        window.refresh();
492    }
493}
494
495impl Element for EditorRoot {
496    type RequestLayoutState = AnyElement;
497    type PrepaintState = Prepaint;
498
499    fn id(&self) -> Option<ElementId> {
500        None
501    }
502
503    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
504        None
505    }
506
507    fn request_layout(
508        &mut self,
509        _: Option<&GlobalElementId>,
510        _: Option<&InspectorElementId>,
511        window: &mut Window,
512        cx: &mut App,
513    ) -> (LayoutId, AnyElement) {
514        let mut child = self
515            .child
516            .take()
517            .unwrap_or_else(|| div().into_any_element());
518        let layout_id = child.request_layout(window, cx);
519        (layout_id, child)
520    }
521
522    fn prepaint(
523        &mut self,
524        _: Option<&GlobalElementId>,
525        _: Option<&InspectorElementId>,
526        bounds: Bounds<Pixels>,
527        child: &mut AnyElement,
528        window: &mut Window,
529        cx: &mut App,
530    ) -> Prepaint {
531        if let Some(backgrounds) = self.backgrounds.take() {
532            let (_, geometries) = collect_leaf_geometry(backgrounds, || child.prepaint(window, cx));
533            // Swap in the complete frame; handlers read it until the next one.
534            let mut frame = super::frame::into_frame(
535                geometries,
536                self.snapshot.leaves.clone(),
537                &self.snapshot.tables,
538            );
539            frame.content = Some(self.content_bounds.get());
540            {
541                let state = self.state.read(cx);
542                let doc = &state.model.doc;
543                let shapes: Vec<super::frame::TableShape> = self
544                    .snapshot
545                    .tables
546                    .iter()
547                    .map(|(table, _)| super::frame::TableShape {
548                        table: *table,
549                        grid: super::table::grid(doc, *table),
550                        aligns: match doc.kind(*table) {
551                            Some(super::kind::BlockKindData::Table { aligns }) => aligns.clone(),
552                            _ => Vec::new(),
553                        },
554                    })
555                    .collect();
556                // gpui-kit's cell padding is `px_3`: 0.75rem.
557                let pad = window.rem_size() * 0.75;
558                let right = frame.content.map(|c| c.right());
559                frame.layout_tables(&shapes, pad, right);
560            }
561            *self.state.read(cx).frame.borrow_mut() = frame;
562            let mask = window.content_mask().bounds;
563            let content = self.content_bounds.get();
564            let cull = self.state.read(cx).cull.clone();
565            let geometry_changed = cull.borrow_mut().set_geometry(
566                content.size.width,
567                window.rem_size(),
568                &self.typography,
569            );
570            // Retain a visible caret or an existing reveal request, but do not
571            // interrupt manual scrolling. Wait for fresh spacer positions.
572            if geometry_changed {
573                if self.caret_was_visible {
574                    self.state.read(cx).reveal.set(true);
575                }
576            } else {
577                self.reveal_caret(bounds, window, cx);
578            }
579            if geometry_changed || cull.borrow_mut().finish_frame(content, mask) {
580                // Geometry changed or scrolling left the laid-out span.
581                let state = self.state.clone();
582                window.on_next_frame(move |_, cx| state.update(cx, |_, cx| cx.notify()));
583            }
584        } else {
585            child.prepaint(window, cx);
586        }
587        Prepaint {
588            hitbox: window.insert_hitbox(bounds, HitboxBehavior::Normal),
589        }
590    }
591
592    fn paint(
593        &mut self,
594        _: Option<&GlobalElementId>,
595        _: Option<&InspectorElementId>,
596        bounds: Bounds<Pixels>,
597        child: &mut AnyElement,
598        prepaint: &mut Prepaint,
599        window: &mut Window,
600        cx: &mut App,
601    ) {
602        let state = self.state.read(cx);
603        let focus = state.focus_handle.clone();
604        let collapsed = state.selection_ranges().is_empty();
605        let marked = state.marked_range();
606        let caret = state.model.caret();
607        let show_caret = state.show_caret(window, cx) && collapsed;
608        let frame = state.frame.clone();
609        let menu_line = state.table_menu.as_ref().map(|m| (m.table, m.line));
610        let grips = state.table_grips_at(window.mouse_position());
611        // Node selection (a table, code block, …): the blocks' laid-out area.
612        let selected_area = state.model.selected_blocks().and_then(|(parent, range)| {
613            let doc = &state.model.doc;
614            let ids = doc.children(parent).get(range)?.to_vec();
615            let frame = frame.borrow();
616            frame
617                .leaves
618                .iter()
619                .filter(|leaf| {
620                    let path = doc.path(leaf.info.block);
621                    ids.iter()
622                        .any(|id| *id == leaf.info.block || path.contains(id))
623                })
624                .map(|leaf| leaf.bounds())
625                .reduce(|a, b| a.union(&b))
626        });
627
628        window.handle_input(
629            &focus,
630            ElementInputHandler::new(bounds, self.state.clone()),
631            cx,
632        );
633        child.paint(window, cx);
634
635        let frame = frame.borrow();
636        let style = &self.style;
637        // Block handle: a grip in the left edge of the hovered table or code
638        // block; clicking the edge node-selects it (`block_handle_at`).
639        let mouse = window.mouse_position();
640        let hovered = frame
641            .tables
642            .iter()
643            .map(|(_, b)| *b)
644            .chain(
645                frame
646                    .leaves
647                    .iter()
648                    .filter(|l| state_plain(&self.state, l.info.block, cx))
649                    .map(|l| l.bounds()),
650            )
651            .find(|b| {
652                mouse.y >= b.top()
653                    && mouse.y < b.bottom()
654                    && mouse.x >= b.left() - px(24.)
655                    && mouse.x < b.right()
656            });
657        if let Some(b) = hovered {
658            let grip = Bounds::new(
659                point(b.left() - px(14.), b.top() + px(2.)),
660                size(px(4.), px(16.).min(b.size.height)),
661            );
662            window.paint_quad(fill(grip, style.caret().opacity(0.35)).corner_radii(px(2.)));
663        }
664        // Table grips (D20): only for the hovered column / row, or the
665        // line whose menu is open.
666        for (table, line, grip) in grips {
667            let open = menu_line == Some((table, line));
668            let alpha = if open { 0.8 } else { 0.35 };
669            window.paint_quad(fill(grip, style.caret().opacity(alpha)).corner_radii(px(2.)));
670        }
671        if let Some(area) = selected_area {
672            let area = area.dilate(px(4.));
673            window.paint_quad(gpui::quad(
674                area,
675                px(4.),
676                style.selection().opacity(0.35),
677                px(2.),
678                style.caret(),
679                gpui::BorderStyle::Solid,
680            ));
681        }
682        // D22: where a dragged block would drop.
683        if let Some(line) = self.state.read(cx).drop_indicator() {
684            window.paint_quad(fill(line, style.caret()).corner_radii(px(1.)));
685        }
686        if let Some((block, range)) = marked
687            && let Some(leaf) = frame.leaf(block)
688        {
689            let a = leaf.caret_point(range.start);
690            let b = leaf.caret_point(range.end);
691            let thickness = style.caret_width();
692            let underline = Bounds::new(
693                point(a.x, a.y + leaf.line_height() - thickness),
694                size((b.x - a.x).max(thickness), thickness),
695            );
696            window.paint_quad(fill(underline, style.composition_underline()));
697        }
698        if show_caret
699            && let Some(pos) = caret
700            && let Some(b) = frame.caret_bounds(pos, style.caret_width())
701        {
702            // Whole device pixels, like the input caret: a 1px line at a
703            // fractional x would blur over two pixels.
704            let b = crate::support::snap_caret_to_device_pixels(b, window.scale_factor());
705            window.paint_quad(fill(b, style.caret()));
706        }
707        drop(frame);
708
709        // Mouse: handlers update the entity at event time, never in paint.
710        let hitbox = prepaint.hitbox.clone();
711        let entity = self.state.clone();
712        window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
713            if phase != DispatchPhase::Bubble
714                || event.button != MouseButton::Left
715                || !hitbox.is_hovered(window)
716            {
717                return;
718            }
719            // A table grip selects its row / column and opens the menu;
720            // any other click closes an open menu.
721            if !event.modifiers.shift
722                && entity.update(cx, |state, cx| {
723                    let hit = state.click_table_grip(event.position, cx);
724                    if !hit {
725                        state.close_table_menu(cx);
726                    }
727                    hit
728                })
729            {
730                let focus = entity.read(cx).focus_handle.clone();
731                window.focus(&focus, cx);
732                cx.stop_propagation();
733                return;
734            }
735            // A task checkbox toggles without moving the caret or starting
736            // a drag; nothing else handles this mouse-down.
737            if !event.modifiers.shift
738                && entity.update(cx, |state, cx| state.click_checkbox(event.position, cx))
739            {
740                cx.stop_propagation();
741                return;
742            }
743            entity.update(cx, |state, cx| {
744                let focus = state.focus_handle.clone();
745                window.focus(&focus, cx);
746                if event.modifiers.secondary() && state.follow_link_at(event.position, cx) {
747                    return;
748                }
749                state.dragging = true;
750                if !event.modifiers.shift {
751                    state.arm_block_drag(event.position);
752                }
753                state.mouse_at(event.position, event.modifiers.shift, cx);
754                if !event.modifiers.shift && event.click_count >= 2 {
755                    state.select_unit(event.click_count >= 3, cx);
756                }
757            });
758        });
759        let entity = self.state.clone();
760        window.on_mouse_event(move |event: &MouseMoveEvent, phase, _, cx| {
761            if phase != DispatchPhase::Bubble {
762                return;
763            }
764            if event.pressed_button.is_none() {
765                entity.update(cx, |state, cx| {
766                    state.hover_at(event.position, event.modifiers.secondary(), cx)
767                });
768                return;
769            }
770            if event.pressed_button != Some(MouseButton::Left) {
771                return;
772            }
773            entity.update(cx, |state, cx| {
774                if state.block_drag_to(event.position, cx) {
775                    return;
776                }
777                if state.dragging {
778                    state.mouse_at(event.position, true, cx);
779                }
780            });
781        });
782        let entity = self.state.clone();
783        window.on_mouse_event(move |_: &MouseUpEvent, phase, _, cx| {
784            if phase == DispatchPhase::Bubble {
785                entity.update(cx, |state, cx| {
786                    state.dragging = false;
787                    state.finish_block_drag(cx);
788                });
789            }
790        });
791    }
792}
793
794fn state_plain(state: &Entity<BlockEditorState>, block: BlockId, cx: &App) -> bool {
795    state.read(cx).model.doc.plain(block).is_some()
796}