From 666ea61dbc0311be869a998d6b26ba2fcbb2730e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 10 Jun 2022 13:47:40 +0200 Subject: [PATCH] Introduce a new `BlockStyle` field for blocks This new field allows blocks to specify how they want to be laid out: - If `Fixed` they can take up all the width they want and they will impact the scroll width of the editor. This is useful for diagnostic messages and allows scrolling the editor further to the right to visualize the entire message. - If `Flex` they can extend all the way to the scroll width without impacting it any further. This is useful for the rename editor that we insert as a block decoration when hitting `F2`. - If `Sticky`, they will be as wide as the editor element and won't participate in the horizontal scrolling of the editor. This is useful for headers in general, where we want e.g. the filename and the jump button to always be visible independently of how much the user has scrolled to the right. --- 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 | 365 ++++++++++++--------- 5 files changed, 233 insertions(+), 168 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index c7d97c1ef35464b1d87e67138eb8740fd53920c7..d123e0a688858af6c160284ca5a2ef5f4268d849 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/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") }) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 80676138e2de280e359ea754be59c3a460183804..4157b81910a4913dcf389096bca50e5c6021e3aa 100644 --- a/crates/editor/src/display_map.rs +++ b/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, diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 36abef60e5a8cb00785569d52fbb592ac579d41b..c52e080b3ea71a811a5017a99aac339c0cd01a74 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/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, disposition: BlockDisposition, } @@ -67,10 +68,18 @@ where { pub position: P, pub height: u8, + pub style: BlockStyle, pub render: Arc 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, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b0f229f32ff3bc79605ec93c0628bc6f4489f4d1..01d9945d436275415b556b382c38648e8533272b 100644 --- a/crates/editor/src/editor.rs +++ b/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, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 899063e138bf1c6a4664ad649ec0e4c9c77c7f25..ab05a98260f01284052d76f2f38a2fb3221c6f67 100644 --- a/crates/editor/src/element.rs +++ b/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); } } @@ -789,6 +792,7 @@ impl EditorElement { rows: Range, snapshot: &EditorSnapshot, 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) { let editor = if let Some(editor) = self.view.upgrade(cx) { editor } else { @@ -806,158 +810,184 @@ impl EditorElement { let tooltip_style = cx.global::().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::, _>(|(_, 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::(*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::(*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 max_width = width.max(scroll_width); + let mut blocks = Vec::new(); + for (row, block) in fixed_blocks { + let element = render_block(block, f32::INFINITY); + max_width = 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::Fixed => unreachable!(), + BlockStyle::Sticky => width, + BlockStyle::Flex => max_width, + }; + let element = render_block(block, width); + blocks.push(BlockLayout { + row, + element, + style, + }); + } + (max_width, blocks) } } @@ -1146,8 +1176,24 @@ impl Element for EditorElement { cx.text_layout_cache, ) .width(); - let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x(); + let mut scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x(); let em_width = style.text.em_width(cx.font_cache); + let (blocks_max_width, blocks) = self.layout_blocks( + start_row..end_row, + &snapshot, + size.x(), + scroll_width + gutter_width, + gutter_padding, + gutter_width, + em_width, + gutter_width + gutter_margin, + line_height, + &style, + &line_layouts, + cx, + ); + scroll_width = scroll_width.max(blocks_max_width - gutter_width); + let max_row = snapshot.max_point().row(); let scroll_max = vec2f( ((scroll_width - text_size.x()) / em_width).max(0.0), @@ -1246,20 +1292,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 +1385,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 +1472,7 @@ pub struct LayoutState { highlighted_rows: Option>, line_layouts: Vec, line_number_layouts: Vec>, - blocks: Vec<(u32, ElementBox)>, + blocks: Vec, line_height: f32, em_width: f32, em_advance: f32, @@ -1451,6 +1483,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 +1801,7 @@ mod tests { editor.set_placeholder_text("hello", cx); editor.insert_blocks( [BlockProperties { + style: BlockStyle::Fixed, disposition: BlockDisposition::Above, height: 3, position: Anchor::min(),