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 b5361b4e5b0d7595bde196064c3e18bceca0500c..f39eab68adccb650fcf2d7aba173bc3e2250af6f 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -8,12 +8,13 @@ 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, 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, + Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, + SelectionGoal, ToPoint, }; use project::{DiagnosticSummary, Project, ProjectPath}; use serde_json::json; @@ -27,15 +28,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 +60,13 @@ struct PathState { diagnostic_groups: Vec, } +#[derive(Clone, Debug)] +struct Jump { + path: ProjectPath, + position: Point, + anchor: Anchor, +} + struct DiagnosticGroupState { primary_diagnostic: DiagnosticEntry, primary_excerpt_ix: usize, @@ -177,6 +188,30 @@ impl ProjectDiagnosticsEditor { } } + fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext) { + let editor = workspace.open_path(action.path.clone(), true, cx); + 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 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]); + }); + 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(); @@ -312,13 +347,20 @@ impl ProjectDiagnosticsEditor { is_first_excerpt_for_group = false; let mut primary = 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), + render: diagnostic_header_renderer( + primary, + path.clone(), + position, + anchor, + ), disposition: BlockDisposition::Above, }); } @@ -575,12 +617,20 @@ impl workspace::Item for ProjectDiagnosticsEditor { } } -fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { +fn diagnostic_header_renderer( + diagnostic: Diagnostic, + path: ProjectPath, + position: Point, + anchor: Anchor, +) -> RenderBlock { + enum JumpIcon {} + let (message, highlights) = highlight_diagnostic_message(&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; + 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 +641,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 +669,47 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { .aligned() .boxed() })) + .with_child( + 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 path = path.clone(); + move |_, _, cx| { + cx.dispatch_action(Jump { + path: path.clone(), + position, + anchor, + }); + } + }) + .with_tooltip( + diagnostic.group_id, + "Jump to diagnostic".to_string(), + Some(Box::new(editor::OpenExcerpts)), + tooltip_style, + cx, + ) + .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") }) @@ -702,7 +791,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 +924,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 +935,7 @@ mod tests { ] ); assert_eq!( - editor.text(), + view.editor.update(cx, |editor, cx| editor.display_text(cx)), concat!( // // main.rs @@ -923,10 +1010,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 +1023,7 @@ mod tests { ] ); assert_eq!( - editor.text(), + view.editor.update(cx, |editor, cx| editor.display_text(cx)), concat!( // // consts.rs @@ -1038,10 +1123,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 +1138,7 @@ mod tests { ] ); assert_eq!( - editor.text(), + view.editor.update(cx, |editor, cx| editor.display_text(cx)), concat!( // // consts.rs @@ -1115,36 +1198,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..771bc1049c5131db9d9e7c59386693685f46f864 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 { @@ -1611,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.rs b/crates/gpui/src/elements.rs index 231339d9e07ed232bb61b1ac7c0ba38c2685c949..d2d254d93eb1d7da3949d60bbfdc011b7fa65293 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::{ @@ -30,7 +31,8 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, + json, Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext, + SizeConstraint, View, }; use core::panic; use json::ToJson; @@ -154,6 +156,20 @@ pub trait Element { { FlexItem::new(self.boxed()).float() } + + fn with_tooltip( + self, + id: usize, + text: String, + action: Option>, + style: TooltipStyle, + cx: &mut RenderContext, + ) -> Tooltip + where + Self: 'static + Sized, + { + Tooltip::new(id, text, action, style, self.boxed(), 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 new file mode 100644 index 0000000000000000000000000000000000000000..9ed0f5cba6ac86f56c2ccee040608a461688fbec --- /dev/null +++ b/crates/gpui/src/elements/tooltip.rs @@ -0,0 +1,217 @@ +use super::{ + ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, ParentElement, + Text, +}; +use crate::{ + fonts::TextStyle, + geometry::{rect::RectF, vector::Vector2F}, + json::json, + 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); + +pub struct Tooltip { + child: ElementBox, + tooltip: Option, + state: ElementStateHandle>, +} + +#[derive(Default)] +struct TooltipState { + visible: Cell, + position: Cell, + 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, + cx: &mut RenderContext, + ) -> Self { + let state_handle = cx.element_state::>(id); + let state = state_handle.read(cx).clone(); + let tooltip = if state.visible.get() { + 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 + }; + 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 { + child, + 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 { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + 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, ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + 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 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()); + } + + cx.scene.push_stacking_context(None); + tooltip.paint(bounds.origin(), bounds, cx); + cx.scene.pop_stacking_context(); + } + } + + 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), + "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..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, @@ -311,7 +313,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 +321,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 +350,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 +451,7 @@ impl Presenter { view_stack: Default::default(), invalidated_views: Default::default(), notify_count: 0, + window_id: self.window_id, app: cx, } } @@ -591,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, @@ -626,6 +630,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 +658,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>, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a8c28f41f37f82f6aa63f13bc694ebe522e085e3..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,6 +31,7 @@ pub struct Theme { pub project_diagnostics: ProjectDiagnostics, pub breadcrumbs: ContainedText, pub contact_notification: ContactNotification, + pub tooltip: TooltipStyle, } #[derive(Deserialize, Default)] @@ -317,7 +318,7 @@ pub struct Icon { pub path: String, } -#[derive(Deserialize, Default)] +#[derive(Clone, Deserialize, Default)] pub struct IconButton { #[serde(flatten)] pub container: ContainerStyle, @@ -461,6 +462,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/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); 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/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, diff --git a/styles/src/styleTree/tooltip.ts b/styles/src/styleTree/tooltip.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfceae168eaeb3e09997f4a6e6d064422dc04b21 --- /dev/null +++ b/styles/src/styleTree/tooltip.ts @@ -0,0 +1,22 @@ +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, "secondary"), + padding: { top: 4, bottom: 4, left: 8, right: 8 }, + margin: { top: 6, left: 6 }, + shadow: shadow(theme), + cornerRadius: 6, + 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