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
3
assets/icons/jump.svg
Normal file
3
assets/icons/jump.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 1.02501V6.27499C7.5 6.56484 7.26484 6.79999 6.975 6.79999C6.68516 6.79999 6.45 6.56484 6.45 6.27499V2.29157L1.3969 7.34468C1.29365 7.44968 1.15934 7.49999 1.02503 7.49999C0.890713 7.49999 0.756401 7.44871 0.653808 7.34619C0.448731 7.14111 0.448731 6.80894 0.653808 6.60375L5.70844 1.55001H1.72502C1.43518 1.55001 1.20002 1.31595 1.20002 1.02501C1.20002 0.734077 1.43518 0.500015 1.72502 0.500015H6.975C7.26594 0.500015 7.5 0.736264 7.5 1.02501Z" fill="white" fill-opacity="0.6"/>
|
||||
</svg>
|
After Width: | Height: | Size: 593 B |
|
@ -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)
|
||||
.with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
|
||||
.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(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,13 +1198,20 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
|
||||
editor
|
||||
.blocks_in_range(0..editor.max_point().row())
|
||||
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(&BlockContext {
|
||||
.render(&mut BlockContext {
|
||||
cx,
|
||||
anchor_x: 0.,
|
||||
scroll_x: 0.,
|
||||
|
@ -1146,5 +1236,6 @@ mod tests {
|
|||
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,7 +780,8 @@ impl EditorElement {
|
|||
.x_for_index(align_to.column() as usize)
|
||||
};
|
||||
|
||||
block.render(&BlockContext {
|
||||
cx.render(&editor, |_, cx| {
|
||||
block.render(&mut BlockContext {
|
||||
cx,
|
||||
anchor_x,
|
||||
gutter_padding,
|
||||
|
@ -783,6 +790,7 @@ impl EditorElement {
|
|||
gutter_width,
|
||||
em_width,
|
||||
})
|
||||
})
|
||||
}
|
||||
TransformBlock::ExcerptHeader {
|
||||
buffer,
|
||||
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
22
styles/src/styleTree/tooltip.ts
Normal file
22
styles/src/styleTree/tooltip.ts
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue