Merge pull request #1167 from zed-industries/block-scroll-width

Antonio Scandurra created

Introduce a new `BlockStyle` field for blocks

Change summary

crates/diagnostics/src/diagnostics.rs      |  10 
crates/editor/src/display_map.rs           |   3 
crates/editor/src/display_map/block_map.rs |  20 +
crates/editor/src/editor.rs                |   3 
crates/editor/src/element.rs               | 369 +++++++++++++----------
5 files changed, 237 insertions(+), 168 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -4,7 +4,7 @@ use anyhow::Result;
 use collections::{BTreeMap, HashSet};
 use editor::{
     diagnostic_block_renderer,
-    display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
+    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
     highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, ExcerptRange, MultiBuffer,
     ToOffset,
 };
@@ -348,6 +348,7 @@ impl ProjectDiagnosticsEditor {
                                 blocks_to_add.push(BlockProperties {
                                     position: header_position,
                                     height: 2,
+                                    style: BlockStyle::Sticky,
                                     render: diagnostic_header_renderer(primary),
                                     disposition: BlockDisposition::Above,
                                 });
@@ -366,6 +367,7 @@ impl ProjectDiagnosticsEditor {
                                     blocks_to_add.push(BlockProperties {
                                         position: (excerpt_id.clone(), entry.range.start.clone()),
                                         height: diagnostic.message.matches('\n').count() as u8 + 1,
+                                        style: BlockStyle::Fixed,
                                         render: diagnostic_block_renderer(diagnostic, true),
                                         disposition: BlockDisposition::Below,
                                     });
@@ -402,6 +404,7 @@ impl ProjectDiagnosticsEditor {
                     BlockProperties {
                         position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
                         height: block.height,
+                        style: block.style,
                         render: block.render,
                         disposition: block.disposition,
                     }
@@ -621,7 +624,6 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
                 .with_color(theme.warning_diagnostic.message.text.color)
         };
 
-        let x_padding = cx.gutter_padding + cx.scroll_x * cx.em_width;
         Flex::row()
             .with_child(
                 icon.constrained()
@@ -651,8 +653,8 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
             }))
             .contained()
             .with_style(style.container)
-            .with_padding_left(x_padding)
-            .with_padding_right(x_padding)
+            .with_padding_left(cx.gutter_padding)
+            .with_padding_right(cx.gutter_padding)
             .expanded()
             .named("diagnostic header")
     })

crates/editor/src/display_map.rs 🔗

@@ -20,7 +20,7 @@ use wrap_map::WrapMap;
 
 pub use block_map::{
     BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext,
-    BlockDisposition, BlockId, BlockProperties, RenderBlock, TransformBlock,
+    BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
 };
 
 pub trait ToDisplayPoint {
@@ -650,6 +650,7 @@ pub mod tests {
                                         height
                                     );
                                     BlockProperties {
+                                        style: BlockStyle::Fixed,
                                         position,
                                         height,
                                         disposition,

crates/editor/src/display_map/block_map.rs 🔗

@@ -56,6 +56,7 @@ pub struct Block {
     id: BlockId,
     position: Anchor,
     height: u8,
+    style: BlockStyle,
     render: Mutex<RenderBlock>,
     disposition: BlockDisposition,
 }
@@ -67,10 +68,18 @@ where
 {
     pub position: P,
     pub height: u8,
+    pub style: BlockStyle,
     pub render: Arc<dyn Fn(&mut BlockContext) -> ElementBox>,
     pub disposition: BlockDisposition,
 }
 
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub enum BlockStyle {
+    Fixed,
+    Flex,
+    Sticky,
+}
+
 pub struct BlockContext<'a, 'b> {
     pub cx: &'b mut RenderContext<'a, crate::Editor>,
     pub anchor_x: f32,
@@ -513,6 +522,7 @@ impl<'a> BlockMapWriter<'a> {
                     height: block.height,
                     render: Mutex::new(block.render),
                     disposition: block.disposition,
+                    style: block.style,
                 }),
             );
 
@@ -940,6 +950,10 @@ impl Block {
     pub fn position(&self) -> &Anchor {
         &self.position
     }
+
+    pub fn style(&self) -> BlockStyle {
+        self.style
+    }
 }
 
 impl Debug for Block {
@@ -1018,18 +1032,21 @@ mod tests {
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
         let block_ids = writer.insert(vec![
             BlockProperties {
+                style: BlockStyle::Fixed,
                 position: buffer_snapshot.anchor_after(Point::new(1, 0)),
                 height: 1,
                 disposition: BlockDisposition::Above,
                 render: Arc::new(|_| Empty::new().named("block 1")),
             },
             BlockProperties {
+                style: BlockStyle::Fixed,
                 position: buffer_snapshot.anchor_after(Point::new(1, 2)),
                 height: 2,
                 disposition: BlockDisposition::Above,
                 render: Arc::new(|_| Empty::new().named("block 2")),
             },
             BlockProperties {
+                style: BlockStyle::Fixed,
                 position: buffer_snapshot.anchor_after(Point::new(3, 3)),
                 height: 3,
                 disposition: BlockDisposition::Below,
@@ -1183,12 +1200,14 @@ mod tests {
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
         writer.insert(vec![
             BlockProperties {
+                style: BlockStyle::Fixed,
                 position: buffer_snapshot.anchor_after(Point::new(1, 12)),
                 disposition: BlockDisposition::Above,
                 render: Arc::new(|_| Empty::new().named("block 1")),
                 height: 1,
             },
             BlockProperties {
+                style: BlockStyle::Fixed,
                 position: buffer_snapshot.anchor_after(Point::new(1, 1)),
                 disposition: BlockDisposition::Below,
                 render: Arc::new(|_| Empty::new().named("block 2")),
@@ -1286,6 +1305,7 @@ mod tests {
                                 height
                             );
                             BlockProperties {
+                                style: BlockStyle::Fixed,
                                 position,
                                 height,
                                 disposition,

crates/editor/src/editor.rs 🔗

@@ -4801,6 +4801,7 @@ impl Editor {
                     cx.focus(&rename_editor);
                     let block_id = this.insert_blocks(
                         [BlockProperties {
+                            style: BlockStyle::Flex,
                             position: range.start.clone(),
                             height: 1,
                             render: Arc::new({
@@ -4985,6 +4986,7 @@ impl Editor {
                         let diagnostic = entry.diagnostic.clone();
                         let message_height = diagnostic.message.lines().count() as u8;
                         BlockProperties {
+                            style: BlockStyle::Fixed,
                             position: buffer.anchor_after(entry.range.start),
                             height: message_height,
                             render: diagnostic_block_renderer(diagnostic, true),
@@ -7932,6 +7934,7 @@ mod tests {
         editor.update(cx, |editor, cx| {
             editor.insert_blocks(
                 [BlockProperties {
+                    style: BlockStyle::Fixed,
                     position: snapshot.anchor_after(Point::new(2, 0)),
                     disposition: BlockDisposition::Below,
                     height: 1,

crates/editor/src/element.rs 🔗

@@ -3,9 +3,9 @@ use super::{
     Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase,
     SoftWrap, ToPoint, MAX_LINE_LEN,
 };
-use crate::hover_popover::HoverAt;
 use crate::{
-    display_map::{DisplaySnapshot, TransformBlock},
+    display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
+    hover_popover::HoverAt,
     EditorStyle,
 };
 use clock::ReplicaId;
@@ -617,10 +617,13 @@ impl EditorElement {
         let scroll_left = scroll_position.x() * layout.em_width;
         let scroll_top = scroll_position.y() * layout.line_height;
 
-        for (row, element) in &mut layout.blocks {
-            let origin = bounds.origin()
-                + vec2f(-scroll_left, *row as f32 * layout.line_height - scroll_top);
-            element.paint(origin, visible_bounds, cx);
+        for block in &mut layout.blocks {
+            let mut origin =
+                bounds.origin() + vec2f(0., block.row as f32 * layout.line_height - scroll_top);
+            if !matches!(block.style, BlockStyle::Sticky) {
+                origin += vec2f(-scroll_left, 0.);
+            }
+            block.element.paint(origin, visible_bounds, cx);
         }
     }
 
@@ -788,7 +791,8 @@ impl EditorElement {
         &mut self,
         rows: Range<u32>,
         snapshot: &EditorSnapshot,
-        width: f32,
+        editor_width: f32,
+        scroll_width: f32,
         gutter_padding: f32,
         gutter_width: f32,
         em_width: f32,
@@ -797,7 +801,7 @@ impl EditorElement {
         style: &EditorStyle,
         line_layouts: &[text_layout::Line],
         cx: &mut LayoutContext,
-    ) -> Vec<(u32, ElementBox)> {
+    ) -> (f32, Vec<BlockLayout>) {
         let editor = if let Some(editor) = self.view.upgrade(cx) {
             editor
         } else {
@@ -806,158 +810,189 @@ impl EditorElement {
 
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
         let scroll_x = snapshot.scroll_position.x();
-        snapshot
+        let (fixed_blocks, non_fixed_blocks) = snapshot
             .blocks_in_range(rows.clone())
-            .map(|(block_row, block)| {
-                let mut element = match block {
-                    TransformBlock::Custom(block) => {
-                        let align_to = block
-                            .position()
-                            .to_point(&snapshot.buffer_snapshot)
-                            .to_display_point(snapshot);
-                        let anchor_x = text_x
-                            + if rows.contains(&align_to.row()) {
-                                line_layouts[(align_to.row() - rows.start) as usize]
-                                    .x_for_index(align_to.column() as usize)
-                            } else {
-                                layout_line(align_to.row(), snapshot, style, cx.text_layout_cache)
-                                    .x_for_index(align_to.column() as usize)
-                            };
+            .partition::<Vec<_>, _>(|(_, block)| match block {
+                TransformBlock::ExcerptHeader { .. } => false,
+                TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
+            });
+        let mut render_block = |block: &TransformBlock, width: f32| {
+            let mut element = match block {
+                TransformBlock::Custom(block) => {
+                    let align_to = block
+                        .position()
+                        .to_point(&snapshot.buffer_snapshot)
+                        .to_display_point(snapshot);
+                    let anchor_x = text_x
+                        + if rows.contains(&align_to.row()) {
+                            line_layouts[(align_to.row() - rows.start) as usize]
+                                .x_for_index(align_to.column() as usize)
+                        } else {
+                            layout_line(align_to.row(), snapshot, style, cx.text_layout_cache)
+                                .x_for_index(align_to.column() as usize)
+                        };
+
+                    cx.render(&editor, |_, cx| {
+                        block.render(&mut BlockContext {
+                            cx,
+                            anchor_x,
+                            gutter_padding,
+                            line_height,
+                            scroll_x,
+                            gutter_width,
+                            em_width,
+                        })
+                    })
+                }
+                TransformBlock::ExcerptHeader {
+                    key,
+                    buffer,
+                    range,
+                    starts_new_buffer,
+                    ..
+                } => {
+                    let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
+                        let jump_position = range
+                            .primary
+                            .as_ref()
+                            .map_or(range.context.start, |primary| primary.start);
+                        let jump_action = crate::Jump {
+                            path: ProjectPath {
+                                worktree_id: file.worktree_id(cx),
+                                path: file.path.clone(),
+                            },
+                            position: language::ToPoint::to_point(&jump_position, buffer),
+                            anchor: jump_position,
+                        };
 
+                        enum JumpIcon {}
                         cx.render(&editor, |_, cx| {
-                            block.render(&mut BlockContext {
-                                cx,
-                                anchor_x,
-                                gutter_padding,
-                                line_height,
-                                scroll_x,
-                                gutter_width,
-                                em_width,
+                            MouseEventHandler::new::<JumpIcon, _, _>(*key, cx, |state, _| {
+                                let style = style.jump_icon.style_for(state, false);
+                                Svg::new("icons/jump.svg")
+                                    .with_color(style.color)
+                                    .constrained()
+                                    .with_width(style.icon_width)
+                                    .aligned()
+                                    .contained()
+                                    .with_style(style.container)
+                                    .constrained()
+                                    .with_width(style.button_width)
+                                    .with_height(style.button_width)
+                                    .boxed()
                             })
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .on_click(move |_, _, cx| cx.dispatch_action(jump_action.clone()))
+                            .with_tooltip(
+                                *key,
+                                "Jump to Buffer".to_string(),
+                                Some(Box::new(crate::OpenExcerpts)),
+                                tooltip_style.clone(),
+                                cx,
+                            )
+                            .aligned()
+                            .flex_float()
+                            .boxed()
                         })
-                    }
-                    TransformBlock::ExcerptHeader {
-                        key,
-                        buffer,
-                        range,
-                        starts_new_buffer,
-                        ..
-                    } => {
-                        let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
-                            let jump_position = range
-                                .primary
-                                .as_ref()
-                                .map_or(range.context.start, |primary| primary.start);
-                            let jump_action = crate::Jump {
-                                path: ProjectPath {
-                                    worktree_id: file.worktree_id(cx),
-                                    path: file.path.clone(),
-                                },
-                                position: language::ToPoint::to_point(&jump_position, buffer),
-                                anchor: jump_position,
-                            };
+                    });
 
-                            enum JumpIcon {}
-                            cx.render(&editor, |_, cx| {
-                                MouseEventHandler::new::<JumpIcon, _, _>(*key, cx, |state, _| {
-                                    let style = style.jump_icon.style_for(state, false);
-                                    Svg::new("icons/jump.svg")
-                                        .with_color(style.color)
-                                        .constrained()
-                                        .with_width(style.icon_width)
-                                        .aligned()
-                                        .contained()
-                                        .with_style(style.container)
-                                        .constrained()
-                                        .with_width(style.button_width)
-                                        .with_height(style.button_width)
-                                        .boxed()
-                                })
-                                .with_cursor_style(CursorStyle::PointingHand)
-                                .on_click({
-                                    move |_, _, cx| cx.dispatch_action(jump_action.clone())
-                                })
-                                .with_tooltip(
-                                    *key,
-                                    "Jump to Buffer".to_string(),
-                                    Some(Box::new(crate::OpenExcerpts)),
-                                    tooltip_style.clone(),
-                                    cx,
-                                )
-                                .aligned()
-                                .flex_float()
-                                .boxed()
-                            })
-                        });
+                    if *starts_new_buffer {
+                        let style = &self.style.diagnostic_path_header;
+                        let font_size =
+                            (style.text_scale_factor * self.style.text.font_size).round();
+
+                        let mut filename = None;
+                        let mut parent_path = None;
+                        if let Some(file) = buffer.file() {
+                            let path = file.path();
+                            filename = path.file_name().map(|f| f.to_string_lossy().to_string());
+                            parent_path =
+                                path.parent().map(|p| p.to_string_lossy().to_string() + "/");
+                        }
 
-                        let padding = gutter_padding + scroll_x * em_width;
-                        if *starts_new_buffer {
-                            let style = &self.style.diagnostic_path_header;
-                            let font_size =
-                                (style.text_scale_factor * self.style.text.font_size).round();
-
-                            let mut filename = None;
-                            let mut parent_path = None;
-                            if let Some(file) = buffer.file() {
-                                let path = file.path();
-                                filename =
-                                    path.file_name().map(|f| f.to_string_lossy().to_string());
-                                parent_path =
-                                    path.parent().map(|p| p.to_string_lossy().to_string() + "/");
-                            }
-
-                            Flex::row()
-                                .with_child(
-                                    Label::new(
-                                        filename.unwrap_or_else(|| "untitled".to_string()),
-                                        style.filename.text.clone().with_font_size(font_size),
-                                    )
-                                    .contained()
-                                    .with_style(style.filename.container)
-                                    .aligned()
-                                    .boxed(),
+                        Flex::row()
+                            .with_child(
+                                Label::new(
+                                    filename.unwrap_or_else(|| "untitled".to_string()),
+                                    style.filename.text.clone().with_font_size(font_size),
                                 )
-                                .with_children(parent_path.map(|path| {
-                                    Label::new(
-                                        path,
-                                        style.path.text.clone().with_font_size(font_size),
-                                    )
+                                .contained()
+                                .with_style(style.filename.container)
+                                .aligned()
+                                .boxed(),
+                            )
+                            .with_children(parent_path.map(|path| {
+                                Label::new(path, style.path.text.clone().with_font_size(font_size))
                                     .contained()
                                     .with_style(style.path.container)
                                     .aligned()
                                     .boxed()
-                                }))
-                                .with_children(jump_icon)
-                                .contained()
-                                .with_style(style.container)
-                                .with_padding_left(padding)
-                                .with_padding_right(padding)
-                                .expanded()
-                                .named("path header block")
-                        } else {
-                            let text_style = self.style.text.clone();
-                            Flex::row()
-                                .with_child(Label::new("…".to_string(), text_style).boxed())
-                                .with_children(jump_icon)
-                                .contained()
-                                .with_padding_left(padding)
-                                .with_padding_right(padding)
-                                .expanded()
-                                .named("collapsed context")
-                        }
+                            }))
+                            .with_children(jump_icon)
+                            .contained()
+                            .with_style(style.container)
+                            .with_padding_left(gutter_padding)
+                            .with_padding_right(gutter_padding)
+                            .expanded()
+                            .named("path header block")
+                    } else {
+                        let text_style = self.style.text.clone();
+                        Flex::row()
+                            .with_child(Label::new("…".to_string(), text_style).boxed())
+                            .with_children(jump_icon)
+                            .contained()
+                            .with_padding_left(gutter_padding)
+                            .with_padding_right(gutter_padding)
+                            .expanded()
+                            .named("collapsed context")
                     }
-                };
+                }
+            };
 
-                element.layout(
-                    SizeConstraint {
-                        min: Vector2F::zero(),
-                        max: vec2f(width, block.height() as f32 * line_height),
-                    },
-                    cx,
-                );
-                (block_row, element)
-            })
-            .collect()
+            element.layout(
+                SizeConstraint {
+                    min: Vector2F::zero(),
+                    max: vec2f(width, block.height() as f32 * line_height),
+                },
+                cx,
+            );
+            element
+        };
+
+        let mut fixed_block_max_width = 0f32;
+        let mut blocks = Vec::new();
+        for (row, block) in fixed_blocks {
+            let element = render_block(block, f32::INFINITY);
+            fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width);
+            blocks.push(BlockLayout {
+                row,
+                element,
+                style: BlockStyle::Fixed,
+            });
+        }
+        for (row, block) in non_fixed_blocks {
+            let style = match block {
+                TransformBlock::Custom(block) => block.style(),
+                TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky,
+            };
+            let width = match style {
+                BlockStyle::Sticky => editor_width,
+                BlockStyle::Flex => editor_width
+                    .max(fixed_block_max_width)
+                    .max(gutter_width + scroll_width),
+                BlockStyle::Fixed => unreachable!(),
+            };
+            let element = render_block(block, width);
+            blocks.push(BlockLayout {
+                row,
+                element,
+                style,
+            });
+        }
+        (
+            scroll_width.max(fixed_block_max_width - gutter_width),
+            blocks,
+        )
     }
 }
 
@@ -1148,6 +1183,21 @@ impl Element for EditorElement {
         .width();
         let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x();
         let em_width = style.text.em_width(cx.font_cache);
+        let (scroll_width, blocks) = self.layout_blocks(
+            start_row..end_row,
+            &snapshot,
+            size.x(),
+            scroll_width,
+            gutter_padding,
+            gutter_width,
+            em_width,
+            gutter_width + gutter_margin,
+            line_height,
+            &style,
+            &line_layouts,
+            cx,
+        );
+
         let max_row = snapshot.max_point().row();
         let scroll_max = vec2f(
             ((scroll_width - text_size.x()) / em_width).max(0.0),
@@ -1246,20 +1296,6 @@ impl Element for EditorElement {
             );
         }
 
-        let blocks = self.layout_blocks(
-            start_row..end_row,
-            &snapshot,
-            size.x().max(scroll_width + gutter_width),
-            gutter_padding,
-            gutter_width,
-            em_width,
-            gutter_width + gutter_margin,
-            line_height,
-            &style,
-            &line_layouts,
-            cx,
-        );
-
         (
             size,
             LayoutState {
@@ -1353,8 +1389,8 @@ impl Element for EditorElement {
             }
         }
 
-        for (_, block) in &mut layout.blocks {
-            if block.dispatch_event(event, cx) {
+        for block in &mut layout.blocks {
+            if block.element.dispatch_event(event, cx) {
                 return true;
             }
         }
@@ -1440,7 +1476,7 @@ pub struct LayoutState {
     highlighted_rows: Option<Range<u32>>,
     line_layouts: Vec<text_layout::Line>,
     line_number_layouts: Vec<Option<text_layout::Line>>,
-    blocks: Vec<(u32, ElementBox)>,
+    blocks: Vec<BlockLayout>,
     line_height: f32,
     em_width: f32,
     em_advance: f32,
@@ -1451,6 +1487,12 @@ pub struct LayoutState {
     hover: Option<(DisplayPoint, ElementBox)>,
 }
 
+struct BlockLayout {
+    row: u32,
+    element: ElementBox,
+    style: BlockStyle,
+}
+
 fn layout_line(
     row: u32,
     snapshot: &EditorSnapshot,
@@ -1763,6 +1805,7 @@ mod tests {
             editor.set_placeholder_text("hello", cx);
             editor.insert_blocks(
                 [BlockProperties {
+                    style: BlockStyle::Fixed,
                     disposition: BlockDisposition::Above,
                     height: 3,
                     position: Anchor::min(),