Merge pull request #1089 from zed-industries/jump-to-diagnostic
Jump to diagnostic
This commit is contained in:
commit
f1964cf2a0
15 changed files with 491 additions and 106 deletions
|
@ -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<DiagnosticGroupState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Jump {
|
||||
path: ProjectPath,
|
||||
position: Point,
|
||||
anchor: Anchor,
|
||||
}
|
||||
|
||||
struct DiagnosticGroupState {
|
||||
primary_diagnostic: DiagnosticEntry<language::Anchor>,
|
||||
primary_excerpt_ix: usize,
|
||||
|
@ -177,6 +188,30 @@ impl ProjectDiagnosticsEditor {
|
|||
}
|
||||
}
|
||||
|
||||
fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
|
||||
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>()?;
|
||||
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<Self>) {
|
||||
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::<Settings>();
|
||||
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::<JumpIcon, _, _>(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<Editor>,
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<dyn Fn(&BlockContext) -> ElementBox>;
|
||||
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> ElementBox>;
|
||||
|
||||
pub struct Block {
|
||||
id: BlockId,
|
||||
|
@ -67,12 +67,12 @@ where
|
|||
{
|
||||
pub position: P,
|
||||
pub height: u8,
|
||||
pub render: Arc<dyn Fn(&BlockContext) -> ElementBox>,
|
||||
pub render: Arc<dyn Fn(&mut BlockContext) -> 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::<Vec<_>>();
|
||||
|
||||
|
@ -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]),
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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::<Settings>();
|
||||
let theme = &settings.theme.editor;
|
||||
let style = diagnostic_style(diagnostic.severity, is_valid, theme);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T: View>(
|
||||
self,
|
||||
id: usize,
|
||||
text: String,
|
||||
action: Option<Box<dyn Action>>,
|
||||
style: TooltipStyle,
|
||||
cx: &mut RenderContext<T>,
|
||||
) -> Tooltip
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Tooltip::new(id, text, action, style, self.boxed(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Lifecycle<T: Element> {
|
||||
|
|
|
@ -25,6 +25,7 @@ pub struct MouseEventHandler {
|
|||
mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
drag: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
|
||||
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(),
|
||||
|
|
217
crates/gpui/src/elements/tooltip.rs
Normal file
217
crates/gpui/src/elements/tooltip.rs
Normal file
|
@ -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<ElementBox>,
|
||||
state: ElementStateHandle<Rc<TooltipState>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TooltipState {
|
||||
visible: Cell<bool>,
|
||||
position: Cell<Vector2F>,
|
||||
debounce: RefCell<Option<Task<()>>>,
|
||||
}
|
||||
|
||||
#[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<T: View>(
|
||||
id: usize,
|
||||
text: String,
|
||||
action: Option<Box<dyn Action>>,
|
||||
style: TooltipStyle,
|
||||
child: ElementBox,
|
||||
cx: &mut RenderContext<T>,
|
||||
) -> Self {
|
||||
let state_handle = cx.element_state::<TooltipState, Rc<TooltipState>>(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::<Self, _, _>(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<Box<dyn Action>>,
|
||||
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)),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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<usize, ElementBox>,
|
||||
view_stack: Vec<usize>,
|
||||
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<usize>,
|
||||
invalidated_views: HashSet<usize>,
|
||||
|
@ -653,6 +658,14 @@ impl<'a> EventContext<'a> {
|
|||
result
|
||||
}
|
||||
|
||||
pub fn window_id(&self) -> usize {
|
||||
self.window_id
|
||||
}
|
||||
|
||||
pub fn view_id(&self) -> Option<usize> {
|
||||
self.view_stack.last().copied()
|
||||
}
|
||||
|
||||
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
|
||||
self.dispatched_actions.push(DispatchDirective {
|
||||
dispatcher_view_id: self.view_stack.last().copied(),
|
||||
|
|
|
@ -49,7 +49,7 @@ pub struct MouseRegion {
|
|||
pub view_id: usize,
|
||||
pub discriminant: Option<(TypeId, usize)>,
|
||||
pub bounds: RectF,
|
||||
pub hover: Option<Rc<dyn Fn(bool, &mut EventContext)>>,
|
||||
pub hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
|
||||
pub mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
pub click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||
pub right_mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
|
|
|
@ -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<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
|
|
|
@ -299,7 +299,9 @@ impl Pane {
|
|||
) -> Box<dyn ItemHandle> {
|
||||
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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue