
See https://zed.dev/channel/gpui-536 Fixes https://github.com/zed-industries/zed/issues/9010 Fixes https://github.com/zed-industries/zed/issues/8883 Fixes https://github.com/zed-industries/zed/issues/8640 Fixes https://github.com/zed-industries/zed/issues/8598 Fixes https://github.com/zed-industries/zed/issues/8579 Fixes https://github.com/zed-industries/zed/issues/8363 Fixes https://github.com/zed-industries/zed/issues/8207 ### Problem After transitioning Zed to GPUI 2, we started noticing that interacting with the mouse on many UI elements would lead to a pretty annoying flicker. The main issue with the old approach was that hover state was calculated based on the previous frame. That is, when computing whether a given element was hovered in the current frame, we would use information about the same element in the previous frame. However, inspecting the previous frame tells us very little about what should be hovered in the current frame, as elements in the current frame may have changed significantly. ### Solution This pull request's main contribution is the introduction of a new `after_layout` phase when redrawing the window. The key idea is that we'll give every element a chance to register a hitbox (see `ElementContext::insert_hitbox`) before painting anything. Then, during the `paint` phase, elements can determine whether they're the topmost and draw their hover state accordingly. We are also removing the ability to give an arbitrary z-index to elements. Instead, we will follow the much simpler painter's algorithm. That is, an element that gets painted after will be drawn on top of an element that got painted earlier. Elements can still escape their current "stacking context" by using the new `ElementContext::defer_draw` method (see `Overlay` for an example). Elements drawn using this method will still be logically considered as being children of their original parent (for keybinding, focus and cache invalidation purposes) but their layout and paint passes will be deferred until the currently-drawn element is done. With these changes we also reworked geometry batching within the `Scene`. The new approach uses an AABB tree to determine geometry occlusion, which allows the GPU to render non-overlapping geometry in parallel. ### Performance Performance is slightly better than on `main` even though this new approach is more correct and we're maintaining an extra data structure (the AABB tree).  Release Notes: - Fixed a bug that was causing popovers to flicker. --------- Co-authored-by: Nathan Sobo <nathan@zed.dev> Co-authored-by: Thorsten <thorsten@zed.dev>
529 lines
19 KiB
Rust
529 lines
19 KiB
Rust
use editor::{scroll::Autoscroll, Anchor, Editor, ExcerptId};
|
|
use gpui::{
|
|
actions, canvas, div, rems, uniform_list, AnyElement, AppContext, Div, EventEmitter,
|
|
FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, MouseButton,
|
|
MouseDownEvent, MouseMoveEvent, ParentElement, Render, Styled, UniformListScrollHandle, View,
|
|
ViewContext, VisualContext, WeakView, WindowContext,
|
|
};
|
|
use language::{Buffer, OwnedSyntaxLayer};
|
|
use std::{mem, ops::Range};
|
|
use theme::ActiveTheme;
|
|
use tree_sitter::{Node, TreeCursor};
|
|
use ui::{h_flex, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
|
|
use workspace::{
|
|
item::{Item, ItemHandle},
|
|
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
|
};
|
|
|
|
actions!(debug, [OpenSyntaxTreeView]);
|
|
|
|
pub fn init(cx: &mut AppContext) {
|
|
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
|
workspace.register_action(|workspace, _: &OpenSyntaxTreeView, cx| {
|
|
let active_item = workspace.active_item(cx);
|
|
let workspace_handle = workspace.weak_handle();
|
|
let syntax_tree_view =
|
|
cx.new_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
|
|
workspace.split_item(SplitDirection::Right, Box::new(syntax_tree_view), cx)
|
|
});
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub struct SyntaxTreeView {
|
|
workspace_handle: WeakView<Workspace>,
|
|
editor: Option<EditorState>,
|
|
list_scroll_handle: UniformListScrollHandle,
|
|
selected_descendant_ix: Option<usize>,
|
|
hovered_descendant_ix: Option<usize>,
|
|
focus_handle: FocusHandle,
|
|
}
|
|
|
|
pub struct SyntaxTreeToolbarItemView {
|
|
tree_view: Option<View<SyntaxTreeView>>,
|
|
subscription: Option<gpui::Subscription>,
|
|
}
|
|
|
|
struct EditorState {
|
|
editor: View<Editor>,
|
|
active_buffer: Option<BufferState>,
|
|
_subscription: gpui::Subscription,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct BufferState {
|
|
buffer: Model<Buffer>,
|
|
excerpt_id: ExcerptId,
|
|
active_layer: Option<OwnedSyntaxLayer>,
|
|
}
|
|
|
|
impl SyntaxTreeView {
|
|
pub fn new(
|
|
workspace_handle: WeakView<Workspace>,
|
|
active_item: Option<Box<dyn ItemHandle>>,
|
|
cx: &mut ViewContext<Self>,
|
|
) -> Self {
|
|
let mut this = Self {
|
|
workspace_handle: workspace_handle.clone(),
|
|
list_scroll_handle: UniformListScrollHandle::new(),
|
|
editor: None,
|
|
hovered_descendant_ix: None,
|
|
selected_descendant_ix: None,
|
|
focus_handle: cx.focus_handle(),
|
|
};
|
|
|
|
this.workspace_updated(active_item, cx);
|
|
cx.observe(
|
|
&workspace_handle.upgrade().unwrap(),
|
|
|this, workspace, cx| {
|
|
this.workspace_updated(workspace.read(cx).active_item(cx), cx);
|
|
},
|
|
)
|
|
.detach();
|
|
|
|
this
|
|
}
|
|
|
|
fn workspace_updated(
|
|
&mut self,
|
|
active_item: Option<Box<dyn ItemHandle>>,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
if let Some(item) = active_item {
|
|
if item.item_id() != cx.entity_id() {
|
|
if let Some(editor) = item.act_as::<Editor>(cx) {
|
|
self.set_editor(editor, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_editor(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
|
if let Some(state) = &self.editor {
|
|
if state.editor == editor {
|
|
return;
|
|
}
|
|
editor.update(cx, |editor, cx| {
|
|
editor.clear_background_highlights::<Self>(cx)
|
|
});
|
|
}
|
|
|
|
let subscription = cx.subscribe(&editor, |this, _, event, cx| {
|
|
let did_reparse = match event {
|
|
editor::EditorEvent::Reparsed => true,
|
|
editor::EditorEvent::SelectionsChanged { .. } => false,
|
|
_ => return,
|
|
};
|
|
this.editor_updated(did_reparse, cx);
|
|
});
|
|
|
|
self.editor = Some(EditorState {
|
|
editor,
|
|
_subscription: subscription,
|
|
active_buffer: None,
|
|
});
|
|
self.editor_updated(true, cx);
|
|
}
|
|
|
|
fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
|
|
// Find which excerpt the cursor is in, and the position within that excerpted buffer.
|
|
let editor_state = self.editor.as_mut()?;
|
|
let editor = &editor_state.editor.read(cx);
|
|
let selection_range = editor.selections.last::<usize>(cx).range();
|
|
let multibuffer = editor.buffer().read(cx);
|
|
let (buffer, range, excerpt_id) = multibuffer
|
|
.range_to_buffer_ranges(selection_range, cx)
|
|
.pop()?;
|
|
|
|
// If the cursor has moved into a different excerpt, retrieve a new syntax layer
|
|
// from that buffer.
|
|
let buffer_state = editor_state
|
|
.active_buffer
|
|
.get_or_insert_with(|| BufferState {
|
|
buffer: buffer.clone(),
|
|
excerpt_id,
|
|
active_layer: None,
|
|
});
|
|
let mut prev_layer = None;
|
|
if did_reparse {
|
|
prev_layer = buffer_state.active_layer.take();
|
|
}
|
|
if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id {
|
|
buffer_state.buffer = buffer.clone();
|
|
buffer_state.excerpt_id = excerpt_id;
|
|
buffer_state.active_layer = None;
|
|
}
|
|
|
|
let layer = match &mut buffer_state.active_layer {
|
|
Some(layer) => layer,
|
|
None => {
|
|
let snapshot = buffer.read(cx).snapshot();
|
|
let layer = if let Some(prev_layer) = prev_layer {
|
|
let prev_range = prev_layer.node().byte_range();
|
|
snapshot
|
|
.syntax_layers()
|
|
.filter(|layer| layer.language == &prev_layer.language)
|
|
.min_by_key(|layer| {
|
|
let range = layer.node().byte_range();
|
|
((range.start as i64) - (prev_range.start as i64)).abs()
|
|
+ ((range.end as i64) - (prev_range.end as i64)).abs()
|
|
})?
|
|
} else {
|
|
snapshot.syntax_layers().next()?
|
|
};
|
|
buffer_state.active_layer.insert(layer.to_owned())
|
|
}
|
|
};
|
|
|
|
// Within the active layer, find the syntax node under the cursor,
|
|
// and scroll to it.
|
|
let mut cursor = layer.node().walk();
|
|
while cursor.goto_first_child_for_byte(range.start).is_some() {
|
|
if !range.is_empty() && cursor.node().end_byte() == range.start {
|
|
cursor.goto_next_sibling();
|
|
}
|
|
}
|
|
|
|
// Ascend to the smallest ancestor that contains the range.
|
|
loop {
|
|
let node_range = cursor.node().byte_range();
|
|
if node_range.start <= range.start && node_range.end >= range.end {
|
|
break;
|
|
}
|
|
if !cursor.goto_parent() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let descendant_ix = cursor.descendant_index();
|
|
self.selected_descendant_ix = Some(descendant_ix);
|
|
self.list_scroll_handle.scroll_to_item(descendant_ix);
|
|
|
|
cx.notify();
|
|
Some(())
|
|
}
|
|
|
|
fn update_editor_with_range_for_descendant_ix(
|
|
&self,
|
|
descendant_ix: usize,
|
|
cx: &mut ViewContext<Self>,
|
|
mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>),
|
|
) -> Option<()> {
|
|
let editor_state = self.editor.as_ref()?;
|
|
let buffer_state = editor_state.active_buffer.as_ref()?;
|
|
let layer = buffer_state.active_layer.as_ref()?;
|
|
|
|
// Find the node.
|
|
let mut cursor = layer.node().walk();
|
|
cursor.goto_descendant(descendant_ix);
|
|
let node = cursor.node();
|
|
let range = node.byte_range();
|
|
|
|
// Build a text anchor range.
|
|
let buffer = buffer_state.buffer.read(cx);
|
|
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
|
|
|
|
// Build a multibuffer anchor range.
|
|
let multibuffer = editor_state.editor.read(cx).buffer();
|
|
let multibuffer = multibuffer.read(cx).snapshot(cx);
|
|
let excerpt_id = buffer_state.excerpt_id;
|
|
let range = multibuffer
|
|
.anchor_in_excerpt(excerpt_id, range.start)
|
|
.unwrap()
|
|
..multibuffer
|
|
.anchor_in_excerpt(excerpt_id, range.end)
|
|
.unwrap();
|
|
|
|
// Update the editor with the anchor range.
|
|
editor_state.editor.update(cx, |editor, cx| {
|
|
f(editor, range, cx);
|
|
});
|
|
Some(())
|
|
}
|
|
|
|
fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &AppContext) -> Div {
|
|
let colors = cx.theme().colors();
|
|
let mut row = h_flex();
|
|
if let Some(field_name) = cursor.field_name() {
|
|
row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]);
|
|
}
|
|
|
|
let node = cursor.node();
|
|
return row
|
|
.child(if node.is_named() {
|
|
Label::new(node.kind()).color(Color::Default)
|
|
} else {
|
|
Label::new(format!("\"{}\"", node.kind())).color(Color::Created)
|
|
})
|
|
.child(
|
|
div()
|
|
.child(Label::new(format_node_range(node)).color(Color::Muted))
|
|
.pl_1(),
|
|
)
|
|
.text_bg(if selected {
|
|
colors.element_selected
|
|
} else {
|
|
Hsla::default()
|
|
})
|
|
.pl(rems(depth as f32))
|
|
.hover(|style| style.bg(colors.element_hover));
|
|
}
|
|
}
|
|
|
|
impl Render for SyntaxTreeView {
|
|
fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement {
|
|
let mut rendered = div().flex_1();
|
|
|
|
if let Some(layer) = self
|
|
.editor
|
|
.as_ref()
|
|
.and_then(|editor| editor.active_buffer.as_ref())
|
|
.and_then(|buffer| buffer.active_layer.as_ref())
|
|
{
|
|
let layer = layer.clone();
|
|
let mut list = uniform_list(
|
|
cx.view().clone(),
|
|
"SyntaxTreeView",
|
|
layer.node().descendant_count(),
|
|
move |this, range, cx| {
|
|
let mut items = Vec::new();
|
|
let mut cursor = layer.node().walk();
|
|
let mut descendant_ix = range.start;
|
|
cursor.goto_descendant(descendant_ix);
|
|
let mut depth = cursor.depth();
|
|
let mut visited_children = false;
|
|
while descendant_ix < range.end {
|
|
if visited_children {
|
|
if cursor.goto_next_sibling() {
|
|
visited_children = false;
|
|
} else if cursor.goto_parent() {
|
|
depth -= 1;
|
|
} else {
|
|
break;
|
|
}
|
|
} else {
|
|
items.push(
|
|
Self::render_node(
|
|
&cursor,
|
|
depth,
|
|
Some(descendant_ix) == this.selected_descendant_ix,
|
|
cx,
|
|
)
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
cx.listener(move |tree_view, _: &MouseDownEvent, cx| {
|
|
tree_view.update_editor_with_range_for_descendant_ix(
|
|
descendant_ix,
|
|
cx,
|
|
|editor, mut range, cx| {
|
|
// Put the cursor at the beginning of the node.
|
|
mem::swap(&mut range.start, &mut range.end);
|
|
|
|
editor.change_selections(
|
|
Some(Autoscroll::newest()),
|
|
cx,
|
|
|selections| {
|
|
selections.select_ranges(vec![range]);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}),
|
|
)
|
|
.on_mouse_move(cx.listener(
|
|
move |tree_view, _: &MouseMoveEvent, cx| {
|
|
if tree_view.hovered_descendant_ix != Some(descendant_ix) {
|
|
tree_view.hovered_descendant_ix = Some(descendant_ix);
|
|
tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, cx, |editor, range, cx| {
|
|
editor.clear_background_highlights::<Self>(cx);
|
|
editor.highlight_background::<Self>(
|
|
vec![range],
|
|
|theme| theme.editor_document_highlight_write_background,
|
|
cx,
|
|
);
|
|
});
|
|
cx.notify();
|
|
}
|
|
},
|
|
)),
|
|
);
|
|
descendant_ix += 1;
|
|
if cursor.goto_first_child() {
|
|
depth += 1;
|
|
} else {
|
|
visited_children = true;
|
|
}
|
|
}
|
|
}
|
|
items
|
|
},
|
|
)
|
|
.size_full()
|
|
.track_scroll(self.list_scroll_handle.clone())
|
|
.text_bg(cx.theme().colors().background).into_any_element();
|
|
|
|
rendered = rendered.child(
|
|
canvas(
|
|
move |bounds, cx| {
|
|
list.layout(bounds.origin, bounds.size.into(), cx);
|
|
list
|
|
},
|
|
|_, mut list, cx| list.paint(cx),
|
|
)
|
|
.size_full(),
|
|
);
|
|
}
|
|
|
|
rendered
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<()> for SyntaxTreeView {}
|
|
|
|
impl FocusableView for SyntaxTreeView {
|
|
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
impl Item for SyntaxTreeView {
|
|
type Event = ();
|
|
|
|
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
|
|
|
|
fn tab_content(&self, _: Option<usize>, selected: bool, _: &WindowContext<'_>) -> AnyElement {
|
|
Label::new("Syntax Tree")
|
|
.color(if selected {
|
|
Color::Default
|
|
} else {
|
|
Color::Muted
|
|
})
|
|
.into_any_element()
|
|
}
|
|
|
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
|
None
|
|
}
|
|
|
|
fn clone_on_split(
|
|
&self,
|
|
_: workspace::WorkspaceId,
|
|
cx: &mut ViewContext<Self>,
|
|
) -> Option<View<Self>>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
Some(cx.new_view(|cx| {
|
|
let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
|
|
if let Some(editor) = &self.editor {
|
|
clone.set_editor(editor.editor.clone(), cx)
|
|
}
|
|
clone
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl SyntaxTreeToolbarItemView {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
tree_view: None,
|
|
subscription: None,
|
|
}
|
|
}
|
|
|
|
fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option<PopoverMenu<ContextMenu>> {
|
|
let tree_view = self.tree_view.as_ref()?;
|
|
let tree_view = tree_view.read(cx);
|
|
|
|
let editor_state = tree_view.editor.as_ref()?;
|
|
let buffer_state = editor_state.active_buffer.as_ref()?;
|
|
let active_layer = buffer_state.active_layer.clone()?;
|
|
let active_buffer = buffer_state.buffer.read(cx).snapshot();
|
|
|
|
let view = cx.view().clone();
|
|
Some(
|
|
popover_menu("Syntax Tree")
|
|
.trigger(Self::render_header(&active_layer))
|
|
.menu(move |cx| {
|
|
ContextMenu::build(cx, |mut menu, cx| {
|
|
for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() {
|
|
menu = menu.entry(
|
|
format!(
|
|
"{} {}",
|
|
layer.language.name(),
|
|
format_node_range(layer.node())
|
|
),
|
|
None,
|
|
cx.handler_for(&view, move |view, cx| {
|
|
view.select_layer(layer_ix, cx);
|
|
}),
|
|
);
|
|
}
|
|
menu
|
|
})
|
|
.into()
|
|
}),
|
|
)
|
|
}
|
|
|
|
fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
|
|
let tree_view = self.tree_view.as_ref()?;
|
|
tree_view.update(cx, |view, cx| {
|
|
let editor_state = view.editor.as_mut()?;
|
|
let buffer_state = editor_state.active_buffer.as_mut()?;
|
|
let snapshot = buffer_state.buffer.read(cx).snapshot();
|
|
let layer = snapshot.syntax_layers().nth(layer_ix)?;
|
|
buffer_state.active_layer = Some(layer.to_owned());
|
|
view.selected_descendant_ix = None;
|
|
cx.notify();
|
|
view.focus_handle.focus(cx);
|
|
Some(())
|
|
})
|
|
}
|
|
|
|
fn render_header(active_layer: &OwnedSyntaxLayer) -> ButtonLike {
|
|
ButtonLike::new("syntax tree header")
|
|
.child(Label::new(active_layer.language.name()))
|
|
.child(Label::new(format_node_range(active_layer.node())))
|
|
}
|
|
}
|
|
|
|
fn format_node_range(node: Node) -> String {
|
|
let start = node.start_position();
|
|
let end = node.end_position();
|
|
format!(
|
|
"[{}:{} - {}:{}]",
|
|
start.row + 1,
|
|
start.column + 1,
|
|
end.row + 1,
|
|
end.column + 1,
|
|
)
|
|
}
|
|
|
|
impl Render for SyntaxTreeToolbarItemView {
|
|
fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> impl IntoElement {
|
|
self.render_menu(cx)
|
|
.unwrap_or_else(|| popover_menu("Empty Syntax Tree"))
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<ToolbarItemEvent> for SyntaxTreeToolbarItemView {}
|
|
|
|
impl ToolbarItemView for SyntaxTreeToolbarItemView {
|
|
fn set_active_pane_item(
|
|
&mut self,
|
|
active_pane_item: Option<&dyn ItemHandle>,
|
|
cx: &mut ViewContext<Self>,
|
|
) -> ToolbarItemLocation {
|
|
if let Some(item) = active_pane_item {
|
|
if let Some(view) = item.downcast::<SyntaxTreeView>() {
|
|
self.tree_view = Some(view.clone());
|
|
self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
|
|
return ToolbarItemLocation::PrimaryLeft;
|
|
}
|
|
}
|
|
self.tree_view = None;
|
|
self.subscription = None;
|
|
ToolbarItemLocation::Hidden
|
|
}
|
|
}
|