From aefdde66a6e2f064ddd941a9852601b45db6c2f8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 15:44:35 +0200 Subject: [PATCH 01/12] Pass a `&mut BlockContext` when rendering blocks This wraps and derefs to `RenderContext`, so that we can easily use `MouseEventHandler`s in blocks. --- crates/diagnostics/src/diagnostics.rs | 86 +++++++++++----------- crates/editor/src/display_map/block_map.rs | 51 ++++++------- crates/editor/src/editor.rs | 4 +- crates/editor/src/element.rs | 24 ++++-- 4 files changed, 83 insertions(+), 82 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index b5361b4e5b0d7595bde196064c3e18bceca0500c..cd258b98f11fedb7689bd58170ca123ee55bae73 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -702,7 +702,7 @@ mod tests { use super::*; use editor::{ display_map::{BlockContext, TransformBlock}, - DisplayPoint, EditorSnapshot, + DisplayPoint, }; use gpui::TestAppContext; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16}; @@ -835,10 +835,8 @@ mod tests { view.next_notification(&cx).await; view.update(cx, |view, cx| { - let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx)); - assert_eq!( - editor_blocks(&editor, cx), + editor_blocks(&view.editor, cx), [ (0, "path header block".into()), (2, "diagnostic header".into()), @@ -848,7 +846,7 @@ mod tests { ] ); assert_eq!( - editor.text(), + view.editor.update(cx, |editor, cx| editor.display_text(cx)), concat!( // // main.rs @@ -923,10 +921,8 @@ mod tests { view.next_notification(&cx).await; view.update(cx, |view, cx| { - let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx)); - assert_eq!( - editor_blocks(&editor, cx), + editor_blocks(&view.editor, cx), [ (0, "path header block".into()), (2, "diagnostic header".into()), @@ -938,7 +934,7 @@ mod tests { ] ); assert_eq!( - editor.text(), + view.editor.update(cx, |editor, cx| editor.display_text(cx)), concat!( // // consts.rs @@ -1038,10 +1034,8 @@ mod tests { view.next_notification(&cx).await; view.update(cx, |view, cx| { - let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx)); - assert_eq!( - editor_blocks(&editor, cx), + editor_blocks(&view.editor, cx), [ (0, "path header block".into()), (2, "diagnostic header".into()), @@ -1055,7 +1049,7 @@ mod tests { ] ); assert_eq!( - editor.text(), + view.editor.update(cx, |editor, cx| editor.display_text(cx)), concat!( // // consts.rs @@ -1115,36 +1109,44 @@ mod tests { }); } - fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> { - editor - .blocks_in_range(0..editor.max_point().row()) - .filter_map(|(row, block)| { - let name = match block { - TransformBlock::Custom(block) => block - .render(&BlockContext { - cx, - anchor_x: 0., - scroll_x: 0., - gutter_padding: 0., - gutter_width: 0., - line_height: 0., - em_width: 0., - }) - .name()? - .to_string(), - TransformBlock::ExcerptHeader { - starts_new_buffer, .. - } => { - if *starts_new_buffer { - "path header block".to_string() - } else { - "collapsed context".to_string() + fn editor_blocks( + editor: &ViewHandle, + cx: &mut MutableAppContext, + ) -> Vec<(u32, String)> { + let mut presenter = cx.build_presenter(editor.id(), 0.); + let mut cx = presenter.build_layout_context(Default::default(), false, cx); + cx.render(editor, |editor, cx| { + let snapshot = editor.snapshot(cx); + snapshot + .blocks_in_range(0..snapshot.max_point().row()) + .filter_map(|(row, block)| { + let name = match block { + TransformBlock::Custom(block) => block + .render(&mut BlockContext { + cx, + anchor_x: 0., + scroll_x: 0., + gutter_padding: 0., + gutter_width: 0., + line_height: 0., + em_width: 0., + }) + .name()? + .to_string(), + TransformBlock::ExcerptHeader { + starts_new_buffer, .. + } => { + if *starts_new_buffer { + "path header block".to_string() + } else { + "collapsed context".to_string() + } } - } - }; + }; - Some((row, name)) - }) - .collect() + Some((row, name)) + }) + .collect() + }) } } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index c5c06048e58d6c3ce9f210d98a878bc2ae240163..bdfff4862c41d156cfad9c5bf8376e0e84a6a9e7 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -4,14 +4,14 @@ use super::{ }; use crate::{Anchor, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; -use gpui::{AppContext, ElementBox}; +use gpui::{ElementBox, RenderContext}; use language::{BufferSnapshot, Chunk, Patch}; use parking_lot::Mutex; use std::{ cell::RefCell, cmp::{self, Ordering}, fmt::Debug, - ops::{Deref, Range}, + ops::{Deref, DerefMut, Range}, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, @@ -50,7 +50,7 @@ struct BlockRow(u32); #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] struct WrapRow(u32); -pub type RenderBlock = Arc ElementBox>; +pub type RenderBlock = Arc ElementBox>; pub struct Block { id: BlockId, @@ -67,12 +67,12 @@ where { pub position: P, pub height: u8, - pub render: Arc ElementBox>, + pub render: Arc ElementBox>, pub disposition: BlockDisposition, } -pub struct BlockContext<'a> { - pub cx: &'a AppContext, +pub struct BlockContext<'a, 'b> { + pub cx: &'b mut RenderContext<'a, crate::Editor>, pub anchor_x: f32, pub scroll_x: f32, pub gutter_width: f32, @@ -916,16 +916,22 @@ impl BlockDisposition { } } -impl<'a> Deref for BlockContext<'a> { - type Target = AppContext; +impl<'a, 'b> Deref for BlockContext<'a, 'b> { + type Target = RenderContext<'a, crate::Editor>; fn deref(&self) -> &Self::Target { - &self.cx + self.cx + } +} + +impl<'a, 'b> DerefMut for BlockContext<'a, 'b> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.cx } } impl Block { - pub fn render(&self, cx: &BlockContext) -> ElementBox { + pub fn render(&self, cx: &mut BlockContext) -> ElementBox { self.render.lock()(cx) } @@ -1008,7 +1014,7 @@ mod tests { let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); - writer.insert(vec![ + let block_ids = writer.insert(vec![ BlockProperties { position: buffer_snapshot.anchor_after(Point::new(1, 0)), height: 1, @@ -1036,22 +1042,7 @@ mod tests { .blocks_in_range(0..8) .map(|(start_row, block)| { let block = block.as_custom().unwrap(); - ( - start_row..start_row + block.height as u32, - block - .render(&BlockContext { - cx, - anchor_x: 0., - gutter_padding: 0., - scroll_x: 0., - gutter_width: 0., - line_height: 0., - em_width: 0., - }) - .name() - .unwrap() - .to_string(), - ) + (start_row..start_row + block.height as u32, block.id) }) .collect::>(); @@ -1059,9 +1050,9 @@ mod tests { assert_eq!( blocks, &[ - (1..2, "block 1".to_string()), - (2..4, "block 2".to_string()), - (7..10, "block 3".to_string()), + (1..2, block_ids[0]), + (2..4, block_ids[1]), + (7..10, block_ids[2]), ] ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d82a5baece26eed60bff60eefc043f517d9dd8df..32773c9d28ac010c39e9b05a8183df96a202e3fb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4745,7 +4745,7 @@ impl Editor { height: 1, render: Arc::new({ let editor = rename_editor.clone(); - move |cx: &BlockContext| { + move |cx: &mut BlockContext| { ChildView::new(editor.clone()) .contained() .with_padding_left(cx.anchor_x) @@ -5866,7 +5866,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend highlighted_lines.push(highlight_diagnostic_message(line)); } - Arc::new(move |cx: &BlockContext| { + Arc::new(move |cx: &mut BlockContext| { let settings = cx.global::(); let theme = &settings.theme.editor; let style = diagnostic_style(diagnostic.severity, is_valid, theme); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d5a4f22eb563d6525c9c5594fb6c9af67de84ee8..39091ecad1f557190f3bdeceae5dd96333e75a97 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -755,6 +755,12 @@ impl EditorElement { line_layouts: &[text_layout::Line], cx: &mut LayoutContext, ) -> Vec<(u32, ElementBox)> { + let editor = if let Some(editor) = self.view.upgrade(cx) { + editor + } else { + return Default::default(); + }; + let scroll_x = snapshot.scroll_position.x(); snapshot .blocks_in_range(rows.clone()) @@ -774,14 +780,16 @@ impl EditorElement { .x_for_index(align_to.column() as usize) }; - block.render(&BlockContext { - cx, - anchor_x, - gutter_padding, - line_height, - scroll_x, - gutter_width, - em_width, + cx.render(&editor, |_, cx| { + block.render(&mut BlockContext { + cx, + anchor_x, + gutter_padding, + line_height, + scroll_x, + gutter_width, + em_width, + }) }) } TransformBlock::ExcerptHeader { From 4f9c20742500a30aaa5e8e745090b04b051225a4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 15:56:59 +0200 Subject: [PATCH 02/12] Show a clickable jump icon for each diagnostic group header --- assets/icons/jump.svg | 3 +++ crates/diagnostics/src/diagnostics.rs | 37 +++++++++++++++++++++++---- crates/theme/src/theme.rs | 3 ++- styles/src/styleTree/editor.ts | 8 ++++++ 4 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 assets/icons/jump.svg diff --git a/assets/icons/jump.svg b/assets/icons/jump.svg new file mode 100644 index 0000000000000000000000000000000000000000..4e89fc433964e88b5d255f5851b1aa51092dd4ca --- /dev/null +++ b/assets/icons/jump.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index cd258b98f11fedb7689bd58170ca123ee55bae73..8dc1b27d700d80e598874ccffd6612ad0cb268ae 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -8,9 +8,9 @@ use editor::{ highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset, }; use gpui::{ - actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity, - ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, - WeakViewHandle, + actions, elements::*, fonts::TextStyle, platform::CursorStyle, serde_json, AnyViewHandle, + AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, + ViewHandle, WeakViewHandle, }; use language::{ Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal, @@ -576,11 +576,13 @@ impl workspace::Item for ProjectDiagnosticsEditor { } fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { + enum JumpIcon {} + let (message, highlights) = highlight_diagnostic_message(&diagnostic.message); Arc::new(move |cx| { let settings = cx.global::(); let theme = &settings.theme.editor; - let style = &theme.diagnostic_header; + let style = theme.diagnostic_header.clone(); let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); let icon_width = cx.em_width * style.icon_width_factor; let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { @@ -591,6 +593,7 @@ 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() @@ -618,9 +621,33 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { .aligned() .boxed() })) + .with_child( + MouseEventHandler::new::(0, 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(|_, _, cx| { + dbg!("click!"); + }) + .aligned() + .flex_float() + .boxed(), + ) .contained() .with_style(style.container) - .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width) + .with_padding_left(x_padding) + .with_padding_right(x_padding) .expanded() .named("diagnostic header") }) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a8c28f41f37f82f6aa63f13bc694ebe522e085e3..d5f50b08e24a2072121c969d7bd8106748a1a110 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -317,7 +317,7 @@ pub struct Icon { pub path: String, } -#[derive(Deserialize, Default)] +#[derive(Clone, Deserialize, Default)] pub struct IconButton { #[serde(flatten)] pub container: ContainerStyle, @@ -461,6 +461,7 @@ pub struct DiagnosticHeader { pub code: ContainedText, pub text_scale_factor: f32, pub icon_width_factor: f32, + pub jump_icon: Interactive, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 06f0d98d70fab9ac3096591fc5a0769fdeac5007..3edd205f9a4008d978b2b44e5ab3e289bb82ebbc 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -98,6 +98,14 @@ export default function editor(theme: Theme) { background: backgroundColor(theme, 300), iconWidthFactor: 1.5, textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these. + jumpIcon: { + color: iconColor(theme, "primary"), + iconWidth: 10, + buttonWidth: 10, + hover: { + color: iconColor(theme, "active") + } + }, border: border(theme, "secondary", { bottom: true, top: true, From d180f7a2c3061d913fa24476dc802a3e07a946ff Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 16:25:14 +0200 Subject: [PATCH 03/12] Jump to primary diagnostic when clicking on header's jump icon --- crates/diagnostics/src/diagnostics.rs | 101 ++++++++++++++++++-------- crates/workspace/src/pane.rs | 4 +- 2 files changed, 75 insertions(+), 30 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 8dc1b27d700d80e598874ccffd6612ad0cb268ae..89ff46bf6420f5f2b6d0ad1bf6943eb17c9ec65a 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -8,12 +8,12 @@ use editor::{ highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset, }; use gpui::{ - actions, elements::*, fonts::TextStyle, platform::CursorStyle, serde_json, AnyViewHandle, - AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + actions, elements::*, fonts::TextStyle, impl_internal_actions, platform::CursorStyle, + serde_json, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, + Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{ - Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal, + Bias, Buffer, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal, }; use project::{DiagnosticSummary, Project, ProjectPath}; use serde_json::json; @@ -27,15 +27,18 @@ use std::{ path::PathBuf, sync::Arc, }; -use util::TryFutureExt; +use util::{ResultExt, TryFutureExt}; use workspace::{ItemHandle as _, ItemNavHistory, Workspace}; actions!(diagnostics, [Deploy]); +impl_internal_actions!(diagnostics, [Jump]); + const CONTEXT_LINE_COUNT: u32 = 1; pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectDiagnosticsEditor::deploy); + cx.add_action(ProjectDiagnosticsEditor::jump); items::init(cx); } @@ -56,6 +59,12 @@ struct PathState { diagnostic_groups: Vec, } +#[derive(Clone, Debug)] +struct Jump { + path: ProjectPath, + range: Range, +} + struct DiagnosticGroupState { primary_diagnostic: DiagnosticEntry, primary_excerpt_ix: usize, @@ -177,6 +186,24 @@ impl ProjectDiagnosticsEditor { } } + fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext) { + let editor = workspace.open_path(action.path.clone(), true, cx); + let range = action.range.clone(); + cx.spawn_weak(|_, mut cx| async move { + let editor = editor.await.log_err()?.downcast::()?; + editor.update(&mut cx, |editor, cx| { + let buffer = editor.buffer().read(cx).as_singleton()?; + let cursor = buffer.read(cx).clip_point(range.start, Bias::Left); + editor.change_selections(Some(Autoscroll::Newest), cx, |s| { + s.select_ranges([cursor..cursor]); + }); + Some(()) + })?; + Some(()) + }) + .detach() + } + fn update_excerpts(&mut self, cx: &mut ViewContext) { let paths = mem::take(&mut self.paths_to_update); let project = self.project.clone(); @@ -311,14 +338,19 @@ impl ProjectDiagnosticsEditor { if is_first_excerpt_for_group { is_first_excerpt_for_group = false; let mut primary = - group.entries[group.primary_ix].diagnostic.clone(); - primary.message = - primary.message.split('\n').next().unwrap().to_string(); + group.entries[group.primary_ix].resolve::(&snapshot); + primary.diagnostic.message = primary + .diagnostic + .message + .split('\n') + .next() + .unwrap() + .to_string(); group_state.block_count += 1; blocks_to_add.push(BlockProperties { position: header_position, height: 2, - render: diagnostic_header_renderer(primary), + render: diagnostic_header_renderer(primary, path.clone()), disposition: BlockDisposition::Above, }); } @@ -575,17 +607,17 @@ impl workspace::Item for ProjectDiagnosticsEditor { } } -fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { +fn diagnostic_header_renderer(entry: DiagnosticEntry, path: ProjectPath) -> RenderBlock { enum JumpIcon {} - let (message, highlights) = highlight_diagnostic_message(&diagnostic.message); + let (message, highlights) = highlight_diagnostic_message(&entry.diagnostic.message); Arc::new(move |cx| { let settings = cx.global::(); let theme = &settings.theme.editor; let style = theme.diagnostic_header.clone(); let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); let icon_width = cx.em_width * style.icon_width_factor; - let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { + let icon = if entry.diagnostic.severity == DiagnosticSeverity::ERROR { Svg::new("icons/diagnostic-error-10.svg") .with_color(theme.error_diagnostic.message.text.color) } else { @@ -614,7 +646,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { .aligned() .boxed(), ) - .with_children(diagnostic.code.clone().map(|code| { + .with_children(entry.diagnostic.code.clone().map(|code| { Label::new(code, style.code.text.clone().with_font_size(font_size)) .contained() .with_style(style.code.container) @@ -622,23 +654,34 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { .boxed() })) .with_child( - MouseEventHandler::new::(0, 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() - }) + MouseEventHandler::new::( + entry.diagnostic.group_id, + 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(|_, _, cx| { - dbg!("click!"); + .on_click({ + let entry = entry.clone(); + let path = path.clone(); + move |_, _, cx| { + cx.dispatch_action(Jump { + path: path.clone(), + range: entry.range.clone(), + }); + } }) .aligned() .flex_float() diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e8e67642739224048ded305f5d970682b778c13a..e712b4a1fb83ac2ba565e1b21604d39ce10a8d2c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -299,7 +299,9 @@ impl Pane { ) -> Box { let existing_item = pane.update(cx, |pane, cx| { for (ix, item) in pane.items.iter().enumerate() { - if item.project_entry_ids(cx).as_slice() == &[project_entry_id] { + if item.project_path(cx).is_some() + && item.project_entry_ids(cx).as_slice() == &[project_entry_id] + { let item = item.boxed_clone(); pane.activate_item(ix, true, focus_item, cx); return Some(item); From 94fc28b29d3bfbc40c064c258e26ae6a39604b43 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 31 May 2022 19:00:44 +0200 Subject: [PATCH 04/12] WIP: start on tooltips --- crates/gpui/src/elements.rs | 3 +- crates/gpui/src/elements/tooltip.rs | 87 +++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 crates/gpui/src/elements/tooltip.rs diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 231339d9e07ed232bb61b1ac7c0ba38c2685c949..97e7e1bec221be4a6e4ebb0ac4f3d1570ff750e3 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -16,13 +16,14 @@ mod overlay; mod stack; mod svg; mod text; +mod tooltip; mod uniform_list; use self::expanded::Expanded; pub use self::{ align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*, hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, - stack::*, svg::*, text::*, uniform_list::*, + stack::*, svg::*, text::*, tooltip::*, uniform_list::*, }; pub use crate::presenter::ChildView; use crate::{ diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs new file mode 100644 index 0000000000000000000000000000000000000000..3ab433f161008b788a6d89e6faaace518949b654 --- /dev/null +++ b/crates/gpui/src/elements/tooltip.rs @@ -0,0 +1,87 @@ +use super::{ContainerStyle, Element, ElementBox}; +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + json::{json, ToJson}, + ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, View, +}; + +pub struct Tooltip { + state: ElementStateHandle, + child: ElementBox, + style: ContainerStyle, + text: String, +} + +#[derive(Default)] +struct TooltipState {} + +impl Tooltip { + pub fn new( + id: usize, + child: ElementBox, + text: String, + cx: &mut RenderContext, + ) -> Self { + Self { + state: cx.element_state::(id), + child, + text, + style: Default::default(), + } + } + + pub fn with_style(mut self, style: ContainerStyle) -> Self { + self.style = style; + self + } +} + +impl Element for Tooltip { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let size = self.child.layout(constraint, cx); + (size, ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + cx: &mut PaintContext, + ) { + self.child.paint(bounds.origin(), visible_bounds, cx); + } + + fn dispatch_event( + &mut self, + event: &crate::Event, + _: RectF, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + cx: &mut crate::EventContext, + ) -> bool { + self.child.dispatch_event(event, cx) + } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &crate::DebugContext, + ) -> serde_json::Value { + json!({ + "child": self.child.debug(cx), + "style": self.style.to_json(), + "text": &self.text, + }) + } +} From 982de971fa89c64202926e0746409f53029b2170 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 1 Jun 2022 09:55:25 +0200 Subject: [PATCH 05/12] Introduce a new `Tooltip` element and a `with_tooltip` helper --- crates/gpui/src/elements.rs | 15 +++- .../gpui/src/elements/mouse_event_handler.rs | 12 ++- crates/gpui/src/elements/tooltip.rs | 84 +++++++++++++++---- crates/gpui/src/presenter.rs | 22 +++-- crates/gpui/src/scene.rs | 2 +- 5 files changed, 108 insertions(+), 27 deletions(-) diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 97e7e1bec221be4a6e4ebb0ac4f3d1570ff750e3..6b0e2a1af639d1c33ed605db5fd5c0b5b39bac7f 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -31,7 +31,8 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, + json, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext, + SizeConstraint, View, }; use core::panic; use json::ToJson; @@ -155,6 +156,18 @@ pub trait Element { { FlexItem::new(self.boxed()).float() } + + fn with_tooltip( + self, + id: usize, + tooltip: ElementBox, + cx: &mut RenderContext, + ) -> Tooltip + where + Self: 'static + Sized, + { + Tooltip::new(id, self.boxed(), tooltip, cx) + } } pub enum Lifecycle { diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 2ad6eaf028fc9bb71426135a705b9cb4dbab69d0..1dea333400fd17d56b7e5957683b11702e7bb5e9 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -25,6 +25,7 @@ pub struct MouseEventHandler { mouse_down_out: Option>, right_mouse_down_out: Option>, drag: Option>, + hover: Option>, padding: Padding, } @@ -47,6 +48,7 @@ impl MouseEventHandler { mouse_down_out: None, right_mouse_down_out: None, drag: None, + hover: None, padding: Default::default(), } } @@ -109,6 +111,14 @@ impl MouseEventHandler { self } + pub fn on_hover( + mut self, + handler: impl Fn(Vector2F, bool, &mut EventContext) + 'static, + ) -> Self { + self.hover = Some(Rc::new(handler)); + self + } + pub fn with_padding(mut self, padding: Padding) -> Self { self.padding = padding; self @@ -153,7 +163,7 @@ impl Element for MouseEventHandler { view_id: cx.current_view_id(), discriminant: Some((self.tag, self.id)), bounds: self.hit_bounds(bounds), - hover: None, + hover: self.hover.clone(), click: self.click.clone(), mouse_down: self.mouse_down.clone(), right_click: self.right_click.clone(), diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 3ab433f161008b788a6d89e6faaace518949b654..b56f269667d61759460aee7fbca45b28b4bf6592 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -1,39 +1,78 @@ -use super::{ContainerStyle, Element, ElementBox}; +use std::{ + cell::{Cell, RefCell}, + rc::Rc, + time::Duration, +}; + +use super::{Element, ElementBox, MouseEventHandler}; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json::{json, ToJson}, - ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, View, + json::json, + ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, Task, View, }; +const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500); + pub struct Tooltip { - state: ElementStateHandle, child: ElementBox, - style: ContainerStyle, - text: String, + tooltip: Option, + state: ElementStateHandle>, } #[derive(Default)] -struct TooltipState {} +struct TooltipState { + visible: Cell, + position: Cell, + debounce: RefCell>>, +} impl Tooltip { pub fn new( id: usize, child: ElementBox, - text: String, + tooltip: ElementBox, cx: &mut RenderContext, ) -> Self { + let state_handle = cx.element_state::>(id); + let state = state_handle.read(cx).clone(); + let tooltip = if state.visible.get() { + Some(tooltip) + } else { + None + }; + let child = MouseEventHandler::new::(id, cx, |_, _| child) + .on_hover(move |position, hover, cx| { + let window_id = cx.window_id(); + if let Some(view_id) = cx.view_id() { + if hover { + if !state.visible.get() { + state.position.set(position); + + let mut debounce = state.debounce.borrow_mut(); + if debounce.is_none() { + *debounce = Some(cx.spawn({ + let state = state.clone(); + |mut cx| async move { + cx.background().timer(DEBOUNCE_TIMEOUT).await; + state.visible.set(true); + cx.update(|cx| cx.notify_view(window_id, view_id)); + } + })); + } + } + } else { + state.visible.set(false); + state.debounce.take(); + } + } + }) + .boxed(); Self { - state: cx.element_state::(id), child, - text, - style: Default::default(), + tooltip, + state: state_handle, } } - - pub fn with_style(mut self, style: ContainerStyle) -> Self { - self.style = style; - self - } } impl Element for Tooltip { @@ -46,6 +85,9 @@ impl Element for Tooltip { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { let size = self.child.layout(constraint, cx); + if let Some(tooltip) = self.tooltip.as_mut() { + tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx); + } (size, ()) } @@ -57,6 +99,13 @@ impl Element for Tooltip { cx: &mut PaintContext, ) { self.child.paint(bounds.origin(), visible_bounds, cx); + if let Some(tooltip) = self.tooltip.as_mut() { + let origin = self.state.read(cx).position.get(); + let size = tooltip.size(); + cx.scene.push_stacking_context(None); + tooltip.paint(origin, RectF::new(origin, size), cx); + cx.scene.pop_stacking_context(); + } } fn dispatch_event( @@ -80,8 +129,7 @@ impl Element for Tooltip { ) -> serde_json::Value { json!({ "child": self.child.debug(cx), - "style": self.style.to_json(), - "text": &self.text, + "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)), }) } } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 87efeb2e5ffdbf598c75ce43c7342bf05c961eef..6e29242b842848c196bd1949e462ccca1e07c592 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -311,7 +311,7 @@ impl Presenter { if let Some(region_id) = region.id() { if !self.hovered_region_ids.contains(®ion_id) { invalidated_views.push(region.view_id); - hovered_regions.push(region.clone()); + hovered_regions.push((region.clone(), position)); self.hovered_region_ids.insert(region_id); } } @@ -319,7 +319,7 @@ impl Presenter { if let Some(region_id) = region.id() { if self.hovered_region_ids.contains(®ion_id) { invalidated_views.push(region.view_id); - unhovered_regions.push(region.clone()); + unhovered_regions.push((region.clone(), position)); self.hovered_region_ids.remove(®ion_id); } } @@ -348,20 +348,20 @@ impl Presenter { let mut event_cx = self.build_event_context(cx); let mut handled = false; - for unhovered_region in unhovered_regions { + for (unhovered_region, position) in unhovered_regions { handled = true; if let Some(hover_callback) = unhovered_region.hover { event_cx.with_current_view(unhovered_region.view_id, |event_cx| { - hover_callback(false, event_cx); + hover_callback(position, false, event_cx); }) } } - for hovered_region in hovered_regions { + for (hovered_region, position) in hovered_regions { handled = true; if let Some(hover_callback) = hovered_region.hover { event_cx.with_current_view(hovered_region.view_id, |event_cx| { - hover_callback(true, event_cx); + hover_callback(position, true, event_cx); }) } } @@ -449,6 +449,7 @@ impl Presenter { view_stack: Default::default(), invalidated_views: Default::default(), notify_count: 0, + window_id: self.window_id, app: cx, } } @@ -626,6 +627,7 @@ pub struct EventContext<'a> { pub font_cache: &'a FontCache, pub text_layout_cache: &'a TextLayoutCache, pub app: &'a mut MutableAppContext, + pub window_id: usize, pub notify_count: usize, view_stack: Vec, invalidated_views: HashSet, @@ -653,6 +655,14 @@ impl<'a> EventContext<'a> { result } + pub fn window_id(&self) -> usize { + self.window_id + } + + pub fn view_id(&self) -> Option { + self.view_stack.last().copied() + } + pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { dispatcher_view_id: self.view_stack.last().copied(), diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 1f503c8bf7750cc908a52ce04f926e55e130a3d6..ffe0fc76d1498e006e850046c770e46fca0035b0 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -49,7 +49,7 @@ pub struct MouseRegion { pub view_id: usize, pub discriminant: Option<(TypeId, usize)>, pub bounds: RectF, - pub hover: Option>, + pub hover: Option>, pub mouse_down: Option>, pub click: Option>, pub right_mouse_down: Option>, From b3242417b3d8545d82d8eed6f2ca360a55de5ccf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 1 Jun 2022 09:55:45 +0200 Subject: [PATCH 06/12] Show tooltip when hovering over jump to diagnostic icon --- crates/diagnostics/src/diagnostics.rs | 25 +++++++++++++++++++++++++ crates/theme/src/theme.rs | 1 + styles/src/styleTree/app.ts | 2 ++ styles/src/styleTree/tooltip.ts | 13 +++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 styles/src/styleTree/tooltip.ts diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 89ff46bf6420f5f2b6d0ad1bf6943eb17c9ec65a..a043e4eae3296df2b6f26d86d799b58df671a86b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -613,6 +613,7 @@ fn diagnostic_header_renderer(entry: DiagnosticEntry, path: ProjectPath) let (message, highlights) = highlight_diagnostic_message(&entry.diagnostic.message); Arc::new(move |cx| { let settings = cx.global::(); + let tooltip_style = settings.theme.tooltip.clone(); let theme = &settings.theme.editor; let style = theme.diagnostic_header.clone(); let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); @@ -683,6 +684,30 @@ fn diagnostic_header_renderer(entry: DiagnosticEntry, path: ProjectPath) }); } }) + .with_tooltip( + entry.diagnostic.group_id, + Flex::row() + .with_child( + Label::new( + "Jump to diagnostic (".to_string(), + tooltip_style.text.clone(), + ) + .boxed(), + ) + .with_child( + KeystrokeLabel::new( + Box::new(editor::OpenExcerpts), + Default::default(), + tooltip_style.text.clone(), + ) + .boxed(), + ) + .with_child(Label::new(")".to_string(), tooltip_style.text).boxed()) + .contained() + .with_style(tooltip_style.container) + .boxed(), + cx, + ) .aligned() .flex_float() .boxed(), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d5f50b08e24a2072121c969d7bd8106748a1a110..9d3bb59bce2ebf4f2434f5b85b170b88543e0277 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -31,6 +31,7 @@ pub struct Theme { pub project_diagnostics: ProjectDiagnostics, pub breadcrumbs: ContainedText, pub contact_notification: ContactNotification, + pub tooltip: ContainedText, } #[derive(Deserialize, Default)] diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 41266ff5f7d775551ce7cec8d7cff91e604d8658..083863f6da942b353c87a79f0882054d5b6255a5 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -12,6 +12,7 @@ import workspace from "./workspace"; import contextMenu from "./contextMenu"; import projectDiagnostics from "./projectDiagnostics"; import contactNotification from "./contactNotification"; +import tooltip from "./tooltip"; export const panel = { padding: { top: 12, bottom: 12 }, @@ -37,5 +38,6 @@ export default function app(theme: Theme): Object { }, }, contactNotification: contactNotification(theme), + tooltip: tooltip(theme), }; } diff --git a/styles/src/styleTree/tooltip.ts b/styles/src/styleTree/tooltip.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6bd85053bd3d1beef4787671a99714f39b7ac0f --- /dev/null +++ b/styles/src/styleTree/tooltip.ts @@ -0,0 +1,13 @@ +import Theme from "../themes/common/theme"; +import { backgroundColor, border, shadow, text } from "./components"; + +export default function tooltip(theme: Theme) { + return { + background: backgroundColor(theme, 500), + border: border(theme, "primary"), + padding: 6, + shadow: shadow(theme), + cornerRadius: 6, + ...text(theme, "sans", "primary", { size: "xs" }) + } +} \ No newline at end of file From 238827642a2dc1aee1872de2c12211bb872ab85c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 1 Jun 2022 10:03:46 +0200 Subject: [PATCH 07/12] Align tooltip based on the available window space --- crates/editor/src/element.rs | 2 +- crates/gpui/src/elements/tooltip.rs | 11 +++++++++-- crates/gpui/src/presenter.rs | 5 ++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 39091ecad1f557190f3bdeceae5dd96333e75a97..771bc1049c5131db9d9e7c59386693685f46f864 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1619,7 +1619,7 @@ mod tests { // Don't panic. let bounds = RectF::new(Default::default(), size); - let mut paint_cx = presenter.build_paint_context(&mut scene, cx); + let mut paint_cx = presenter.build_paint_context(&mut scene, bounds.size(), cx); element.paint(bounds, bounds, &mut state, &mut paint_cx); } } diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index b56f269667d61759460aee7fbca45b28b4bf6592..4c255e139ed7ab4a84d4e5ec2f8d1d8b885020ef 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -101,9 +101,16 @@ impl Element for Tooltip { self.child.paint(bounds.origin(), visible_bounds, cx); if let Some(tooltip) = self.tooltip.as_mut() { let origin = self.state.read(cx).position.get(); - let size = tooltip.size(); + let mut bounds = RectF::new(origin, tooltip.size()); + if bounds.lower_right().x() > cx.window_size.x() { + bounds.set_origin_x(bounds.origin_x() - bounds.width()); + } + if bounds.lower_right().y() > cx.window_size.y() { + bounds.set_origin_y(bounds.origin_y() - bounds.height()); + } + cx.scene.push_stacking_context(None); - tooltip.paint(origin, RectF::new(origin, size), cx); + tooltip.paint(bounds.origin(), bounds, cx); cx.scene.pop_stacking_context(); } } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 6e29242b842848c196bd1949e462ccca1e07c592..720e9e90b5ad314af5f7a9b8eace771538ec6213 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -148,7 +148,7 @@ impl Presenter { if let Some(root_view_id) = cx.root_view_id(self.window_id) { self.layout(window_size, refreshing, cx); - let mut paint_cx = self.build_paint_context(&mut scene, cx); + let mut paint_cx = self.build_paint_context(&mut scene, window_size, cx); paint_cx.paint( root_view_id, Vector2F::zero(), @@ -205,10 +205,12 @@ impl Presenter { pub fn build_paint_context<'a>( &'a mut self, scene: &'a mut Scene, + window_size: Vector2F, cx: &'a mut MutableAppContext, ) -> PaintContext { PaintContext { scene, + window_size, font_cache: &self.font_cache, text_layout_cache: &self.text_layout_cache, rendered_views: &mut self.rendered_views, @@ -592,6 +594,7 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> { pub struct PaintContext<'a> { rendered_views: &'a mut HashMap, view_stack: Vec, + pub window_size: Vector2F, pub scene: &'a mut Scene, pub font_cache: &'a FontCache, pub text_layout_cache: &'a TextLayoutCache, From b63d965b4628c50d9d3f0a5b6d6bb493f3cb71d0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 1 Jun 2022 10:08:25 +0200 Subject: [PATCH 08/12] Space out tooltip a little bit to ensure it doesn't overlap cursor --- styles/src/styleTree/tooltip.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/src/styleTree/tooltip.ts b/styles/src/styleTree/tooltip.ts index a6bd85053bd3d1beef4787671a99714f39b7ac0f..3fc7bd012b4b0725e55d2cb18f0e0b10dab62a3b 100644 --- a/styles/src/styleTree/tooltip.ts +++ b/styles/src/styleTree/tooltip.ts @@ -6,6 +6,7 @@ export default function tooltip(theme: Theme) { background: backgroundColor(theme, 500), border: border(theme, "primary"), padding: 6, + margin: { top: 8, left: 8 }, shadow: shadow(theme), cornerRadius: 6, ...text(theme, "sans", "primary", { size: "xs" }) From 0e1307fb23517ddbd658d0753d1cb3031677abca Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 1 Jun 2022 10:18:10 +0200 Subject: [PATCH 09/12] :memo: --- crates/gpui/src/elements/tooltip.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 4c255e139ed7ab4a84d4e5ec2f8d1d8b885020ef..9fc5894c63e66514e5843359299594a453eff0dd 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -102,9 +102,13 @@ impl Element for Tooltip { if let Some(tooltip) = self.tooltip.as_mut() { let origin = self.state.read(cx).position.get(); let mut bounds = RectF::new(origin, tooltip.size()); + + // Align tooltip to the left if its bounds overflow the window width. if bounds.lower_right().x() > cx.window_size.x() { bounds.set_origin_x(bounds.origin_x() - bounds.width()); } + + // Align tooltip to the top if its bounds overflow the window height. if bounds.lower_right().y() > cx.window_size.y() { bounds.set_origin_y(bounds.origin_y() - bounds.height()); } From 9ca9f63046fe342602d8b66f104e983c1ba5a810 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 2 Jun 2022 08:36:42 +0200 Subject: [PATCH 10/12] Match figma styling for tooltips --- styles/src/styleTree/tooltip.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/styles/src/styleTree/tooltip.ts b/styles/src/styleTree/tooltip.ts index 3fc7bd012b4b0725e55d2cb18f0e0b10dab62a3b..228d510ddd20166a0aa49f6e0dec97c2a5cf2345 100644 --- a/styles/src/styleTree/tooltip.ts +++ b/styles/src/styleTree/tooltip.ts @@ -4,11 +4,11 @@ import { backgroundColor, border, shadow, text } from "./components"; export default function tooltip(theme: Theme) { return { background: backgroundColor(theme, 500), - border: border(theme, "primary"), - padding: 6, - margin: { top: 8, left: 8 }, + border: border(theme, "secondary"), + padding: { top: 4, bottom: 4, left: 8, right: 8 }, + margin: { top: 6, left: 6 }, shadow: shadow(theme), cornerRadius: 6, - ...text(theme, "sans", "primary", { size: "xs" }) + ...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }) } } \ No newline at end of file From cc028cca784b6b6fc82685c7559adce4dfd9a1c6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 2 Jun 2022 09:12:50 +0200 Subject: [PATCH 11/12] Simplify usage of tooltip Now you simply specify a text, an action and a style and GPUI will take of rendering it properly. This is simpler compared to always providing a custom element and should make tooltip more consistent across the UI. --- crates/diagnostics/src/diagnostics.rs | 23 +------ crates/gpui/src/elements.rs | 8 ++- crates/gpui/src/elements/tooltip.rs | 89 ++++++++++++++++++++++++--- crates/theme/src/theme.rs | 4 +- styles/src/styleTree/tooltip.ts | 10 ++- 5 files changed, 99 insertions(+), 35 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index a043e4eae3296df2b6f26d86d799b58df671a86b..501ac0fca932f101f3179c173eb231068e41b647 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -686,26 +686,9 @@ fn diagnostic_header_renderer(entry: DiagnosticEntry, path: ProjectPath) }) .with_tooltip( entry.diagnostic.group_id, - Flex::row() - .with_child( - Label::new( - "Jump to diagnostic (".to_string(), - tooltip_style.text.clone(), - ) - .boxed(), - ) - .with_child( - KeystrokeLabel::new( - Box::new(editor::OpenExcerpts), - Default::default(), - tooltip_style.text.clone(), - ) - .boxed(), - ) - .with_child(Label::new(")".to_string(), tooltip_style.text).boxed()) - .contained() - .with_style(tooltip_style.container) - .boxed(), + "Jump to diagnostic".to_string(), + Some(Box::new(editor::OpenExcerpts)), + tooltip_style, cx, ) .aligned() diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 6b0e2a1af639d1c33ed605db5fd5c0b5b39bac7f..d2d254d93eb1d7da3949d60bbfdc011b7fa65293 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -31,7 +31,7 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - json, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext, + json, Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext, SizeConstraint, View, }; use core::panic; @@ -160,13 +160,15 @@ pub trait Element { fn with_tooltip( self, id: usize, - tooltip: ElementBox, + text: String, + action: Option>, + style: TooltipStyle, cx: &mut RenderContext, ) -> Tooltip where Self: 'static + Sized, { - Tooltip::new(id, self.boxed(), tooltip, cx) + Tooltip::new(id, text, action, style, self.boxed(), cx) } } diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 9fc5894c63e66514e5843359299594a453eff0dd..9ed0f5cba6ac86f56c2ccee040608a461688fbec 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -1,14 +1,19 @@ -use std::{ - cell::{Cell, RefCell}, - rc::Rc, - time::Duration, +use super::{ + ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, ParentElement, + Text, }; - -use super::{Element, ElementBox, MouseEventHandler}; use crate::{ + fonts::TextStyle, geometry::{rect::RectF, vector::Vector2F}, json::json, - ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, Task, View, + Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, + Task, View, +}; +use serde::Deserialize; +use std::{ + cell::{Cell, RefCell}, + rc::Rc, + time::Duration, }; const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500); @@ -26,17 +31,53 @@ struct TooltipState { debounce: RefCell>>, } +#[derive(Clone, Deserialize, Default)] +pub struct TooltipStyle { + #[serde(flatten)] + container: ContainerStyle, + text: TextStyle, + keystroke: KeystrokeStyle, + max_text_width: f32, +} + +#[derive(Clone, Deserialize, Default)] +pub struct KeystrokeStyle { + #[serde(flatten)] + container: ContainerStyle, + #[serde(flatten)] + text: TextStyle, +} + impl Tooltip { pub fn new( id: usize, + text: String, + action: Option>, + style: TooltipStyle, child: ElementBox, - tooltip: ElementBox, cx: &mut RenderContext, ) -> Self { let state_handle = cx.element_state::>(id); let state = state_handle.read(cx).clone(); let tooltip = if state.visible.get() { - Some(tooltip) + let mut collapsed_tooltip = Self::render_tooltip( + text.clone(), + style.clone(), + action.as_ref().map(|a| a.boxed_clone()), + true, + ) + .boxed(); + Some( + Self::render_tooltip(text, style, action, false) + .constrained() + .dynamically(move |constraint, cx| { + SizeConstraint::strict_along( + Axis::Vertical, + collapsed_tooltip.layout(constraint, cx).y(), + ) + }) + .boxed(), + ) } else { None }; @@ -73,6 +114,36 @@ impl Tooltip { state: state_handle, } } + + fn render_tooltip( + text: String, + style: TooltipStyle, + action: Option>, + measure: bool, + ) -> impl Element { + Flex::row() + .with_child({ + let text = Text::new(text, style.text) + .constrained() + .with_max_width(style.max_text_width); + if measure { + text.flex(1., false).boxed() + } else { + text.flex(1., false).aligned().boxed() + } + }) + .with_children(action.map(|action| { + let keystroke_label = + KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text); + if measure { + keystroke_label.boxed() + } else { + keystroke_label.aligned().boxed() + } + })) + .contained() + .with_style(style.container) + } } impl Element for Tooltip { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9d3bb59bce2ebf4f2434f5b85b170b88543e0277..363912763347d6299b0d992d6643189ef25c021c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -2,7 +2,7 @@ mod theme_registry; use gpui::{ color::Color, - elements::{ContainerStyle, ImageStyle, LabelStyle}, + elements::{ContainerStyle, ImageStyle, LabelStyle, TooltipStyle}, fonts::{HighlightStyle, TextStyle}, Border, MouseState, }; @@ -31,7 +31,7 @@ pub struct Theme { pub project_diagnostics: ProjectDiagnostics, pub breadcrumbs: ContainedText, pub contact_notification: ContactNotification, - pub tooltip: ContainedText, + pub tooltip: TooltipStyle, } #[derive(Deserialize, Default)] diff --git a/styles/src/styleTree/tooltip.ts b/styles/src/styleTree/tooltip.ts index 228d510ddd20166a0aa49f6e0dec97c2a5cf2345..bfceae168eaeb3e09997f4a6e6d064422dc04b21 100644 --- a/styles/src/styleTree/tooltip.ts +++ b/styles/src/styleTree/tooltip.ts @@ -9,6 +9,14 @@ export default function tooltip(theme: Theme) { margin: { top: 6, left: 6 }, shadow: shadow(theme), cornerRadius: 6, - ...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }) + text: text(theme, "sans", "secondary", { size: "xs", weight: "bold" }), + keystroke: { + background: backgroundColor(theme, "on500"), + cornerRadius: 4, + margin: { left: 6 }, + padding: { left: 3, right: 3 }, + ...text(theme, "mono", "muted", { size: "xs", weight: "bold" }) + }, + maxTextWidth: 200, } } \ No newline at end of file From 6979e67bed22c4358f87ce1a8f9e194254a73d06 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 2 Jun 2022 09:30:07 +0200 Subject: [PATCH 12/12] Use anchors to jump to diagnostic whenever possible --- crates/diagnostics/src/diagnostics.rs | 87 +++++++++++++++------------ 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 501ac0fca932f101f3179c173eb231068e41b647..f39eab68adccb650fcf2d7aba173bc3e2250af6f 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -13,7 +13,8 @@ use gpui::{ Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{ - Bias, Buffer, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal, + Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, + SelectionGoal, ToPoint, }; use project::{DiagnosticSummary, Project, ProjectPath}; use serde_json::json; @@ -62,7 +63,8 @@ struct PathState { #[derive(Clone, Debug)] struct Jump { path: ProjectPath, - range: Range, + position: Point, + anchor: Anchor, } struct DiagnosticGroupState { @@ -188,12 +190,18 @@ impl ProjectDiagnosticsEditor { fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext) { let editor = workspace.open_path(action.path.clone(), true, cx); - let range = action.range.clone(); + let position = action.position; + let anchor = action.anchor; cx.spawn_weak(|_, mut cx| async move { let editor = editor.await.log_err()?.downcast::()?; editor.update(&mut cx, |editor, cx| { let buffer = editor.buffer().read(cx).as_singleton()?; - let cursor = buffer.read(cx).clip_point(range.start, Bias::Left); + let buffer = buffer.read(cx); + let cursor = if buffer.can_resolve(&anchor) { + anchor.to_point(buffer) + } else { + buffer.clip_point(position, Bias::Left) + }; editor.change_selections(Some(Autoscroll::Newest), cx, |s| { s.select_ranges([cursor..cursor]); }); @@ -338,19 +346,21 @@ impl ProjectDiagnosticsEditor { if is_first_excerpt_for_group { is_first_excerpt_for_group = false; let mut primary = - group.entries[group.primary_ix].resolve::(&snapshot); - primary.diagnostic.message = primary - .diagnostic - .message - .split('\n') - .next() - .unwrap() - .to_string(); + group.entries[group.primary_ix].diagnostic.clone(); + let anchor = group.entries[group.primary_ix].range.start; + let position = anchor.to_point(&snapshot); + primary.message = + primary.message.split('\n').next().unwrap().to_string(); group_state.block_count += 1; blocks_to_add.push(BlockProperties { position: header_position, height: 2, - render: diagnostic_header_renderer(primary, path.clone()), + render: diagnostic_header_renderer( + primary, + path.clone(), + position, + anchor, + ), disposition: BlockDisposition::Above, }); } @@ -607,10 +617,15 @@ impl workspace::Item for ProjectDiagnosticsEditor { } } -fn diagnostic_header_renderer(entry: DiagnosticEntry, path: ProjectPath) -> RenderBlock { +fn diagnostic_header_renderer( + diagnostic: Diagnostic, + path: ProjectPath, + position: Point, + anchor: Anchor, +) -> RenderBlock { enum JumpIcon {} - let (message, highlights) = highlight_diagnostic_message(&entry.diagnostic.message); + let (message, highlights) = highlight_diagnostic_message(&diagnostic.message); Arc::new(move |cx| { let settings = cx.global::(); let tooltip_style = settings.theme.tooltip.clone(); @@ -618,7 +633,7 @@ fn diagnostic_header_renderer(entry: DiagnosticEntry, path: ProjectPath) let style = theme.diagnostic_header.clone(); let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); let icon_width = cx.em_width * style.icon_width_factor; - let icon = if entry.diagnostic.severity == DiagnosticSeverity::ERROR { + let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { Svg::new("icons/diagnostic-error-10.svg") .with_color(theme.error_diagnostic.message.text.color) } else { @@ -647,7 +662,7 @@ fn diagnostic_header_renderer(entry: DiagnosticEntry, path: ProjectPath) .aligned() .boxed(), ) - .with_children(entry.diagnostic.code.clone().map(|code| { + .with_children(diagnostic.code.clone().map(|code| { Label::new(code, style.code.text.clone().with_font_size(font_size)) .contained() .with_style(style.code.container) @@ -655,37 +670,33 @@ fn diagnostic_header_renderer(entry: DiagnosticEntry, path: ProjectPath) .boxed() })) .with_child( - MouseEventHandler::new::( - entry.diagnostic.group_id, - 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() - }, - ) + MouseEventHandler::new::(diagnostic.group_id, 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({ - let entry = entry.clone(); let path = path.clone(); move |_, _, cx| { cx.dispatch_action(Jump { path: path.clone(), - range: entry.range.clone(), + position, + anchor, }); } }) .with_tooltip( - entry.diagnostic.group_id, + diagnostic.group_id, "Jump to diagnostic".to_string(), Some(Box::new(editor::OpenExcerpts)), tooltip_style,