From 4700d33728f4ea137cd4143e5a72bdf763cb16e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 Mar 2024 10:45:57 +0100 Subject: [PATCH] Fix flickering (#9012) 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). ![before_after](https://github.com/zed-industries/zed/assets/482957/c8120b07-1dbd-4776-834a-d040e569a71e) Release Notes: - Fixed a bug that was causing popovers to flicker. --------- Co-authored-by: Nathan Sobo Co-authored-by: Thorsten --- crates/assistant/src/assistant_panel.rs | 44 +- crates/cli/src/main.rs | 2 +- crates/collab/src/main.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 5 +- crates/collab_ui/src/collab_panel.rs | 133 +- crates/collab_ui/src/collab_titlebar_item.rs | 39 +- crates/collab_ui/src/face_pile.rs | 28 +- crates/diagnostics/src/diagnostics.rs | 28 +- crates/editor/src/display_map.rs | 2 +- crates/editor/src/editor.rs | 34 +- crates/editor/src/element.rs | 4042 +++++++++-------- crates/editor/src/hover_links.rs | 4 +- crates/editor/src/hover_popover.rs | 7 +- crates/editor/src/test.rs | 9 +- crates/extensions_ui/src/extensions_ui.rs | 29 +- crates/gpui/src/app.rs | 89 +- crates/gpui/src/app/test_context.rs | 13 +- crates/gpui/src/bounds_tree.rs | 292 ++ crates/gpui/src/element.rs | 408 +- crates/gpui/src/elements/canvas.rs | 52 +- crates/gpui/src/elements/div.rs | 1754 +++---- crates/gpui/src/elements/img.rs | 113 +- crates/gpui/src/elements/list.rs | 76 +- crates/gpui/src/elements/overlay.rs | 61 +- crates/gpui/src/elements/svg.rs | 35 +- crates/gpui/src/elements/text.rs | 410 +- crates/gpui/src/elements/uniform_list.rs | 167 +- crates/gpui/src/geometry.rs | 44 + crates/gpui/src/gpui.rs | 1 + crates/gpui/src/key_dispatch.rs | 152 +- crates/gpui/src/platform.rs | 4 +- .../gpui/src/platform/mac/metal_renderer.rs | 1 + crates/gpui/src/platform/test/platform.rs | 2 +- crates/gpui/src/scene.rs | 400 +- crates/gpui/src/style.rs | 161 +- crates/gpui/src/styled.rs | 6 - crates/gpui/src/taffy.rs | 27 +- crates/gpui/src/text_system.rs | 26 +- crates/gpui/src/text_system/line.rs | 366 +- crates/gpui/src/text_system/line_layout.rs | 150 +- crates/gpui/src/view.rs | 284 +- crates/gpui/src/window.rs | 369 +- crates/gpui/src/window/element_cx.rs | 956 ++-- crates/gpui/src/window/prompts.rs | 17 +- crates/gpui_macros/src/derive_into_element.rs | 4 - crates/install_cli/src/install_cli.rs | 2 +- crates/language_tools/src/lsp_log.rs | 2 +- crates/language_tools/src/syntax_tree_view.rs | 26 +- crates/languages/src/csharp.rs | 2 +- crates/languages/src/elixir.rs | 2 +- crates/languages/src/lua.rs | 2 +- crates/languages/src/rust.rs | 2 +- crates/languages/src/toml.rs | 2 +- crates/languages/src/zig.rs | 2 +- crates/project_panel/src/project_panel.rs | 2 +- crates/storybook/src/stories.rs | 2 - crates/storybook/src/stories/z_index.rs | 172 - crates/storybook/src/story_selector.rs | 2 - crates/terminal/src/terminal.rs | 2 +- crates/terminal_view/src/terminal_element.rs | 566 ++- crates/theme/src/styles/stories/players.rs | 3 - crates/ui/src/components/avatar/avatar.rs | 5 +- crates/ui/src/components/context_menu.rs | 2 +- crates/ui/src/components/popover_menu.rs | 207 +- crates/ui/src/components/right_click_menu.rs | 245 +- crates/ui/src/components/tab_bar.rs | 2 - crates/ui/src/styled_ext.rs | 1 - crates/ui/src/styles/elevation.rs | 34 - crates/workspace/src/dock.rs | 14 +- crates/workspace/src/modal_layer.rs | 26 +- crates/workspace/src/pane.rs | 2 - .../src/pane/dragged_item_receiver.rs | 239 - crates/workspace/src/pane_group.rs | 256 +- crates/workspace/src/workspace.rs | 63 +- 74 files changed, 6434 insertions(+), 6301 deletions(-) create mode 100644 crates/gpui/src/bounds_tree.rs delete mode 100644 crates/storybook/src/stories/z_index.rs delete mode 100644 crates/workspace/src/pane/dragged_item_receiver.rs diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index fd590ea514..98d47809e2 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -31,9 +31,9 @@ use fs::Fs; use futures::StreamExt; use gpui::{ canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, - AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter, - FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, - IntoElement, Model, ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, + AsyncAppContext, AsyncWindowContext, ClipboardItem, Context, EventEmitter, FocusHandle, + FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, + ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, }; @@ -1284,25 +1284,25 @@ impl Render for AssistantPanel { let view = cx.view().clone(); let scroll_handle = self.saved_conversations_scroll_handle.clone(); let conversation_count = self.saved_conversations.len(); - canvas(move |bounds, cx| { - uniform_list( - view, - "saved_conversations", - conversation_count, - |this, range, cx| { - range - .map(|ix| this.render_saved_conversation(ix, cx)) - .collect() - }, - ) - .track_scroll(scroll_handle) - .into_any_element() - .draw( - bounds.origin, - bounds.size.map(AvailableSpace::Definite), - cx, - ); - }) + canvas( + move |bounds, cx| { + let mut list = uniform_list( + view, + "saved_conversations", + conversation_count, + |this, range, cx| { + range + .map(|ix| this.render_saved_conversation(ix, cx)) + .collect() + }, + ) + .track_scroll(scroll_handle) + .into_any_element(); + list.layout(bounds.origin, bounds.size.into(), cx); + list + }, + |_bounds, mut list, cx| list.paint(cx), + ) .size_full() .into_any_element() }), diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 86e901368a..e6ec1e559d 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -156,7 +156,7 @@ mod linux { } } -// todo(windows) +// todo("windows") #[cfg(target_os = "windows")] mod windows { use std::path::Path; diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index a082ff9219..e4b134b06b 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -132,7 +132,7 @@ async fn main() -> Result<()> { .await .map_err(|e| anyhow!(e))?; - // todo(windows) + // todo("windows") #[cfg(windows)] unimplemented!(); } diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index c0a8a8d2f4..85f7f548b2 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -600,11 +600,9 @@ impl ChatPanel { ) -> Div { div() .absolute() - .z_index(1) .child( div() .absolute() - .z_index(1) .right_8() .w_6() .rounded_tl_md() @@ -638,7 +636,6 @@ impl ChatPanel { .child( div() .absolute() - .z_index(1) .right_2() .w_6() .rounded_tr_md() @@ -855,7 +852,7 @@ impl Render for ChatPanel { .size_full() .on_action(cx.listener(Self::send)) .child( - h_flex().z_index(1).child( + h_flex().child( TabBar::new("chat_header").child( h_flex() .w_full() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index f008e858df..00a1625621 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -989,7 +989,6 @@ impl CollabPanel { .children(has_channel_buffer_changed.then(|| { div() .w_1p5() - .z_index(1) .absolute() .right(px(2.)) .top(px(2.)) @@ -1022,7 +1021,6 @@ impl CollabPanel { .children(has_messages_notification.then(|| { div() .w_1p5() - .z_index(1) .absolute() .right(px(2.)) .top(px(4.)) @@ -2617,7 +2615,6 @@ impl CollabPanel { .children(has_notes_notification.then(|| { div() .w_1p5() - .z_index(1) .absolute() .right(px(-1.)) .top(px(-1.)) @@ -2632,49 +2629,44 @@ impl CollabPanel { ), ) .child( - h_flex() - .absolute() - .right(rems(0.)) - .z_index(1) - .h_full() - .child( - h_flex() - .h_full() - .gap_1() - .px_1() - .child( - IconButton::new("channel_chat", IconName::MessageBubbles) - .style(ButtonStyle::Filled) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(if has_messages_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| { - this.join_channel_chat(channel_id, cx) - })) - .tooltip(|cx| Tooltip::text("Open channel chat", cx)) - .visible_on_hover(""), - ) - .child( - IconButton::new("channel_notes", IconName::File) - .style(ButtonStyle::Filled) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(if has_notes_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| { - this.open_channel_notes(channel_id, cx) - })) - .tooltip(|cx| Tooltip::text("Open channel notes", cx)) - .visible_on_hover(""), - ), - ), + h_flex().absolute().right(rems(0.)).h_full().child( + h_flex() + .h_full() + .gap_1() + .px_1() + .child( + IconButton::new("channel_chat", IconName::MessageBubbles) + .style(ButtonStyle::Filled) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(if has_messages_notification { + Color::Default + } else { + Color::Muted + }) + .on_click(cx.listener(move |this, _, cx| { + this.join_channel_chat(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel chat", cx)) + .visible_on_hover(""), + ) + .child( + IconButton::new("channel_notes", IconName::File) + .style(ButtonStyle::Filled) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(if has_notes_notification { + Color::Default + } else { + Color::Muted + }) + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel notes", cx)) + .visible_on_hover(""), + ), + ), ) .tooltip({ let channel_store = self.channel_store.clone(); @@ -2720,31 +2712,34 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> let thickness = px(1.); let color = cx.theme().colors().text; - canvas(move |bounds, cx| { - let start_x = (bounds.left() + bounds.right() - thickness) / 2.; - let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.; - let right = bounds.right(); - let top = bounds.top(); + canvas( + |_, _| {}, + move |bounds, _, cx| { + let start_x = (bounds.left() + bounds.right() - thickness) / 2.; + let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.; + let right = bounds.right(); + let top = bounds.top(); - cx.paint_quad(fill( - Bounds::from_corners( - point(start_x, top), - point( - start_x + thickness, - if is_last { - start_y - } else { - bounds.bottom() + if overdraw { px(1.) } else { px(0.) } - }, + cx.paint_quad(fill( + Bounds::from_corners( + point(start_x, top), + point( + start_x + thickness, + if is_last { + start_y + } else { + bounds.bottom() + if overdraw { px(1.) } else { px(0.) } + }, + ), ), - ), - color, - )); - cx.paint_quad(fill( - Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)), - color, - )); - }) + color, + )); + cx.paint_quad(fill( + Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)), + color, + )); + }, + ) .w(width) .h(line_height) } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index ea5b96b09f..47a12c2cb1 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -329,24 +329,27 @@ impl Render for CollabTitlebarItem { } } -fn render_color_ribbon(color: Hsla) -> gpui::Canvas { - canvas(move |bounds, cx| { - let height = bounds.size.height; - let horizontal_offset = height; - let vertical_offset = px(height.0 / 2.0); - let mut path = Path::new(bounds.lower_left()); - path.curve_to( - bounds.origin + point(horizontal_offset, vertical_offset), - bounds.origin + point(px(0.0), vertical_offset), - ); - path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset)); - path.curve_to( - bounds.lower_right(), - bounds.upper_right() + point(px(0.0), vertical_offset), - ); - path.line_to(bounds.lower_left()); - cx.paint_path(path, color); - }) +fn render_color_ribbon(color: Hsla) -> impl Element { + canvas( + move |_, _| {}, + move |bounds, _, cx| { + let height = bounds.size.height; + let horizontal_offset = height; + let vertical_offset = px(height.0 / 2.0); + let mut path = Path::new(bounds.lower_left()); + path.curve_to( + bounds.origin + point(horizontal_offset, vertical_offset), + bounds.origin + point(px(0.0), vertical_offset), + ); + path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset)); + path.curve_to( + bounds.lower_right(), + bounds.upper_right() + point(px(0.0), vertical_offset), + ); + path.line_to(bounds.lower_left()); + cx.paint_path(path, color); + }, + ) .h_1() .w_full() } diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index 71d15eb155..ecdcfcaa93 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -14,25 +14,25 @@ impl FacePile { } pub fn new(faces: SmallVec<[AnyElement; 2]>) -> Self { - Self { - base: h_flex(), - faces, - } + Self { base: div(), faces } } } impl RenderOnce for FacePile { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - let player_count = self.faces.len(); - let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| { - let isnt_last = ix < player_count - 1; - - div() - .z_index((player_count - ix) as u16) - .when(isnt_last, |div| div.neg_mr_1()) - .child(player) - }); - self.base.children(player_list) + // Lay the faces out in reverse so they overlap in the desired order (left to right, front to back) + self.base + .flex() + .flex_row_reverse() + .items_center() + .justify_start() + .children( + self.faces + .into_iter() + .enumerate() + .rev() + .map(|(ix, player)| div().when(ix > 0, |div| div.neg_ml_1()).child(player)), + ) } } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d211eed516..be5e4a7f8a 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -894,7 +894,7 @@ mod tests { display_map::{BlockContext, TransformBlock}, DisplayPoint, GutterDimensions, }; - use gpui::{px, TestAppContext, VisualTestContext, WindowContext}; + use gpui::{px, Stateful, TestAppContext, VisualTestContext, WindowContext}; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; use project::FakeFs; use serde_json::json; @@ -1600,20 +1600,18 @@ mod tests { let name: SharedString = match block { TransformBlock::Custom(block) => cx.with_element_context({ |cx| -> Option { - block - .render(&mut BlockContext { - context: cx, - anchor_x: px(0.), - gutter_dimensions: &GutterDimensions::default(), - line_height: px(0.), - em_width: px(0.), - max_width: px(0.), - block_id: ix, - editor_style: &editor::EditorStyle::default(), - }) - .inner_id()? - .try_into() - .ok() + let mut element = block.render(&mut BlockContext { + context: cx, + anchor_x: px(0.), + gutter_dimensions: &GutterDimensions::default(), + line_height: px(0.), + em_width: px(0.), + max_width: px(0.), + block_id: ix, + editor_style: &editor::EditorStyle::default(), + }); + let element = element.downcast_mut::>().unwrap(); + element.interactivity().element_id.clone()?.try_into().ok() } })?, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 2e3e4c0c12..3bcaf8803a 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -46,7 +46,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; -pub use self::fold_map::{Fold, FoldPoint}; +pub use self::fold_map::{Fold, FoldId, FoldPoint}; pub use self::inlay_map::{InlayOffset, InlayPoint}; pub(crate) use inlay_map::Inlay; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a5c8b4e5cb..21ccd3d8b7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -51,7 +51,9 @@ pub use display_map::DisplayPoint; use display_map::*; pub use editor_settings::EditorSettings; use element::LineWithInvisibles; -pub use element::{Cursor, EditorElement, HighlightedRange, HighlightedRangeLine}; +pub use element::{ + CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, +}; use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; @@ -4202,14 +4204,14 @@ impl Editor { } pub fn render_fold_indicators( - &self, + &mut self, fold_data: Vec>, _style: &EditorStyle, gutter_hovered: bool, _line_height: Pixels, _gutter_margin: Pixels, - editor_view: View, - ) -> Vec> { + cx: &mut ViewContext, + ) -> Vec> { fold_data .iter() .enumerate() @@ -4218,24 +4220,20 @@ impl Editor { .map(|(fold_status, buffer_row, active)| { (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { IconButton::new(ix, ui::IconName::ChevronDown) - .on_click({ - let view = editor_view.clone(); - move |_e, cx| { - view.update(cx, |editor, cx| match fold_status { - FoldStatus::Folded => { - editor.unfold_at(&UnfoldAt { buffer_row }, cx); - } - FoldStatus::Foldable => { - editor.fold_at(&FoldAt { buffer_row }, cx); - } - }) + .on_click(cx.listener(move |this, _e, cx| match fold_status { + FoldStatus::Folded => { + this.unfold_at(&UnfoldAt { buffer_row }, cx); } - }) + FoldStatus::Foldable => { + this.fold_at(&FoldAt { buffer_row }, cx); + } + })) .icon_color(ui::Color::Muted) .icon_size(ui::IconSize::Small) .selected(fold_status == FoldStatus::Folded) .selected_icon(ui::IconName::ChevronRight) .size(ui::ButtonSize::None) + .into_any_element() }) }) .flatten() @@ -9215,7 +9213,7 @@ impl Editor { &self, search_range: Range, display_snapshot: &DisplaySnapshot, - cx: &mut ViewContext, + cx: &WindowContext, ) -> Vec> { display_snapshot .buffer_snapshot @@ -9986,7 +9984,7 @@ impl EditorSnapshot { self.is_focused } - pub fn placeholder_text(&self, _cx: &mut WindowContext) -> Option<&Arc> { + pub fn placeholder_text(&self) -> Option<&Arc> { self.placeholder_text.as_ref() } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index edee7b53f8..33e81faabd 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -22,11 +22,11 @@ use git::diff::DiffHunkStatus; use gpui::{ div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ContentMask, Corners, CursorStyle, - DispatchPhase, Edges, Element, ElementInputHandler, Entity, Hsla, InteractiveBounds, + DispatchPhase, Edges, Element, ElementContext, ElementInputHandler, Entity, Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, - SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, - TextStyle, View, ViewContext, WindowContext, + SharedString, Size, Stateful, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, + TextStyleRefinement, View, ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -43,14 +43,14 @@ use std::{ borrow::Cow, cmp::{self, Ordering}, fmt::Write, - iter, + iter, mem, ops::Range, sync::Arc, }; use sum_tree::Bias; use theme::{ActiveTheme, PlayerColor}; use ui::prelude::*; -use ui::{h_flex, ButtonLike, ButtonStyle, IconButton, Tooltip}; +use ui::{h_flex, ButtonLike, ButtonStyle, Tooltip}; use util::ResultExt; use workspace::item::Item; @@ -342,30 +342,18 @@ impl EditorElement { register_action(view, cx, Editor::revert_selected_hunks); } - fn register_key_listeners( - &self, - cx: &mut ElementContext, - text_bounds: Bounds, - layout: &LayoutState, - ) { + fn register_key_listeners(&self, cx: &mut ElementContext, layout: &EditorLayout) { let position_map = layout.position_map.clone(); - let stacking_order = cx.stacking_order().clone(); cx.on_key_event({ let editor = self.editor.clone(); + let text_hitbox = layout.text_hitbox.clone(); move |event: &ModifiersChangedEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } editor.update(cx, |editor, cx| { - Self::modifiers_changed( - editor, - event, - &position_map, - text_bounds, - &stacking_order, - cx, - ) + Self::modifiers_changed(editor, event, &position_map, &text_hitbox, cx) }) } }); @@ -375,19 +363,16 @@ impl EditorElement { editor: &mut Editor, event: &ModifiersChangedEvent, position_map: &PositionMap, - text_bounds: Bounds, - stacking_order: &StackingOrder, + text_hitbox: &Hitbox, cx: &mut ViewContext, ) { let mouse_position = cx.mouse_position(); - if !text_bounds.contains(&mouse_position) - || !cx.was_top_layer(&mouse_position, stacking_order) - { + if !text_hitbox.is_hovered(cx) { return; } editor.update_hovered_link( - position_map.point_for_position(text_bounds, mouse_position), + position_map.point_for_position(text_hitbox.bounds, mouse_position), &position_map.snapshot, event.modifiers, cx, @@ -398,26 +383,25 @@ impl EditorElement { editor: &mut Editor, event: &MouseDownEvent, position_map: &PositionMap, - text_bounds: Bounds, - gutter_bounds: Bounds, - stacking_order: &StackingOrder, + text_hitbox: &Hitbox, + gutter_hitbox: &Hitbox, cx: &mut ViewContext, ) { + if cx.default_prevented() { + return; + } + let mut click_count = event.click_count; let modifiers = event.modifiers; - if cx.default_prevented() { - return; - } else if gutter_bounds.contains(&event.position) { + if gutter_hitbox.is_hovered(cx) { click_count = 3; // Simulate triple-click when clicking the gutter to select lines - } else if !text_bounds.contains(&event.position) { - return; - } - if !cx.was_top_layer(&event.position, stacking_order) { + } else if !text_hitbox.is_hovered(cx) { return; } - let point_for_position = position_map.point_for_position(text_bounds, event.position); + let point_for_position = + position_map.point_for_position(text_hitbox.bounds, event.position); let position = point_for_position.previous_valid; if modifiers.shift && modifiers.alt { editor.select( @@ -453,13 +437,14 @@ impl EditorElement { editor: &mut Editor, event: &MouseDownEvent, position_map: &PositionMap, - text_bounds: Bounds, + text_hitbox: &Hitbox, cx: &mut ViewContext, ) { - if !text_bounds.contains(&event.position) { + if !text_hitbox.is_hovered(cx) { return; } - let point_for_position = position_map.point_for_position(text_bounds, event.position); + let point_for_position = + position_map.point_for_position(text_hitbox.bounds, event.position); mouse_context_menu::deploy_context_menu( editor, event.position, @@ -473,9 +458,7 @@ impl EditorElement { editor: &mut Editor, event: &MouseUpEvent, position_map: &PositionMap, - text_bounds: Bounds, - interactive_bounds: &InteractiveBounds, - stacking_order: &StackingOrder, + text_hitbox: &Hitbox, cx: &mut ViewContext, ) { let end_selection = editor.has_pending_selection(); @@ -485,13 +468,8 @@ impl EditorElement { editor.select(SelectPhase::End, cx); } - if interactive_bounds.visibly_contains(&event.position, cx) - && !pending_nonempty_selections - && event.modifiers.command - && text_bounds.contains(&event.position) - && cx.was_top_layer(&event.position, stacking_order) - { - let point = position_map.point_for_position(text_bounds, event.position); + if !pending_nonempty_selections && event.modifiers.command && text_hitbox.is_hovered(cx) { + let point = position_map.point_for_position(text_hitbox.bounds, event.position); editor.handle_click_hovered_link(point, event.modifiers, cx); cx.stop_propagation(); @@ -505,8 +483,6 @@ impl EditorElement { event: &MouseMoveEvent, position_map: &PositionMap, text_bounds: Bounds, - _gutter_bounds: Bounds, - _stacking_order: &StackingOrder, cx: &mut ViewContext, ) { if !editor.has_pending_selection() { @@ -549,21 +525,18 @@ impl EditorElement { editor: &mut Editor, event: &MouseMoveEvent, position_map: &PositionMap, - text_bounds: Bounds, - gutter_bounds: Bounds, - stacking_order: &StackingOrder, + text_hitbox: &Hitbox, + gutter_hitbox: &Hitbox, cx: &mut ViewContext, ) { let modifiers = event.modifiers; - let text_hovered = text_bounds.contains(&event.position); - let gutter_hovered = gutter_bounds.contains(&event.position); - let was_top = cx.was_top_layer(&event.position, stacking_order); - + let gutter_hovered = gutter_hitbox.is_hovered(cx); editor.set_gutter_hovered(gutter_hovered, cx); // Don't trigger hover popover if mouse is hovering over context menu - if text_hovered && was_top { - let point_for_position = position_map.point_for_position(text_bounds, event.position); + if text_hitbox.is_hovered(cx) { + let point_for_position = + position_map.point_for_position(text_hitbox.bounds, event.position); editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx); @@ -574,7 +547,7 @@ impl EditorElement { } else { editor.hide_hovered_link(cx); hover_at(editor, None, cx); - if gutter_hovered && was_top { + if gutter_hovered { cx.stop_propagation(); } } @@ -625,742 +598,331 @@ impl EditorElement { cx.notify() } - fn paint_background( + fn layout_selections( &self, - gutter_bounds: Bounds, - text_bounds: Bounds, - layout: &LayoutState, + start_anchor: Anchor, + end_anchor: Anchor, + snapshot: &EditorSnapshot, + start_row: u32, + end_row: u32, cx: &mut ElementContext, + ) -> ( + Vec<(PlayerColor, Vec)>, + BTreeMap, + Option, ) { - let bounds = gutter_bounds.union(&text_bounds); - let scroll_top = - layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height; - let gutter_bg = cx.theme().colors().editor_gutter_background; - cx.paint_quad(fill(gutter_bounds, gutter_bg)); - cx.paint_quad(fill(text_bounds, self.style.background)); + let mut selections: Vec<(PlayerColor, Vec)> = Vec::new(); + let mut active_rows = BTreeMap::new(); + let mut newest_selection_head = None; + let editor = self.editor.read(cx); - if let EditorMode::Full = layout.mode { - let mut active_rows = layout.active_rows.iter().peekable(); - while let Some((start_row, contains_non_empty_selection)) = active_rows.next() { - let mut end_row = *start_row; - while active_rows.peek().map_or(false, |r| { - *r.0 == end_row + 1 && r.1 == contains_non_empty_selection - }) { - active_rows.next().unwrap(); - end_row += 1; - } + if editor.show_local_selections { + let mut local_selections: Vec> = editor + .selections + .disjoint_in_range(start_anchor..end_anchor, cx); + local_selections.extend(editor.selections.pending(cx)); + let mut layouts = Vec::new(); + let newest = editor.selections.newest(cx); + for selection in local_selections.drain(..) { + let is_empty = selection.start == selection.end; + let is_newest = selection == newest; - if !contains_non_empty_selection { - let origin = point( - bounds.origin.x, - bounds.origin.y + (layout.position_map.line_height * *start_row as f32) - - scroll_top, - ); - let size = size( - bounds.size.width, - layout.position_map.line_height * (end_row - start_row + 1) as f32, - ); - let active_line_bg = cx.theme().colors().editor_active_line_background; - cx.paint_quad(fill(Bounds { origin, size }, active_line_bg)); - } - } - - let mut paint_highlight = |highlight_row_start: u32, highlight_row_end: u32, color| { - let origin = point( - bounds.origin.x, - bounds.origin.y - + (layout.position_map.line_height * highlight_row_start as f32) - - scroll_top, + let layout = SelectionLayout::new( + selection, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + is_newest, + editor.leader_peer_id.is_none(), + None, ); - let size = size( - bounds.size.width, - layout.position_map.line_height - * (highlight_row_end + 1 - highlight_row_start) as f32, - ); - cx.paint_quad(fill(Bounds { origin, size }, color)); - }; - let mut last_row = None; - let mut highlight_row_start = 0u32; - let mut highlight_row_end = 0u32; - for (&row, &color) in &layout.highlighted_rows { - let paint = last_row.map_or(false, |(last_row, last_color)| { - last_color != color || last_row + 1 < row - }); - - if paint { - let paint_range_is_unfinished = highlight_row_end == 0; - if paint_range_is_unfinished { - highlight_row_end = row; - last_row = None; - } - paint_highlight(highlight_row_start, highlight_row_end, color); - highlight_row_start = 0; - highlight_row_end = 0; - if !paint_range_is_unfinished { - highlight_row_start = row; - last_row = Some((row, color)); - } - } else { - if last_row.is_none() { - highlight_row_start = row; - } else { - highlight_row_end = row; - } - last_row = Some((row, color)); + if is_newest { + newest_selection_head = Some(layout.head); } - } - if let Some((row, hsla)) = last_row { - highlight_row_end = row; - paint_highlight(highlight_row_start, highlight_row_end, hsla); - } - let scroll_left = - layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width; - - for (wrap_position, active) in layout.wrap_guides.iter() { - let x = (text_bounds.origin.x + *wrap_position + layout.position_map.em_width / 2.) - - scroll_left; - - if x < text_bounds.origin.x - || (layout.show_scrollbars && x > self.scrollbar_left(&bounds)) + for row in cmp::max(layout.active_rows.start, start_row) + ..=cmp::min(layout.active_rows.end, end_row) { - continue; + let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); + *contains_non_empty_selection |= !is_empty; } + layouts.push(layout); + } - let color = if *active { - cx.theme().colors().editor_active_wrap_guide + let player = if editor.read_only(cx) { + cx.theme().players().read_only() + } else { + self.style.local_player + }; + + selections.push((player, layouts)); + } + + if let Some(collaboration_hub) = &editor.collaboration_hub { + // When following someone, render the local selections in their color. + if let Some(leader_id) = editor.leader_peer_id { + if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) { + if let Some(participant_index) = collaboration_hub + .user_participant_indices(cx) + .get(&collaborator.user_id) + { + if let Some((local_selection_style, _)) = selections.first_mut() { + *local_selection_style = cx + .theme() + .players() + .color_for_participant(participant_index.0); + } + } + } + } + + let mut remote_selections = HashMap::default(); + for selection in snapshot.remote_selections_in_range( + &(start_anchor..end_anchor), + collaboration_hub.as_ref(), + cx, + ) { + let selection_style = if let Some(participant_index) = selection.participant_index { + cx.theme() + .players() + .color_for_participant(participant_index.0) } else { - cx.theme().colors().editor_wrap_guide + cx.theme().players().absent() }; - cx.paint_quad(fill( - Bounds { - origin: point(x, text_bounds.origin.y), - size: size(px(1.), text_bounds.size.height), - }, - color, - )); + + // Don't re-render the leader's selections, since the local selections + // match theirs. + if Some(selection.peer_id) == editor.leader_peer_id { + continue; + } + let key = HoveredCursor { + replica_id: selection.replica_id, + selection_id: selection.selection.id, + }; + + let is_shown = + editor.show_cursor_names || editor.hovered_cursors.contains_key(&key); + + remote_selections + .entry(selection.replica_id) + .or_insert((selection_style, Vec::new())) + .1 + .push(SelectionLayout::new( + selection.selection, + selection.line_mode, + selection.cursor_shape, + &snapshot.display_snapshot, + false, + false, + if is_shown { selection.user_name } else { None }, + )); } + + selections.extend(remote_selections.into_values()); } + (selections, active_rows, newest_selection_head) } - fn paint_gutter( - &mut self, - bounds: Bounds, - layout: &mut LayoutState, + #[allow(clippy::too_many_arguments)] + fn layout_folds( + &self, + snapshot: &EditorSnapshot, + content_origin: gpui::Point, + visible_anchor_range: Range, + visible_display_row_range: Range, + scroll_pixel_position: gpui::Point, + line_height: Pixels, + line_layouts: &[LineWithInvisibles], cx: &mut ElementContext, - ) { - let line_height = layout.position_map.line_height; + ) -> Vec { + snapshot + .folds_in_range(visible_anchor_range.clone()) + .filter_map(|fold| { + let fold_range = fold.range.clone(); + let display_range = fold.range.start.to_display_point(&snapshot) + ..fold.range.end.to_display_point(&snapshot); + debug_assert_eq!(display_range.start.row(), display_range.end.row()); + let row = display_range.start.row(); + debug_assert!(row < visible_display_row_range.end); + let line_layout = line_layouts + .get((row - visible_display_row_range.start) as usize) + .map(|l| &l.line)?; - let scroll_position = layout.position_map.snapshot.scroll_position(); - let scroll_top = scroll_position.y * line_height; + let start_x = content_origin.x + + line_layout.x_for_index(display_range.start.column() as usize) + - scroll_pixel_position.x; + let start_y = content_origin.y + row as f32 * line_height - scroll_pixel_position.y; + let end_x = content_origin.x + + line_layout.x_for_index(display_range.end.column() as usize) + - scroll_pixel_position.x; - if bounds.contains(&cx.mouse_position()) { - let stacking_order = cx.stacking_order().clone(); - cx.set_cursor_style(CursorStyle::Arrow, stacking_order); - } + let fold_bounds = Bounds { + origin: point(start_x, start_y), + size: size(end_x - start_x, line_height), + }; - let show_git_gutter = matches!( - ProjectSettings::get_global(cx).git.git_gutter, - Some(GitGutterSetting::TrackedFiles) - ); - - if show_git_gutter { - Self::paint_diff_hunks(bounds, layout, cx); - } - - let gutter_settings = EditorSettings::get_global(cx).gutter; - - for (ix, line) in layout.line_numbers.iter().enumerate() { - if let Some(line) = line { - let line_origin = bounds.origin - + point( - bounds.size.width - line.width - layout.gutter_dimensions.right_padding, - ix as f32 * line_height - (scroll_top % line_height), - ); - - line.paint(line_origin, line_height, cx).log_err(); - } - } - - cx.with_z_index(1, |cx| { - for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { - if let Some(fold_indicator) = fold_indicator { - debug_assert!(gutter_settings.folds); - let mut fold_indicator = fold_indicator.into_any_element(); - let available_space = size( - AvailableSpace::MinContent, - AvailableSpace::Definite(line_height * 0.55), - ); - let fold_indicator_size = fold_indicator.measure(available_space, cx); - - let position = point( - bounds.size.width - layout.gutter_dimensions.right_padding, - ix as f32 * line_height - (scroll_top % line_height), - ); - let centering_offset = point( - (layout.gutter_dimensions.right_padding + layout.gutter_dimensions.margin - - fold_indicator_size.width) - / 2., - (line_height - fold_indicator_size.height) / 2., - ); - let origin = bounds.origin + position + centering_offset; - fold_indicator.draw(origin, available_space, cx); - } - } - - if let Some(indicator) = layout.code_actions_indicator.take() { - debug_assert!(gutter_settings.code_actions); - let mut button = indicator.button.into_any_element(); - let available_space = size( - AvailableSpace::MinContent, - AvailableSpace::Definite(line_height), - ); - let indicator_size = button.measure(available_space, cx); - - let mut x = Pixels::ZERO; - let mut y = indicator.row as f32 * line_height - scroll_top; - // Center indicator. - x += (layout.gutter_dimensions.margin + layout.gutter_dimensions.left_padding - - indicator_size.width) - / 2.; - y += (line_height - indicator_size.height) / 2.; - - button.draw(bounds.origin + point(x, y), available_space, cx); - } - }); + let mut hover_element = div() + .id(fold.id) + .size_full() + .cursor_pointer() + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .on_click( + cx.listener_for(&self.editor, move |editor: &mut Editor, _, cx| { + editor.unfold_ranges( + [fold_range.start..fold_range.end], + true, + false, + cx, + ); + cx.stop_propagation(); + }), + ) + .into_any(); + hover_element.layout(fold_bounds.origin, fold_bounds.size.into(), cx); + Some(FoldLayout { + display_range, + hover_element, + }) + }) + .collect() } - fn paint_diff_hunks(bounds: Bounds, layout: &LayoutState, cx: &mut ElementContext) { - let line_height = layout.position_map.line_height; + #[allow(clippy::too_many_arguments)] + fn layout_cursors( + &self, + snapshot: &EditorSnapshot, + selections: &[(PlayerColor, Vec)], + visible_display_row_range: Range, + line_layouts: &[LineWithInvisibles], + text_hitbox: &Hitbox, + content_origin: gpui::Point, + scroll_pixel_position: gpui::Point, + line_height: Pixels, + em_width: Pixels, + cx: &mut ElementContext, + ) -> Vec { + self.editor.update(cx, |editor, cx| { + let mut cursors = Vec::new(); + for (player_color, selections) in selections { + for selection in selections { + let cursor_position = selection.head; + if (selection.is_local && !editor.show_local_cursors(cx)) + || !visible_display_row_range.contains(&cursor_position.row()) + { + continue; + } - let scroll_position = layout.position_map.snapshot.scroll_position(); - let scroll_top = scroll_position.y * line_height; + let cursor_row_layout = &line_layouts + [(cursor_position.row() - visible_display_row_range.start) as usize] + .line; + let cursor_column = cursor_position.column() as usize; - for hunk in &layout.display_hunks { - let (display_row_range, status) = match hunk { - //TODO: This rendering is entirely a horrible hack - &DisplayDiffHunk::Folded { display_row: row } => { - let start_y = row as f32 * line_height - scroll_top; - let end_y = start_y + line_height; - - let width = 0.275 * line_height; - let highlight_origin = bounds.origin + point(-width, start_y); - let highlight_size = size(width * 2., end_y - start_y); - let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad(quad( - highlight_bounds, - Corners::all(1. * line_height), - cx.theme().status().modified, - Edges::default(), - transparent_black(), - )); - - continue; - } - - DisplayDiffHunk::Unfolded { - display_row_range, - status, - } => (display_row_range, status), - }; - - let color = match status { - DiffHunkStatus::Added => cx.theme().status().created, - DiffHunkStatus::Modified => cx.theme().status().modified, - - //TODO: This rendering is entirely a horrible hack - DiffHunkStatus::Removed => { - let row = display_row_range.start; - - let offset = line_height / 2.; - let start_y = row as f32 * line_height - offset - scroll_top; - let end_y = start_y + line_height; - - let width = 0.275 * line_height; - let highlight_origin = bounds.origin + point(-width, start_y); - let highlight_size = size(width * 2., end_y - start_y); - let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad(quad( - highlight_bounds, - Corners::all(1. * line_height), - cx.theme().status().deleted, - Edges::default(), - transparent_black(), - )); - - continue; - } - }; - - let start_row = display_row_range.start; - let end_row = display_row_range.end; - // If we're in a multibuffer, row range span might include an - // excerpt header, so if we were to draw the marker straight away, - // the hunk might include the rows of that header. - // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap. - // Instead, we simply check whether the range we're dealing with includes - // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header. - let end_row_in_current_excerpt = layout - .position_map - .snapshot - .blocks_in_range(start_row..end_row) - .find_map(|(start_row, block)| { - if matches!(block, TransformBlock::ExcerptHeader { .. }) { - Some(start_row) + let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); + let mut block_width = + cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; + if block_width == Pixels::ZERO { + block_width = em_width; + } + let block_text = if let CursorShape::Block = selection.cursor_shape { + snapshot + .chars_at(cursor_position) + .next() + .and_then(|(character, _)| { + let text = if character == '\n' { + SharedString::from(" ") + } else { + SharedString::from(character.to_string()) + }; + let len = text.len(); + cx.text_system() + .shape_line( + text, + cursor_row_layout.font_size, + &[TextRun { + len, + font: self.style.text.font(), + color: self.style.background, + background_color: None, + strikethrough: None, + underline: None, + }], + ) + .log_err() + }) } else { None - } - }) - .unwrap_or(end_row); + }; - let start_y = start_row as f32 * line_height - scroll_top; - let end_y = end_row_in_current_excerpt as f32 * line_height - scroll_top; - - let width = 0.275 * line_height; - let highlight_origin = bounds.origin + point(-width, start_y); - let highlight_size = size(width * 2., end_y - start_y); - let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad(quad( - highlight_bounds, - Corners::all(0.05 * line_height), - color, - Edges::default(), - transparent_black(), - )); - } - } - - fn paint_text( - &mut self, - text_bounds: Bounds, - layout: &mut LayoutState, - cx: &mut ElementContext, - ) { - let start_row = layout.visible_display_row_range.start; - // Offset the content_bounds from the text_bounds by the gutter margin (which is roughly half a character wide) to make hit testing work more like how we want. - let content_origin = - text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO); - let line_end_overshoot = 0.15 * layout.position_map.line_height; - let whitespace_setting = self - .editor - .read(cx) - .buffer - .read(cx) - .settings_at(0, cx) - .show_whitespaces; - - cx.with_content_mask( - Some(ContentMask { - bounds: text_bounds, - }), - |cx| { - let interactive_text_bounds = InteractiveBounds { - bounds: text_bounds, - stacking_order: cx.stacking_order().clone(), - }; - if text_bounds.contains(&cx.mouse_position()) { - if self - .editor - .read(cx) - .hovered_link_state - .as_ref() - .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty()) - { - cx.set_cursor_style( - CursorStyle::PointingHand, - interactive_text_bounds.stacking_order.clone(), - ); - } else { - cx.set_cursor_style( - CursorStyle::IBeam, - interactive_text_bounds.stacking_order.clone(), - ); - } - } - - let fold_corner_radius = 0.15 * layout.position_map.line_height; - cx.with_element_id(Some("folds"), |cx| { - let snapshot = &layout.position_map.snapshot; - - for fold in snapshot.folds_in_range(layout.visible_anchor_range.clone()) { - let fold_range = fold.range.clone(); - let display_range = fold.range.start.to_display_point(&snapshot) - ..fold.range.end.to_display_point(&snapshot); - debug_assert_eq!(display_range.start.row(), display_range.end.row()); - let row = display_range.start.row(); - debug_assert!(row < layout.visible_display_row_range.end); - let Some(line_layout) = &layout - .position_map - .line_layouts - .get((row - layout.visible_display_row_range.start) as usize) - .map(|l| &l.line) - else { - continue; - }; - - let start_x = content_origin.x - + line_layout.x_for_index(display_range.start.column() as usize) - - layout.position_map.scroll_position.x; - let start_y = content_origin.y - + row as f32 * layout.position_map.line_height - - layout.position_map.scroll_position.y; - let end_x = content_origin.x - + line_layout.x_for_index(display_range.end.column() as usize) - - layout.position_map.scroll_position.x; - - let fold_bounds = Bounds { - origin: point(start_x, start_y), - size: size(end_x - start_x, layout.position_map.line_height), - }; - - let fold_background = cx.with_z_index(1, |cx| { - div() - .id(fold.id) - .size_full() - .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) - .on_click(cx.listener_for( - &self.editor, - move |editor: &mut Editor, _, cx| { - editor.unfold_ranges( - [fold_range.start..fold_range.end], - true, - false, - cx, - ); - cx.stop_propagation(); - }, - )) - .draw_and_update_state( - fold_bounds.origin, - fold_bounds.size, - cx, - |fold_element_state, cx| { - if fold_element_state.is_active() { - cx.theme().colors().ghost_element_active - } else if fold_bounds.contains(&cx.mouse_position()) { - cx.theme().colors().ghost_element_hover - } else { - cx.theme().colors().ghost_element_background - } - }, - ) - }); - - self.paint_highlighted_range( - display_range.clone(), - fold_background, - fold_corner_radius, - fold_corner_radius * 2., - layout, - content_origin, - text_bounds, - cx, - ); - } - }); - - for (range, color) in &layout.highlighted_ranges { - self.paint_highlighted_range( - range.clone(), - *color, - Pixels::ZERO, - line_end_overshoot, - layout, - content_origin, - text_bounds, - cx, - ); - } - - let mut cursors = SmallVec::<[Cursor; 32]>::new(); - let corner_radius = 0.15 * layout.position_map.line_height; - let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); - - for (participant_ix, (player_color, selections)) in - layout.selections.iter().enumerate() - { - for selection in selections.into_iter() { - self.paint_highlighted_range( - selection.range.clone(), - player_color.selection, - corner_radius, - corner_radius * 2., - layout, - content_origin, - text_bounds, - cx, - ); - - if selection.is_local && !selection.range.is_empty() { - invisible_display_ranges.push(selection.range.clone()); - } - - if !selection.is_local || self.editor.read(cx).show_local_cursors(cx) { - let cursor_position = selection.head; - if layout - .visible_display_row_range - .contains(&cursor_position.row()) - { - let cursor_row_layout = &layout.position_map.line_layouts - [(cursor_position.row() - start_row) as usize] - .line; - let cursor_column = cursor_position.column() as usize; - - let cursor_character_x = - cursor_row_layout.x_for_index(cursor_column); - let mut block_width = cursor_row_layout - .x_for_index(cursor_column + 1) - - cursor_character_x; - if block_width == Pixels::ZERO { - block_width = layout.position_map.em_width; - } - let block_text = if let CursorShape::Block = selection.cursor_shape - { - layout - .position_map - .snapshot - .chars_at(cursor_position) - .next() - .and_then(|(character, _)| { - let text = if character == '\n' { - SharedString::from(" ") - } else { - SharedString::from(character.to_string()) - }; - let len = text.len(); - cx.text_system() - .shape_line( - text, - cursor_row_layout.font_size, - &[TextRun { - len, - font: self.style.text.font(), - color: self.style.background, - background_color: None, - strikethrough: None, - underline: None, - }], - ) - .log_err() - }) - } else { - None - }; - - let x = cursor_character_x - layout.position_map.scroll_position.x; - let y = cursor_position.row() as f32 - * layout.position_map.line_height - - layout.position_map.scroll_position.y; - if selection.is_newest { - self.editor.update(cx, |editor, _| { - editor.pixel_position_of_newest_cursor = Some(point( - text_bounds.origin.x + x + block_width / 2., - text_bounds.origin.y - + y - + layout.position_map.line_height / 2., - )) - }); - } - - cursors.push(Cursor { - color: player_color.cursor, - block_width, - origin: point(x, y), - line_height: layout.position_map.line_height, - shape: selection.cursor_shape, - block_text, - cursor_name: selection.user_name.clone().map(|name| { - CursorName { - string: name, - color: self.style.background, - is_top_row: cursor_position.row() == 0, - z_index: (participant_ix % 256).try_into().unwrap(), - } - }), - }); - } - } - } - } - - for (ix, line_with_invisibles) in - layout.position_map.line_layouts.iter().enumerate() - { - let row = start_row + ix as u32; - line_with_invisibles.draw( - layout, - row, - content_origin, - whitespace_setting, - &invisible_display_ranges, - cx, - ) - } - - cx.with_z_index(0, |cx| self.paint_redactions(text_bounds, &layout, cx)); - - cx.with_z_index(1, |cx| { - for cursor in cursors { - cursor.paint(content_origin, cx); - } - }); - }, - ) - } - - fn paint_redactions( - &mut self, - text_bounds: Bounds, - layout: &LayoutState, - cx: &mut ElementContext, - ) { - let content_origin = - text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO); - let line_end_overshoot = layout.line_end_overshoot(); - - // A softer than perfect black - let redaction_color = gpui::rgb(0x0e1111); - - for range in layout.redacted_ranges.iter() { - self.paint_highlighted_range( - range.clone(), - redaction_color.into(), - Pixels::ZERO, - line_end_overshoot, - layout, - content_origin, - text_bounds, - cx, - ); - } - } - - fn paint_overlays( - &mut self, - text_bounds: Bounds, - layout: &mut LayoutState, - cx: &mut ElementContext, - ) { - let content_origin = - text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO); - let start_row = layout.visible_display_row_range.start; - if let Some((position, mut context_menu)) = layout.context_menu.take() { - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - let context_menu_size = context_menu.measure(available_space, cx); - - let cursor_row_layout = - &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; - let x = cursor_row_layout.x_for_index(position.column() as usize) - - layout.position_map.scroll_position.x; - let y = (position.row() + 1) as f32 * layout.position_map.line_height - - layout.position_map.scroll_position.y; - let mut list_origin = content_origin + point(x, y); - let list_width = context_menu_size.width; - let list_height = context_menu_size.height; - - // Snap the right edge of the list to the right edge of the window if - // its horizontal bounds overflow. - if list_origin.x + list_width > cx.viewport_size().width { - list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO); - } - - if list_origin.y + list_height > text_bounds.lower_right().y { - list_origin.y -= layout.position_map.line_height + list_height; - } - - cx.break_content_mask(|cx| context_menu.draw(list_origin, available_space, cx)); - } - - if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() { - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - - // This is safe because we check on layout whether the required row is available - let hovered_row_layout = - &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; - - // Minimum required size: Take the first popover, and add 1.5 times the minimum popover - // height. This is the size we will use to decide whether to render popovers above or below - // the hovered line. - let first_size = hover_popovers[0].measure(available_space, cx); - let height_to_reserve = - first_size.height + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height; - - // Compute Hovered Point - let x = hovered_row_layout.x_for_index(position.column() as usize) - - layout.position_map.scroll_position.x; - let y = position.row() as f32 * layout.position_map.line_height - - layout.position_map.scroll_position.y; - let hovered_point = content_origin + point(x, y); - - if hovered_point.y - height_to_reserve > Pixels::ZERO { - // There is enough space above. Render popovers above the hovered point - let mut current_y = hovered_point.y; - for mut hover_popover in hover_popovers { - let size = hover_popover.measure(available_space, cx); - let mut popover_origin = point(hovered_point.x, current_y - size.height); - - let x_out_of_bounds = - text_bounds.upper_right().x - (popover_origin.x + size.width); - if x_out_of_bounds < Pixels::ZERO { - popover_origin.x = popover_origin.x + x_out_of_bounds; + let x = cursor_character_x - scroll_pixel_position.x; + let y = cursor_position.row() as f32 * line_height - scroll_pixel_position.y; + if selection.is_newest { + editor.pixel_position_of_newest_cursor = Some(point( + text_hitbox.origin.x + x + block_width / 2., + text_hitbox.origin.y + y + line_height / 2., + )) } - if cx.was_top_layer(&popover_origin, cx.stacking_order()) { - cx.break_content_mask(|cx| { - hover_popover.draw(popover_origin, available_space, cx) - }); - } - - current_y = popover_origin.y - HOVER_POPOVER_GAP; - } - } else { - // There is not enough space above. Render popovers below the hovered point - let mut current_y = hovered_point.y + layout.position_map.line_height; - for mut hover_popover in hover_popovers { - let size = hover_popover.measure(available_space, cx); - let mut popover_origin = point(hovered_point.x, current_y); - - let x_out_of_bounds = - text_bounds.upper_right().x - (popover_origin.x + size.width); - if x_out_of_bounds < Pixels::ZERO { - popover_origin.x = popover_origin.x + x_out_of_bounds; - } - - hover_popover.draw(popover_origin, available_space, cx); - - current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP; + let mut cursor = CursorLayout { + color: player_color.cursor, + block_width, + origin: point(x, y), + line_height, + shape: selection.cursor_shape, + block_text, + cursor_name: None, + }; + let cursor_name = selection.user_name.clone().map(|name| CursorName { + string: name, + color: self.style.background, + is_top_row: cursor_position.row() == 0, + }); + cx.with_element_context(|cx| cursor.layout(content_origin, cursor_name, cx)); + cursors.push(cursor); } } - } - - if let Some(mouse_context_menu) = self.editor.read(cx).mouse_context_menu.as_ref() { - let element = overlay() - .position(mouse_context_menu.position) - .child(mouse_context_menu.context_menu.clone()) - .anchor(AnchorCorner::TopLeft) - .snap_to_window(); - element.into_any().draw( - gpui::Point::default(), - size(AvailableSpace::MinContent, AvailableSpace::MinContent), - cx, - ); - } + cursors + }) } - fn scrollbar_left(&self, bounds: &Bounds) -> Pixels { - bounds.upper_right().x - self.style.scrollbar_width - } - - fn paint_scrollbar( - &mut self, + fn layout_scrollbar( + &self, + snapshot: &EditorSnapshot, bounds: Bounds, - layout: &mut LayoutState, + scroll_position: gpui::Point, + line_height: Pixels, + height_in_lines: f32, cx: &mut ElementContext, - ) { - if layout.mode != EditorMode::Full { - return; + ) -> Option { + let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; + let show_scrollbars = match scrollbar_settings.show { + ShowScrollbar::Auto => { + let editor = self.editor.read(cx); + let is_singleton = editor.is_singleton(cx); + // Git + (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) + || + // Selections + (is_singleton && scrollbar_settings.selections && editor.has_background_highlights::()) + || + // Symbols Selections + (is_singleton && scrollbar_settings.symbols_selections && (editor.has_background_highlights::() || editor.has_background_highlights::())) + || + // Diagnostics + (is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics()) + || + // Scrollmanager + editor.scroll_manager.scrollbars_visible() + } + ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(), + ShowScrollbar::Always => true, + ShowScrollbar::Never => false, + }; + if snapshot.mode != EditorMode::Full { + return None; } + let visible_row_range = scroll_position.y..scroll_position.y + height_in_lines; + // If a drag took place after we started dragging the scrollbar, // cancel the scrollbar drag. if cx.has_active_drag() { @@ -1369,430 +931,82 @@ impl EditorElement { }); } - let top = bounds.origin.y; - let bottom = bounds.lower_left().y; - let right = bounds.lower_right().x; - let left = self.scrollbar_left(&bounds); - let row_range = layout.scrollbar_row_range.clone(); - let max_row = layout.max_row as f32 + (row_range.end - row_range.start); + let track_bounds = Bounds::from_corners( + point(self.scrollbar_left(&bounds), bounds.origin.y), + point(bounds.lower_right().x, bounds.lower_left().y), + ); + let scroll_height = snapshot.max_point().row() as f32 + height_in_lines; let mut height = bounds.size.height; let mut first_row_y_offset = px(0.0); // Impose a minimum height on the scrollbar thumb - let row_height = height / max_row; - let min_thumb_height = layout.position_map.line_height; - let thumb_height = (row_range.end - row_range.start) * row_height; + let row_height = height / scroll_height; + let min_thumb_height = line_height; + let thumb_height = height_in_lines * row_height; if thumb_height < min_thumb_height { first_row_y_offset = (min_thumb_height - thumb_height) / 2.0; height -= min_thumb_height - thumb_height; } - let y_for_row = |row: f32| -> Pixels { top + first_row_y_offset + row * row_height }; - - let thumb_top = y_for_row(row_range.start) - first_row_y_offset; - let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset; - let track_bounds = Bounds::from_corners(point(left, top), point(right, bottom)); - let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom)); - - if layout.show_scrollbars { - cx.paint_quad(quad( - track_bounds, - Corners::default(), - cx.theme().colors().scrollbar_track_background, - Edges { - top: Pixels::ZERO, - right: Pixels::ZERO, - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_track_border, - )); - let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; - if layout.is_singleton && scrollbar_settings.selections { - let start_anchor = Anchor::min(); - let end_anchor = Anchor::max(); - let background_ranges = self - .editor - .read(cx) - .background_highlight_row_ranges::( - start_anchor..end_anchor, - &layout.position_map.snapshot, - 50000, - ); - for range in background_ranges { - let start_y = y_for_row(range.start().row() as f32); - let mut end_y = y_for_row(range.end().row() as f32); - if end_y - start_y < px(1.) { - end_y = start_y + px(1.); - } - let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); - cx.paint_quad(quad( - bounds, - Corners::default(), - cx.theme().status().info, - Edges { - top: Pixels::ZERO, - right: px(1.), - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_thumb_border, - )); - } - } - - if layout.is_singleton && scrollbar_settings.symbols_selections { - let selection_ranges = self.editor.read(cx).background_highlights_in_range( - Anchor::min()..Anchor::max(), - &layout.position_map.snapshot, - cx.theme().colors(), - ); - for hunk in selection_ranges { - let start_display = Point::new(hunk.0.start.row(), 0) - .to_display_point(&layout.position_map.snapshot.display_snapshot); - let end_display = Point::new(hunk.0.end.row(), 0) - .to_display_point(&layout.position_map.snapshot.display_snapshot); - let start_y = y_for_row(start_display.row() as f32); - let mut end_y = if hunk.0.start == hunk.0.end { - y_for_row((end_display.row() + 1) as f32) - } else { - y_for_row((end_display.row()) as f32) - }; - - if end_y - start_y < px(1.) { - end_y = start_y + px(1.); - } - let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); - - cx.paint_quad(quad( - bounds, - Corners::default(), - cx.theme().status().info, - Edges { - top: Pixels::ZERO, - right: px(1.), - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_thumb_border, - )); - } - } - - if layout.is_singleton && scrollbar_settings.git_diff { - for hunk in layout - .position_map - .snapshot - .buffer_snapshot - .git_diff_hunks_in_range(0..(max_row.floor() as u32)) - { - let start_display = Point::new(hunk.associated_range.start, 0) - .to_display_point(&layout.position_map.snapshot.display_snapshot); - let end_display = Point::new(hunk.associated_range.end, 0) - .to_display_point(&layout.position_map.snapshot.display_snapshot); - let start_y = y_for_row(start_display.row() as f32); - let mut end_y = if hunk.associated_range.start == hunk.associated_range.end { - y_for_row((end_display.row() + 1) as f32) - } else { - y_for_row((end_display.row()) as f32) - }; - - if end_y - start_y < px(1.) { - end_y = start_y + px(1.); - } - let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); - - let color = match hunk.status() { - DiffHunkStatus::Added => cx.theme().status().created, - DiffHunkStatus::Modified => cx.theme().status().modified, - DiffHunkStatus::Removed => cx.theme().status().deleted, - }; - cx.paint_quad(quad( - bounds, - Corners::default(), - color, - Edges { - top: Pixels::ZERO, - right: px(1.), - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_thumb_border, - )); - } - } - - if layout.is_singleton && scrollbar_settings.diagnostics { - let max_point = layout - .position_map - .snapshot - .display_snapshot - .buffer_snapshot - .max_point(); - - let diagnostics = layout - .position_map - .snapshot - .buffer_snapshot - .diagnostics_in_range::<_, Point>(Point::zero()..max_point, false) - // We want to sort by severity, in order to paint the most severe diagnostics last. - .sorted_by_key(|diagnostic| std::cmp::Reverse(diagnostic.diagnostic.severity)); - - for diagnostic in diagnostics { - let start_display = diagnostic - .range - .start - .to_display_point(&layout.position_map.snapshot.display_snapshot); - let end_display = diagnostic - .range - .end - .to_display_point(&layout.position_map.snapshot.display_snapshot); - let start_y = y_for_row(start_display.row() as f32); - let mut end_y = if diagnostic.range.start == diagnostic.range.end { - y_for_row((end_display.row() + 1) as f32) - } else { - y_for_row((end_display.row()) as f32) - }; - - if end_y - start_y < px(1.) { - end_y = start_y + px(1.); - } - let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); - - let color = match diagnostic.diagnostic.severity { - DiagnosticSeverity::ERROR => cx.theme().status().error, - DiagnosticSeverity::WARNING => cx.theme().status().warning, - DiagnosticSeverity::INFORMATION => cx.theme().status().info, - _ => cx.theme().status().hint, - }; - cx.paint_quad(quad( - bounds, - Corners::default(), - color, - Edges { - top: Pixels::ZERO, - right: px(1.), - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_thumb_border, - )); - } - } - - cx.paint_quad(quad( - thumb_bounds, - Corners::default(), - cx.theme().colors().scrollbar_thumb_background, - Edges { - top: Pixels::ZERO, - right: px(1.), - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_thumb_border, - )); - } - - let interactive_track_bounds = InteractiveBounds { - bounds: track_bounds, - stacking_order: cx.stacking_order().clone(), - }; - let mut mouse_position = cx.mouse_position(); - if track_bounds.contains(&mouse_position) { - cx.set_cursor_style( - CursorStyle::Arrow, - interactive_track_bounds.stacking_order.clone(), - ); - } - - cx.on_mouse_event({ - let editor = self.editor.clone(); - move |event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture { - return; - } - - editor.update(cx, |editor, cx| { - if event.pressed_button == Some(MouseButton::Left) - && editor.scroll_manager.is_dragging_scrollbar() - { - let y = mouse_position.y; - let new_y = event.position.y; - if (track_bounds.top()..track_bounds.bottom()).contains(&y) { - let mut position = editor.scroll_position(cx); - position.y += (new_y - y) * max_row / height; - if position.y < 0.0 { - position.y = 0.0; - } - editor.set_scroll_position(position, cx); - } - - mouse_position = event.position; - cx.stop_propagation(); - } else { - editor.scroll_manager.set_is_dragging_scrollbar(false, cx); - if interactive_track_bounds.visibly_contains(&event.position, cx) { - editor.scroll_manager.show_scrollbar(cx); - } - } - }) - } - }); - - if self.editor.read(cx).scroll_manager.is_dragging_scrollbar() { - cx.on_mouse_event({ - let editor = self.editor.clone(); - move |_: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Capture { - return; - } - - editor.update(cx, |editor, cx| { - editor.scroll_manager.set_is_dragging_scrollbar(false, cx); - cx.stop_propagation(); - }); - } - }); - } else { - cx.on_mouse_event({ - let editor = self.editor.clone(); - move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Capture { - return; - } - - editor.update(cx, |editor, cx| { - if track_bounds.contains(&event.position) { - editor.scroll_manager.set_is_dragging_scrollbar(true, cx); - - let y = event.position.y; - if y < thumb_top || thumb_bottom < y { - let center_row = ((y - top) * max_row / height).round() as u32; - let top_row = center_row - .saturating_sub((row_range.end - row_range.start) as u32 / 2); - let mut position = editor.scroll_position(cx); - position.y = top_row as f32; - editor.set_scroll_position(position, cx); - } else { - editor.scroll_manager.show_scrollbar(cx); - } - - cx.stop_propagation(); - } - }); - } - }); - } + Some(ScrollbarLayout { + hitbox: cx.insert_hitbox(track_bounds, false), + visible_row_range, + height, + scroll_height, + first_row_y_offset, + row_height, + visible: show_scrollbars, + }) } #[allow(clippy::too_many_arguments)] - fn paint_highlighted_range( + fn layout_gutter_fold_indicators( &self, - range: Range, - color: Hsla, - corner_radius: Pixels, - line_end_overshoot: Pixels, - layout: &LayoutState, - content_origin: gpui::Point, - bounds: Bounds, + fold_statuses: Vec>, + line_height: Pixels, + gutter_dimensions: &GutterDimensions, + gutter_settings: crate::editor_settings::Gutter, + scroll_pixel_position: gpui::Point, + gutter_hitbox: &Hitbox, cx: &mut ElementContext, - ) { - let start_row = layout.visible_display_row_range.start; - let end_row = layout.visible_display_row_range.end; - if range.start != range.end { - let row_range = if range.end.column() == 0 { - cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) - } else { - cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) - }; - - let highlighted_range = HighlightedRange { - color, - line_height: layout.position_map.line_height, - corner_radius, - start_y: content_origin.y - + row_range.start as f32 * layout.position_map.line_height - - layout.position_map.scroll_position.y, - lines: row_range - .into_iter() - .map(|row| { - let line_layout = - &layout.position_map.line_layouts[(row - start_row) as usize].line; - HighlightedRangeLine { - start_x: if row == range.start.row() { - content_origin.x - + line_layout.x_for_index(range.start.column() as usize) - - layout.position_map.scroll_position.x - } else { - content_origin.x - layout.position_map.scroll_position.x - }, - end_x: if row == range.end.row() { - content_origin.x - + line_layout.x_for_index(range.end.column() as usize) - - layout.position_map.scroll_position.x - } else { - content_origin.x + line_layout.width + line_end_overshoot - - layout.position_map.scroll_position.x - }, - } - }) - .collect(), - }; - - highlighted_range.paint(bounds, cx); - } - } - - fn paint_blocks( - &mut self, - bounds: Bounds, - layout: &mut LayoutState, - cx: &mut ElementContext, - ) { - let scroll_position = layout.position_map.snapshot.scroll_position(); - let scroll_left = scroll_position.x * layout.position_map.em_width; - let scroll_top = scroll_position.y * layout.position_map.line_height; - - for mut block in layout.blocks.drain(..) { - let mut origin = bounds.origin - + point( - Pixels::ZERO, - block.row as f32 * layout.position_map.line_height - scroll_top, - ); - if !matches!(block.style, BlockStyle::Sticky) { - origin += point(-scroll_left, Pixels::ZERO); - } - block.element.draw(origin, block.available_space, cx); - } - } - - fn column_pixels(&self, column: usize, cx: &WindowContext) -> Pixels { - let style = &self.style; - let font_size = style.text.font_size.to_pixels(cx.rem_size()); - let layout = cx - .text_system() - .shape_line( - SharedString::from(" ".repeat(column)), - font_size, - &[TextRun { - len: column, - font: style.text.font(), - color: Hsla::default(), - background_color: None, - underline: None, - strikethrough: None, - }], + ) -> Vec> { + let mut indicators = self.editor.update(cx, |editor, cx| { + editor.render_fold_indicators( + fold_statuses, + &self.style, + editor.gutter_hovered, + line_height, + gutter_dimensions.margin, + cx, ) - .unwrap(); + }); - layout.width - } + for (ix, fold_indicator) in indicators.iter_mut().enumerate() { + if let Some(fold_indicator) = fold_indicator { + debug_assert!(gutter_settings.folds); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(line_height * 0.55), + ); + let fold_indicator_size = fold_indicator.measure(available_space, cx); - fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels { - let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; - self.column_pixels(digit_count, cx) + let position = point( + gutter_dimensions.width - gutter_dimensions.right_padding, + ix as f32 * line_height - (scroll_pixel_position.y % line_height), + ); + let centering_offset = point( + (gutter_dimensions.right_padding + gutter_dimensions.margin + - fold_indicator_size.width) + / 2., + (line_height - fold_indicator_size.height) / 2., + ); + let origin = gutter_hitbox.origin + position + centering_offset; + fold_indicator.layout(origin, available_space, cx); + } + } + + indicators } //Folds contained in a hunk are ignored apart from shrinking visual size @@ -1818,6 +1032,42 @@ impl EditorElement { .collect() } + fn layout_code_actions_indicator( + &self, + line_height: Pixels, + newest_selection_head: DisplayPoint, + scroll_pixel_position: gpui::Point, + gutter_dimensions: &GutterDimensions, + gutter_hitbox: &Hitbox, + cx: &mut ElementContext, + ) -> Option { + let mut active = false; + let mut button = None; + self.editor.update(cx, |editor, cx| { + active = matches!( + editor.context_menu.read().as_ref(), + Some(crate::ContextMenu::CodeActions(_)) + ); + button = editor.render_code_actions_indicator(&self.style, active, cx); + }); + + let mut button = button?.into_any_element(); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(line_height), + ); + let indicator_size = button.measure(available_space, cx); + + let mut x = Pixels::ZERO; + let mut y = newest_selection_head.row() as f32 * line_height - scroll_pixel_position.y; + // Center indicator. + x += + (gutter_dimensions.margin + gutter_dimensions.left_padding - indicator_size.width) / 2.; + y += (line_height - indicator_size.height) / 2.; + button.layout(gutter_hitbox.origin + point(x, y), available_space, cx); + Some(button) + } + fn calculate_relative_line_numbers( &self, snapshot: &EditorSnapshot, @@ -1868,18 +1118,32 @@ impl EditorElement { relative_rows } - fn shape_line_numbers( + fn layout_line_numbers( &self, rows: Range, active_rows: &BTreeMap, - newest_selection_head: DisplayPoint, - is_singleton: bool, + newest_selection_head: Option, snapshot: &EditorSnapshot, - cx: &ViewContext, + cx: &ElementContext, ) -> ( Vec>, Vec>, ) { + let editor = self.editor.read(cx); + let is_singleton = editor.is_singleton(cx); + let newest_selection_head = newest_selection_head.unwrap_or_else(|| { + let newest = editor.selections.newest::(cx); + SelectionLayout::new( + newest, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + true, + true, + None, + ) + .head + }); let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); let include_line_numbers = EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full; @@ -1955,7 +1219,7 @@ impl EditorElement { rows: Range, line_number_layouts: &[Option], snapshot: &EditorSnapshot, - cx: &mut ViewContext, + cx: &ElementContext, ) -> Vec { if rows.start >= rows.end { return Vec::new(); @@ -1965,7 +1229,7 @@ impl EditorElement { if snapshot.is_empty() { let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); let placeholder_color = cx.theme().colors().text_placeholder; - let placeholder_text = snapshot.placeholder_text(cx); + let placeholder_text = snapshot.placeholder_text(); let placeholder_lines = placeholder_text .as_ref() @@ -2007,487 +1271,21 @@ impl EditorElement { } } - fn compute_layout(&mut self, bounds: Bounds, cx: &mut ElementContext) -> LayoutState { - self.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let style = self.style.clone(); - - let font_id = cx.text_system().resolve_font(&style.text.font()); - let font_size = style.text.font_size.to_pixels(cx.rem_size()); - let line_height = style.text.line_height_in_pixels(cx.rem_size()); - let em_width = cx - .text_system() - .typographic_bounds(font_id, font_size, 'm') - .unwrap() - .size - .width; - let em_advance = cx - .text_system() - .advance(font_id, font_size, 'm') - .unwrap() - .width; - - let gutter_dimensions = snapshot.gutter_dimensions( - font_id, - font_size, - em_width, - self.max_line_number_width(&snapshot, cx), - cx, - ); - - editor.gutter_width = gutter_dimensions.width; - - let text_width = bounds.size.width - gutter_dimensions.width; - let overscroll = size(em_width, px(0.)); - let _snapshot = { - editor.set_visible_line_count(bounds.size.height / line_height, cx); - - let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width; - let wrap_width = match editor.soft_wrap_mode(cx) { - SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, - SoftWrap::EditorWidth => editor_width, - SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), - }; - - if editor.set_wrap_width(Some(wrap_width), cx) { - editor.snapshot(cx) - } else { - snapshot - } - }; - - let wrap_guides = editor - .wrap_guides(cx) - .iter() - .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) - .collect::>(); - - let gutter_size = size(gutter_dimensions.width, bounds.size.height); - let text_size = size(text_width, bounds.size.height); - - let autoscroll_horizontally = - editor.autoscroll_vertically(bounds.size.height, line_height, cx); - let mut snapshot = editor.snapshot(cx); - - let scroll_position = snapshot.scroll_position(); - // The scroll position is a fractional point, the whole number of which represents - // the top of the window in terms of display rows. - let start_row = scroll_position.y as u32; - let height_in_lines = bounds.size.height / line_height; - let max_row = snapshot.max_point().row(); - - // Add 1 to ensure selections bleed off screen - let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row); - - let start_anchor = if start_row == 0 { - Anchor::min() - } else { - snapshot - .buffer_snapshot - .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left)) - }; - let end_anchor = if end_row > max_row { - Anchor::max() - } else { - snapshot - .buffer_snapshot - .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) - }; - - let mut selections: Vec<(PlayerColor, Vec)> = Vec::new(); - let mut active_rows = BTreeMap::new(); - let is_singleton = editor.is_singleton(cx); - - let highlighted_rows = editor.highlighted_display_rows(cx); - let highlighted_ranges = editor.background_highlights_in_range( - start_anchor..end_anchor, - &snapshot.display_snapshot, - cx.theme().colors(), - ); - - let redacted_ranges = editor.redacted_ranges(start_anchor..end_anchor, &snapshot.display_snapshot, cx); - - let mut newest_selection_head = None; - - if editor.show_local_selections { - let mut local_selections: Vec> = editor - .selections - .disjoint_in_range(start_anchor..end_anchor, cx); - local_selections.extend(editor.selections.pending(cx)); - let mut layouts = Vec::new(); - let newest = editor.selections.newest(cx); - for selection in local_selections.drain(..) { - let is_empty = selection.start == selection.end; - let is_newest = selection == newest; - - let layout = SelectionLayout::new( - selection, - editor.selections.line_mode, - editor.cursor_shape, - &snapshot.display_snapshot, - is_newest, - editor.leader_peer_id.is_none(), - None, - ); - if is_newest { - newest_selection_head = Some(layout.head); - } - - for row in cmp::max(layout.active_rows.start, start_row) - ..=cmp::min(layout.active_rows.end, end_row) - { - let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); - *contains_non_empty_selection |= !is_empty; - } - layouts.push(layout); - } - - let player = if editor.read_only(cx) { - cx.theme().players().read_only() - } else { - style.local_player - }; - - selections.push((player, layouts)); - } - - if let Some(collaboration_hub) = &editor.collaboration_hub { - // When following someone, render the local selections in their color. - if let Some(leader_id) = editor.leader_peer_id { - if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) { - if let Some(participant_index) = collaboration_hub - .user_participant_indices(cx) - .get(&collaborator.user_id) - { - if let Some((local_selection_style, _)) = selections.first_mut() { - *local_selection_style = cx - .theme() - .players() - .color_for_participant(participant_index.0); - } - } - } - } - - let mut remote_selections = HashMap::default(); - for selection in snapshot.remote_selections_in_range( - &(start_anchor..end_anchor), - collaboration_hub.as_ref(), - cx, - ) { - let selection_style = if let Some(participant_index) = selection.participant_index { - cx.theme() - .players() - .color_for_participant(participant_index.0) - } else { - cx.theme().players().absent() - }; - - // Don't re-render the leader's selections, since the local selections - // match theirs. - if Some(selection.peer_id) == editor.leader_peer_id { - continue; - } - let key = HoveredCursor{replica_id: selection.replica_id, selection_id: selection.selection.id}; - - let is_shown = editor.show_cursor_names || editor.hovered_cursors.contains_key(&key); - - remote_selections - .entry(selection.replica_id) - .or_insert((selection_style, Vec::new())) - .1 - .push(SelectionLayout::new( - selection.selection, - selection.line_mode, - selection.cursor_shape, - &snapshot.display_snapshot, - false, - false, - if is_shown { - selection.user_name - } else { - None - }, - )); - } - - selections.extend(remote_selections.into_values()); - } - - let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; - let show_scrollbars = match scrollbar_settings.show { - ShowScrollbar::Auto => { - // Git - (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) - || - // Selections - (is_singleton && scrollbar_settings.selections && editor.has_background_highlights::()) - || - // Symbols Selections - (is_singleton && scrollbar_settings.symbols_selections && (editor.has_background_highlights::() || editor.has_background_highlights::())) - || - // Diagnostics - (is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics()) - || - // Scrollmanager - editor.scroll_manager.scrollbars_visible() - } - ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(), - ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - }; - - let head_for_relative = newest_selection_head.unwrap_or_else(|| { - let newest = editor.selections.newest::(cx); - SelectionLayout::new( - newest, - editor.selections.line_mode, - editor.cursor_shape, - &snapshot.display_snapshot, - true, - true, - None, - ) - .head - }); - - let (line_numbers, fold_statuses) = self.shape_line_numbers( - start_row..end_row, - &active_rows, - head_for_relative, - is_singleton, - &snapshot, - cx, - ); - - let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); - - let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines); - - let mut max_visible_line_width = Pixels::ZERO; - let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx); - for line_with_invisibles in &line_layouts { - if line_with_invisibles.line.width > max_visible_line_width { - max_visible_line_width = line_with_invisibles.line.width; - } - } - - let longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx) - .unwrap() - .width; - let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; - - let (scroll_width, blocks) = cx.with_element_context(|cx| { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.layout_blocks( - start_row..end_row, - &snapshot, - bounds.size.width, - scroll_width, - text_width, - &gutter_dimensions, - em_width, - gutter_dimensions.width + gutter_dimensions.margin, - line_height, - &style, - &line_layouts, - editor, - cx, - ) - }) - }); - - let scroll_max = point( - ((scroll_width - text_size.width) / em_width).max(0.0), - max_row as f32, - ); - - let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); - - let autoscrolled = if autoscroll_horizontally { - editor.autoscroll_horizontally( - start_row, - text_size.width, - scroll_width, - em_width, - &line_layouts, - cx, - ) - } else { - false - }; - - if clamped || autoscrolled { - snapshot = editor.snapshot(cx); - } - - let gutter_settings = EditorSettings::get_global(cx).gutter; - - let mut context_menu = None; - let mut code_actions_indicator = None; - if let Some(newest_selection_head) = newest_selection_head { - if (start_row..end_row).contains(&newest_selection_head.row()) { - if editor.context_menu_visible() { - let max_height = cmp::min( - 12. * line_height, - cmp::max( - 3. * line_height, - (bounds.size.height - line_height) / 2., - ) - ); - context_menu = - editor.render_context_menu(newest_selection_head, &self.style, max_height, cx); - } - - let active = matches!( - editor.context_menu.read().as_ref(), - Some(crate::ContextMenu::CodeActions(_)) - ); - - if gutter_settings.code_actions { - code_actions_indicator = editor - .render_code_actions_indicator(&style, active, cx) - .map(|element| CodeActionsIndicator { - row: newest_selection_head.row(), - button: element, - }); - } - } - } - - let visible_rows = start_row..start_row + line_layouts.len() as u32; - let max_size = size( - (120. * em_width) // Default size - .min(bounds.size.width / 2.) // Shrink to half of the editor width - .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters - (16. * line_height) // Default size - .min(bounds.size.height / 2.) // Shrink to half of the editor height - .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines - ); - - let hover = if context_menu.is_some() { - None - } else { - editor.hover_state.render( - &snapshot, - &style, - visible_rows, - max_size, - editor.workspace.as_ref().map(|(w, _)| w.clone()), - cx, - ) - }; - - let editor_view = cx.view().clone(); - let fold_indicators = if gutter_settings.folds { - cx.with_element_context(|cx| { - cx.with_element_id(Some("gutter_fold_indicators"), |_cx| { - editor.render_fold_indicators( - fold_statuses, - &style, - editor.gutter_hovered, - line_height, - gutter_dimensions.margin, - editor_view, - ) - }) - }) - } else { - Vec::new() - }; - - let invisible_symbol_font_size = font_size / 2.; - let tab_invisible = cx - .text_system() - .shape_line( - "→".into(), - invisible_symbol_font_size, - &[TextRun { - len: "→".len(), - font: self.style.text.font(), - color: cx.theme().colors().editor_invisible, - background_color: None, - underline: None, - strikethrough: None, - }], - ) - .unwrap(); - let space_invisible = cx - .text_system() - .shape_line( - "•".into(), - invisible_symbol_font_size, - &[TextRun { - len: "•".len(), - font: self.style.text.font(), - color: cx.theme().colors().editor_invisible, - background_color: None, - underline: None, - strikethrough: None, - }], - ) - .unwrap(); - - LayoutState { - mode: snapshot.mode, - position_map: Arc::new(PositionMap { - size: bounds.size, - scroll_position: point( - scroll_position.x * em_width, - scroll_position.y * line_height, - ), - scroll_max, - line_layouts, - line_height, - em_width, - em_advance, - snapshot, - }), - visible_anchor_range: start_anchor..end_anchor, - visible_display_row_range: start_row..end_row, - wrap_guides, - gutter_size, - gutter_dimensions, - text_size, - scrollbar_row_range, - show_scrollbars, - is_singleton, - max_row, - active_rows, - highlighted_rows, - highlighted_ranges, - redacted_ranges, - line_numbers, - display_hunks, - blocks, - selections, - context_menu, - code_actions_indicator, - fold_indicators, - tab_invisible, - space_invisible, - hover_popovers: hover, - } - }) - } - #[allow(clippy::too_many_arguments)] - fn layout_blocks( + fn build_blocks( &self, rows: Range, snapshot: &EditorSnapshot, - editor_width: Pixels, - scroll_width: Pixels, - text_width: Pixels, + hitbox: &Hitbox, + text_hitbox: &Hitbox, + scroll_width: &mut Pixels, gutter_dimensions: &GutterDimensions, em_width: Pixels, text_x: Pixels, line_height: Pixels, - style: &EditorStyle, line_layouts: &[LineWithInvisibles], - editor: &mut Editor, cx: &mut ElementContext, - ) -> (Pixels, Vec) { + ) -> Vec { let mut block_id = 0; let (fixed_blocks, non_fixed_blocks) = snapshot .blocks_in_range(rows.clone()) @@ -2499,7 +1297,6 @@ impl EditorElement { let render_block = |block: &TransformBlock, available_space: Size, block_id: usize, - editor: &mut Editor, cx: &mut ElementContext| { let mut element = match block { TransformBlock::Custom(block) => { @@ -2513,7 +1310,7 @@ impl EditorElement { .line .x_for_index(align_to.column() as usize) } else { - layout_line(align_to.row(), snapshot, style, cx) + layout_line(align_to.row(), snapshot, &self.style, cx) .unwrap() .x_for_index(align_to.column() as usize) }; @@ -2525,7 +1322,7 @@ impl EditorElement { line_height, em_width, block_id, - max_width: scroll_width.max(text_width), + max_width: text_hitbox.size.width.max(*scroll_width), editor_style: &self.style, }) } @@ -2536,7 +1333,9 @@ impl EditorElement { starts_new_buffer, .. } => { - let include_root = editor + let include_root = self + .editor + .read(cx) .project .as_ref() .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) @@ -2673,8 +1472,7 @@ impl EditorElement { AvailableSpace::MinContent, AvailableSpace::Definite(block.height() as f32 * line_height), ); - let (element, element_size) = - render_block(block, available_space, block_id, editor, cx); + let (element, element_size) = render_block(block, available_space, block_id, cx); block_id += 1; fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); blocks.push(BlockLayout { @@ -2690,17 +1488,19 @@ impl EditorElement { TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky, }; let width = match style { - BlockStyle::Sticky => editor_width, - BlockStyle::Flex => editor_width + BlockStyle::Sticky => hitbox.size.width, + BlockStyle::Flex => hitbox + .size + .width .max(fixed_block_max_width) - .max(gutter_dimensions.width + scroll_width), + .max(gutter_dimensions.width + *scroll_width), BlockStyle::Fixed => unreachable!(), }; let available_space = size( AvailableSpace::Definite(width), AvailableSpace::Definite(block.height() as f32 * line_height), ); - let (element, _) = render_block(block, available_space, block_id, editor, cx); + let (element, _) = render_block(block, available_space, block_id, cx); block_id += 1; blocks.push(BlockLayout { row, @@ -2709,36 +1509,1009 @@ impl EditorElement { style, }); } - ( - scroll_width.max(fixed_block_max_width - gutter_dimensions.width), - blocks, + + *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width); + blocks + } + + fn layout_blocks( + &self, + blocks: &mut Vec, + hitbox: &Hitbox, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + cx: &mut ElementContext, + ) { + for block in blocks { + let mut origin = hitbox.origin + + point( + Pixels::ZERO, + block.row as f32 * line_height - scroll_pixel_position.y, + ); + if !matches!(block.style, BlockStyle::Sticky) { + origin += point(-scroll_pixel_position.x, Pixels::ZERO); + } + block.element.layout(origin, block.available_space, cx); + } + } + + #[allow(clippy::too_many_arguments)] + fn layout_context_menu( + &self, + line_height: Pixels, + hitbox: &Hitbox, + text_hitbox: &Hitbox, + content_origin: gpui::Point, + start_row: u32, + scroll_pixel_position: gpui::Point, + line_layouts: &[LineWithInvisibles], + newest_selection_head: DisplayPoint, + cx: &mut ElementContext, + ) -> bool { + let max_height = cmp::min( + 12. * line_height, + cmp::max(3. * line_height, (hitbox.size.height - line_height) / 2.), + ); + let Some((position, mut context_menu)) = self.editor.update(cx, |editor, cx| { + if editor.context_menu_visible() { + editor.render_context_menu(newest_selection_head, &self.style, max_height, cx) + } else { + None + } + }) else { + return false; + }; + + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + let context_menu_size = context_menu.measure(available_space, cx); + + let cursor_row_layout = &line_layouts[(position.row() - start_row) as usize].line; + let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_pixel_position.x; + let y = (position.row() + 1) as f32 * line_height - scroll_pixel_position.y; + let mut list_origin = content_origin + point(x, y); + let list_width = context_menu_size.width; + let list_height = context_menu_size.height; + + // Snap the right edge of the list to the right edge of the window if + // its horizontal bounds overflow. + if list_origin.x + list_width > cx.viewport_size().width { + list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO); + } + + if list_origin.y + list_height > text_hitbox.lower_right().y { + list_origin.y -= line_height + list_height; + } + + cx.defer_draw(context_menu, list_origin, 1); + true + } + + fn layout_mouse_context_menu(&self, cx: &mut ElementContext) -> Option { + let mouse_context_menu = self.editor.read(cx).mouse_context_menu.as_ref()?; + let mut element = overlay() + .position(mouse_context_menu.position) + .child(mouse_context_menu.context_menu.clone()) + .anchor(AnchorCorner::TopLeft) + .snap_to_window() + .into_any(); + element.layout(gpui::Point::default(), AvailableSpace::min_size(), cx); + Some(element) + } + + #[allow(clippy::too_many_arguments)] + fn layout_hover_popovers( + &self, + snapshot: &EditorSnapshot, + hitbox: &Hitbox, + text_hitbox: &Hitbox, + visible_display_row_range: Range, + content_origin: gpui::Point, + scroll_pixel_position: gpui::Point, + line_layouts: &[LineWithInvisibles], + line_height: Pixels, + em_width: Pixels, + cx: &mut ElementContext, + ) { + let max_size = size( + (120. * em_width) // Default size + .min(hitbox.size.width / 2.) // Shrink to half of the editor width + .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters + (16. * line_height) // Default size + .min(hitbox.size.height / 2.) // Shrink to half of the editor height + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + ); + + let hover_popovers = self.editor.update(cx, |editor, cx| { + editor.hover_state.render( + &snapshot, + &self.style, + visible_display_row_range.clone(), + max_size, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ) + }); + let Some((position, mut hover_popovers)) = hover_popovers else { + return; + }; + + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + + // This is safe because we check on layout whether the required row is available + let hovered_row_layout = + &line_layouts[(position.row() - visible_display_row_range.start) as usize].line; + + // Minimum required size: Take the first popover, and add 1.5 times the minimum popover + // height. This is the size we will use to decide whether to render popovers above or below + // the hovered line. + let first_size = hover_popovers[0].measure(available_space, cx); + let height_to_reserve = first_size.height + 1.5 * MIN_POPOVER_LINE_HEIGHT * line_height; + + // Compute Hovered Point + let x = + hovered_row_layout.x_for_index(position.column() as usize) - scroll_pixel_position.x; + let y = position.row() as f32 * line_height - scroll_pixel_position.y; + let hovered_point = content_origin + point(x, y); + + if hovered_point.y - height_to_reserve > Pixels::ZERO { + // There is enough space above. Render popovers above the hovered point + let mut current_y = hovered_point.y; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = point(hovered_point.x, current_y - size.height); + + let x_out_of_bounds = text_hitbox.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.x = popover_origin.x + x_out_of_bounds; + } + + cx.defer_draw(hover_popover, popover_origin, 2); + + current_y = popover_origin.y - HOVER_POPOVER_GAP; + } + } else { + // There is not enough space above. Render popovers below the hovered point + let mut current_y = hovered_point.y + line_height; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = point(hovered_point.x, current_y); + + let x_out_of_bounds = text_hitbox.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.x = popover_origin.x + x_out_of_bounds; + } + + cx.defer_draw(hover_popover, popover_origin, 2); + + current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP; + } + } + } + + fn paint_background(&self, layout: &EditorLayout, cx: &mut ElementContext) { + cx.paint_layer(layout.hitbox.bounds, |cx| { + let scroll_top = + layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height; + let gutter_bg = cx.theme().colors().editor_gutter_background; + cx.paint_quad(fill(layout.gutter_hitbox.bounds, gutter_bg)); + cx.paint_quad(fill(layout.text_hitbox.bounds, self.style.background)); + + if let EditorMode::Full = layout.mode { + let mut active_rows = layout.active_rows.iter().peekable(); + while let Some((start_row, contains_non_empty_selection)) = active_rows.next() { + let mut end_row = *start_row; + while active_rows.peek().map_or(false, |r| { + *r.0 == end_row + 1 && r.1 == contains_non_empty_selection + }) { + active_rows.next().unwrap(); + end_row += 1; + } + + if !contains_non_empty_selection { + let origin = point( + layout.hitbox.origin.x, + layout.hitbox.origin.y + + (layout.position_map.line_height * *start_row as f32) + - scroll_top, + ); + let size = size( + layout.hitbox.size.width, + layout.position_map.line_height * (end_row - start_row + 1) as f32, + ); + let active_line_bg = cx.theme().colors().editor_active_line_background; + cx.paint_quad(fill(Bounds { origin, size }, active_line_bg)); + } + } + + let mut paint_highlight = + |highlight_row_start: u32, highlight_row_end: u32, color| { + let origin = point( + layout.hitbox.origin.x, + layout.hitbox.origin.y + + (layout.position_map.line_height * highlight_row_start as f32) + - scroll_top, + ); + let size = size( + layout.hitbox.size.width, + layout.position_map.line_height + * (highlight_row_end + 1 - highlight_row_start) as f32, + ); + cx.paint_quad(fill(Bounds { origin, size }, color)); + }; + + let mut last_row = None; + let mut highlight_row_start = 0u32; + let mut highlight_row_end = 0u32; + for (&row, &color) in &layout.highlighted_rows { + let paint = last_row.map_or(false, |(last_row, last_color)| { + last_color != color || last_row + 1 < row + }); + + if paint { + let paint_range_is_unfinished = highlight_row_end == 0; + if paint_range_is_unfinished { + highlight_row_end = row; + last_row = None; + } + paint_highlight(highlight_row_start, highlight_row_end, color); + highlight_row_start = 0; + highlight_row_end = 0; + if !paint_range_is_unfinished { + highlight_row_start = row; + last_row = Some((row, color)); + } + } else { + if last_row.is_none() { + highlight_row_start = row; + } else { + highlight_row_end = row; + } + last_row = Some((row, color)); + } + } + if let Some((row, hsla)) = last_row { + highlight_row_end = row; + paint_highlight(highlight_row_start, highlight_row_end, hsla); + } + + let scroll_left = + layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width; + + for (wrap_position, active) in layout.wrap_guides.iter() { + let x = (layout.text_hitbox.origin.x + + *wrap_position + + layout.position_map.em_width / 2.) + - scroll_left; + + let show_scrollbars = layout + .scrollbar_layout + .as_ref() + .map_or(false, |scrollbar| scrollbar.visible); + if x < layout.text_hitbox.origin.x + || (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds)) + { + continue; + } + + let color = if *active { + cx.theme().colors().editor_active_wrap_guide + } else { + cx.theme().colors().editor_wrap_guide + }; + cx.paint_quad(fill( + Bounds { + origin: point(x, layout.text_hitbox.origin.y), + size: size(px(1.), layout.text_hitbox.size.height), + }, + color, + )); + } + } + }) + } + + fn paint_gutter(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + let line_height = layout.position_map.line_height; + + let scroll_position = layout.position_map.snapshot.scroll_position(); + let scroll_top = scroll_position.y * line_height; + + cx.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox); + + let show_git_gutter = matches!( + ProjectSettings::get_global(cx).git.git_gutter, + Some(GitGutterSetting::TrackedFiles) + ); + + if show_git_gutter { + Self::paint_diff_hunks(layout, cx); + } + + for (ix, line) in layout.line_numbers.iter().enumerate() { + if let Some(line) = line { + let line_origin = layout.gutter_hitbox.origin + + point( + layout.gutter_hitbox.size.width + - line.width + - layout.gutter_dimensions.right_padding, + ix as f32 * line_height - (scroll_top % line_height), + ); + + line.paint(line_origin, line_height, cx).log_err(); + } + } + + cx.paint_layer(layout.gutter_hitbox.bounds, |cx| { + cx.with_element_id(Some("gutter_fold_indicators"), |cx| { + for fold_indicator in layout.fold_indicators.iter_mut().flatten() { + fold_indicator.paint(cx); + } + }); + + if let Some(indicator) = layout.code_actions_indicator.as_mut() { + indicator.paint(cx); + } + }) + } + + fn paint_diff_hunks(layout: &EditorLayout, cx: &mut ElementContext) { + if layout.display_hunks.is_empty() { + return; + } + + let line_height = layout.position_map.line_height; + + let scroll_position = layout.position_map.snapshot.scroll_position(); + let scroll_top = scroll_position.y * line_height; + + cx.paint_layer(layout.gutter_hitbox.bounds, |cx| { + for hunk in &layout.display_hunks { + let (display_row_range, status) = match hunk { + //TODO: This rendering is entirely a horrible hack + &DisplayDiffHunk::Folded { display_row: row } => { + let start_y = row as f32 * line_height - scroll_top; + let end_y = start_y + line_height; + + let width = 0.275 * line_height; + let highlight_origin = layout.gutter_hitbox.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + let highlight_bounds = Bounds::new(highlight_origin, highlight_size); + cx.paint_quad(quad( + highlight_bounds, + Corners::all(1. * line_height), + cx.theme().status().modified, + Edges::default(), + transparent_black(), + )); + + continue; + } + + DisplayDiffHunk::Unfolded { + display_row_range, + status, + } => (display_row_range, status), + }; + + let color = match status { + DiffHunkStatus::Added => cx.theme().status().created, + DiffHunkStatus::Modified => cx.theme().status().modified, + + //TODO: This rendering is entirely a horrible hack + DiffHunkStatus::Removed => { + let row = display_row_range.start; + + let offset = line_height / 2.; + let start_y = row as f32 * line_height - offset - scroll_top; + let end_y = start_y + line_height; + + let width = 0.275 * line_height; + let highlight_origin = layout.gutter_hitbox.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + let highlight_bounds = Bounds::new(highlight_origin, highlight_size); + cx.paint_quad(quad( + highlight_bounds, + Corners::all(1. * line_height), + cx.theme().status().deleted, + Edges::default(), + transparent_black(), + )); + + continue; + } + }; + + let start_row = display_row_range.start; + let end_row = display_row_range.end; + // If we're in a multibuffer, row range span might include an + // excerpt header, so if we were to draw the marker straight away, + // the hunk might include the rows of that header. + // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap. + // Instead, we simply check whether the range we're dealing with includes + // any excerpt headers and if so, we stop painting the diff hunk on the first row of that header. + let end_row_in_current_excerpt = layout + .position_map + .snapshot + .blocks_in_range(start_row..end_row) + .find_map(|(start_row, block)| { + if matches!(block, TransformBlock::ExcerptHeader { .. }) { + Some(start_row) + } else { + None + } + }) + .unwrap_or(end_row); + + let start_y = start_row as f32 * line_height - scroll_top; + let end_y = end_row_in_current_excerpt as f32 * line_height - scroll_top; + + let width = 0.275 * line_height; + let highlight_origin = layout.gutter_hitbox.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + let highlight_bounds = Bounds::new(highlight_origin, highlight_size); + cx.paint_quad(quad( + highlight_bounds, + Corners::all(0.05 * line_height), + color, + Edges::default(), + transparent_black(), + )); + } + }) + } + + fn paint_text(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + cx.with_content_mask( + Some(ContentMask { + bounds: layout.text_hitbox.bounds, + }), + |cx| { + let cursor_style = if self + .editor + .read(cx) + .hovered_link_state + .as_ref() + .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty()) + { + CursorStyle::PointingHand + } else { + CursorStyle::IBeam + }; + cx.set_cursor_style(cursor_style, &layout.text_hitbox); + + cx.with_element_id(Some("folds"), |cx| self.paint_folds(layout, cx)); + let invisible_display_ranges = self.paint_highlights(layout, cx); + self.paint_lines(&invisible_display_ranges, layout, cx); + self.paint_redactions(layout, cx); + self.paint_cursors(layout, cx); + }, ) } - fn paint_scroll_wheel_listener( + fn paint_highlights( &mut self, - interactive_bounds: &InteractiveBounds, - layout: &LayoutState, + layout: &mut EditorLayout, + cx: &mut ElementContext, + ) -> SmallVec<[Range; 32]> { + cx.paint_layer(layout.text_hitbox.bounds, |cx| { + let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); + let line_end_overshoot = 0.15 * layout.position_map.line_height; + for (range, color) in &layout.highlighted_ranges { + self.paint_highlighted_range( + range.clone(), + *color, + Pixels::ZERO, + line_end_overshoot, + layout, + cx, + ); + } + + let corner_radius = 0.15 * layout.position_map.line_height; + + for (player_color, selections) in &layout.selections { + for selection in selections.into_iter() { + self.paint_highlighted_range( + selection.range.clone(), + player_color.selection, + corner_radius, + corner_radius * 2., + layout, + cx, + ); + + if selection.is_local && !selection.range.is_empty() { + invisible_display_ranges.push(selection.range.clone()); + } + } + } + invisible_display_ranges + }) + } + + fn paint_lines( + &mut self, + invisible_display_ranges: &[Range], + layout: &EditorLayout, cx: &mut ElementContext, ) { + let whitespace_setting = self + .editor + .read(cx) + .buffer + .read(cx) + .settings_at(0, cx) + .show_whitespaces; + + for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() { + let row = layout.visible_display_row_range.start + ix as u32; + line_with_invisibles.draw( + layout, + row, + layout.content_origin, + whitespace_setting, + invisible_display_ranges, + cx, + ) + } + } + + fn paint_redactions(&mut self, layout: &EditorLayout, cx: &mut ElementContext) { + if layout.redacted_ranges.is_empty() { + return; + } + + let line_end_overshoot = layout.line_end_overshoot(); + + // A softer than perfect black + let redaction_color = gpui::rgb(0x0e1111); + + cx.paint_layer(layout.text_hitbox.bounds, |cx| { + for range in layout.redacted_ranges.iter() { + self.paint_highlighted_range( + range.clone(), + redaction_color.into(), + Pixels::ZERO, + line_end_overshoot, + layout, + cx, + ); + } + }); + } + + fn paint_cursors(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + cx.paint_layer(layout.text_hitbox.bounds, |cx| { + for cursor in &mut layout.cursors { + cursor.paint(layout.content_origin, cx); + } + }); + } + + fn paint_scrollbar(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + let Some(scrollbar_layout) = layout.scrollbar_layout.as_ref() else { + return; + }; + + let thumb_bounds = scrollbar_layout.thumb_bounds(); + if scrollbar_layout.visible { + cx.paint_layer(scrollbar_layout.hitbox.bounds, |cx| { + cx.paint_quad(quad( + scrollbar_layout.hitbox.bounds, + Corners::default(), + cx.theme().colors().scrollbar_track_background, + Edges { + top: Pixels::ZERO, + right: Pixels::ZERO, + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_track_border, + )); + let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; + let is_singleton = self.editor.read(cx).is_singleton(cx); + if is_singleton && scrollbar_settings.selections { + let start_anchor = Anchor::min(); + let end_anchor = Anchor::max(); + let background_ranges = self + .editor + .read(cx) + .background_highlight_row_ranges::( + start_anchor..end_anchor, + &layout.position_map.snapshot, + 50000, + ); + for range in background_ranges { + let start_y = scrollbar_layout.y_for_row(range.start().row() as f32); + let mut end_y = scrollbar_layout.y_for_row(range.end().row() as f32); + if end_y - start_y < px(1.) { + end_y = start_y + px(1.); + } + let bounds = Bounds::from_corners( + point(scrollbar_layout.hitbox.left(), start_y), + point(scrollbar_layout.hitbox.right(), end_y), + ); + cx.paint_quad(quad( + bounds, + Corners::default(), + cx.theme().status().info, + Edges { + top: Pixels::ZERO, + right: px(1.), + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_thumb_border, + )); + } + } + + if is_singleton && scrollbar_settings.symbols_selections { + let selection_ranges = self.editor.read(cx).background_highlights_in_range( + Anchor::min()..Anchor::max(), + &layout.position_map.snapshot, + cx.theme().colors(), + ); + for hunk in selection_ranges { + let start_display = Point::new(hunk.0.start.row(), 0) + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let end_display = Point::new(hunk.0.end.row(), 0) + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let start_y = scrollbar_layout.y_for_row(start_display.row() as f32); + let mut end_y = if hunk.0.start == hunk.0.end { + scrollbar_layout.y_for_row((end_display.row() + 1) as f32) + } else { + scrollbar_layout.y_for_row(end_display.row() as f32) + }; + + if end_y - start_y < px(1.) { + end_y = start_y + px(1.); + } + let bounds = Bounds::from_corners( + point(scrollbar_layout.hitbox.left(), start_y), + point(scrollbar_layout.hitbox.right(), end_y), + ); + + cx.paint_quad(quad( + bounds, + Corners::default(), + cx.theme().status().info, + Edges { + top: Pixels::ZERO, + right: px(1.), + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_thumb_border, + )); + } + } + + if is_singleton && scrollbar_settings.git_diff { + for hunk in layout + .position_map + .snapshot + .buffer_snapshot + .git_diff_hunks_in_range(0..layout.max_row) + { + let start_display = Point::new(hunk.associated_range.start, 0) + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let end_display = Point::new(hunk.associated_range.end, 0) + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let start_y = scrollbar_layout.y_for_row(start_display.row() as f32); + let mut end_y = if hunk.associated_range.start == hunk.associated_range.end + { + scrollbar_layout.y_for_row((end_display.row() + 1) as f32) + } else { + scrollbar_layout.y_for_row(end_display.row() as f32) + }; + + if end_y - start_y < px(1.) { + end_y = start_y + px(1.); + } + let bounds = Bounds::from_corners( + point(scrollbar_layout.hitbox.left(), start_y), + point(scrollbar_layout.hitbox.right(), end_y), + ); + + let color = match hunk.status() { + DiffHunkStatus::Added => cx.theme().status().created, + DiffHunkStatus::Modified => cx.theme().status().modified, + DiffHunkStatus::Removed => cx.theme().status().deleted, + }; + cx.paint_quad(quad( + bounds, + Corners::default(), + color, + Edges { + top: Pixels::ZERO, + right: px(1.), + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_thumb_border, + )); + } + } + + if is_singleton && scrollbar_settings.diagnostics { + let max_point = layout + .position_map + .snapshot + .display_snapshot + .buffer_snapshot + .max_point(); + + let diagnostics = layout + .position_map + .snapshot + .buffer_snapshot + .diagnostics_in_range::<_, Point>(Point::zero()..max_point, false) + // We want to sort by severity, in order to paint the most severe diagnostics last. + .sorted_by_key(|diagnostic| { + std::cmp::Reverse(diagnostic.diagnostic.severity) + }); + + for diagnostic in diagnostics { + let start_display = diagnostic + .range + .start + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let end_display = diagnostic + .range + .end + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let start_y = scrollbar_layout.y_for_row(start_display.row() as f32); + let mut end_y = if diagnostic.range.start == diagnostic.range.end { + scrollbar_layout.y_for_row((end_display.row() + 1) as f32) + } else { + scrollbar_layout.y_for_row(end_display.row() as f32) + }; + + if end_y - start_y < px(1.) { + end_y = start_y + px(1.); + } + let bounds = Bounds::from_corners( + point(scrollbar_layout.hitbox.left(), start_y), + point(scrollbar_layout.hitbox.right(), end_y), + ); + + let color = match diagnostic.diagnostic.severity { + DiagnosticSeverity::ERROR => cx.theme().status().error, + DiagnosticSeverity::WARNING => cx.theme().status().warning, + DiagnosticSeverity::INFORMATION => cx.theme().status().info, + _ => cx.theme().status().hint, + }; + cx.paint_quad(quad( + bounds, + Corners::default(), + color, + Edges { + top: Pixels::ZERO, + right: px(1.), + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_thumb_border, + )); + } + } + + cx.paint_quad(quad( + thumb_bounds, + Corners::default(), + cx.theme().colors().scrollbar_thumb_background, + Edges { + top: Pixels::ZERO, + right: px(1.), + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_thumb_border, + )); + }); + } + + cx.set_cursor_style(CursorStyle::Arrow, &scrollbar_layout.hitbox); + + let scroll_height = scrollbar_layout.scroll_height; + let height = scrollbar_layout.height; + let row_range = scrollbar_layout.visible_row_range.clone(); + + cx.on_mouse_event({ + let editor = self.editor.clone(); + let hitbox = scrollbar_layout.hitbox.clone(); + let mut mouse_position = cx.mouse_position(); + move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { + return; + } + + editor.update(cx, |editor, cx| { + if event.pressed_button == Some(MouseButton::Left) + && editor.scroll_manager.is_dragging_scrollbar() + { + let y = mouse_position.y; + let new_y = event.position.y; + if (hitbox.top()..hitbox.bottom()).contains(&y) { + let mut position = editor.scroll_position(cx); + position.y += (new_y - y) * scroll_height / height; + if position.y < 0.0 { + position.y = 0.0; + } + editor.set_scroll_position(position, cx); + } + + mouse_position = event.position; + cx.stop_propagation(); + } else { + editor.scroll_manager.set_is_dragging_scrollbar(false, cx); + if hitbox.is_hovered(cx) { + editor.scroll_manager.show_scrollbar(cx); + } + } + }) + } + }); + + if self.editor.read(cx).scroll_manager.is_dragging_scrollbar() { + cx.on_mouse_event({ + let editor = self.editor.clone(); + move |_: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Capture { + return; + } + + editor.update(cx, |editor, cx| { + editor.scroll_manager.set_is_dragging_scrollbar(false, cx); + cx.stop_propagation(); + }); + } + }); + } else { + cx.on_mouse_event({ + let editor = self.editor.clone(); + let hitbox = scrollbar_layout.hitbox.clone(); + move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Capture || !hitbox.is_hovered(cx) { + return; + } + + editor.update(cx, |editor, cx| { + editor.scroll_manager.set_is_dragging_scrollbar(true, cx); + + let y = event.position.y; + if y < thumb_bounds.top() || thumb_bounds.bottom() < y { + let center_row = + ((y - hitbox.top()) * scroll_height / height).round() as u32; + let top_row = center_row + .saturating_sub((row_range.end - row_range.start) as u32 / 2); + let mut position = editor.scroll_position(cx); + position.y = top_row as f32; + editor.set_scroll_position(position, cx); + } else { + editor.scroll_manager.show_scrollbar(cx); + } + + cx.stop_propagation(); + }); + } + }); + } + } + + #[allow(clippy::too_many_arguments)] + fn paint_highlighted_range( + &self, + range: Range, + color: Hsla, + corner_radius: Pixels, + line_end_overshoot: Pixels, + layout: &EditorLayout, + cx: &mut ElementContext, + ) { + let start_row = layout.visible_display_row_range.start; + let end_row = layout.visible_display_row_range.end; + if range.start != range.end { + let row_range = if range.end.column() == 0 { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) + } else { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) + }; + + let highlighted_range = HighlightedRange { + color, + line_height: layout.position_map.line_height, + corner_radius, + start_y: layout.content_origin.y + + row_range.start as f32 * layout.position_map.line_height + - layout.position_map.scroll_pixel_position.y, + lines: row_range + .into_iter() + .map(|row| { + let line_layout = + &layout.position_map.line_layouts[(row - start_row) as usize].line; + HighlightedRangeLine { + start_x: if row == range.start.row() { + layout.content_origin.x + + line_layout.x_for_index(range.start.column() as usize) + - layout.position_map.scroll_pixel_position.x + } else { + layout.content_origin.x + - layout.position_map.scroll_pixel_position.x + }, + end_x: if row == range.end.row() { + layout.content_origin.x + + line_layout.x_for_index(range.end.column() as usize) + - layout.position_map.scroll_pixel_position.x + } else { + layout.content_origin.x + line_layout.width + line_end_overshoot + - layout.position_map.scroll_pixel_position.x + }, + } + }) + .collect(), + }; + + highlighted_range.paint(layout.text_hitbox.bounds, cx); + } + } + + fn paint_folds(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + if layout.folds.is_empty() { + return; + } + + cx.paint_layer(layout.text_hitbox.bounds, |cx| { + let fold_corner_radius = 0.15 * layout.position_map.line_height; + for mut fold in mem::take(&mut layout.folds) { + fold.hover_element.paint(cx); + + let hover_element = fold.hover_element.downcast_mut::>().unwrap(); + let fold_background = if hover_element.interactivity().active.unwrap() { + cx.theme().colors().ghost_element_active + } else if hover_element.interactivity().hovered.unwrap() { + cx.theme().colors().ghost_element_hover + } else { + cx.theme().colors().ghost_element_background + }; + + self.paint_highlighted_range( + fold.display_range.clone(), + fold_background, + fold_corner_radius, + fold_corner_radius * 2., + layout, + cx, + ); + } + }) + } + + fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + for mut block in layout.blocks.drain(..) { + block.element.paint(cx); + } + } + + fn paint_mouse_context_menu(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() { + mouse_context_menu.paint(cx); + } + } + + fn paint_scroll_wheel_listener(&mut self, layout: &EditorLayout, cx: &mut ElementContext) { cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); - let interactive_bounds = interactive_bounds.clone(); + let hitbox = layout.hitbox.clone(); let mut delta = ScrollDelta::default(); move |event: &ScrollWheelEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && interactive_bounds.visibly_contains(&event.position, cx) - { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { delta = delta.coalesce(event.delta); editor.update(cx, |editor, cx| { - let position = event.position; let position_map: &PositionMap = &position_map; - let bounds = &interactive_bounds; - if !bounds.visibly_contains(&position, cx) { - return; - } let line_height = position_map.line_height; let max_glyph_width = position_map.em_width; @@ -2770,45 +2543,30 @@ impl EditorElement { }); } - fn paint_mouse_listeners( - &mut self, - bounds: Bounds, - gutter_bounds: Bounds, - text_bounds: Bounds, - layout: &LayoutState, - cx: &mut ElementContext, - ) { - let interactive_bounds = InteractiveBounds { - bounds: bounds.intersect(&cx.content_mask().bounds), - stacking_order: cx.stacking_order().clone(), - }; - - self.paint_scroll_wheel_listener(&interactive_bounds, layout, cx); + fn paint_mouse_listeners(&mut self, layout: &EditorLayout, cx: &mut ElementContext) { + self.paint_scroll_wheel_listener(layout, cx); cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); - let stacking_order = cx.stacking_order().clone(); - let interactive_bounds = interactive_bounds.clone(); + let text_hitbox = layout.text_hitbox.clone(); + let gutter_hitbox = layout.gutter_hitbox.clone(); move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && interactive_bounds.visibly_contains(&event.position, cx) - { + if phase == DispatchPhase::Bubble { match event.button { MouseButton::Left => editor.update(cx, |editor, cx| { Self::mouse_left_down( editor, event, &position_map, - text_bounds, - gutter_bounds, - &stacking_order, + &text_hitbox, + &gutter_hitbox, cx, ); }), MouseButton::Right => editor.update(cx, |editor, cx| { - Self::mouse_right_down(editor, event, &position_map, text_bounds, cx); + Self::mouse_right_down(editor, event, &position_map, &text_hitbox, cx); }), _ => {} }; @@ -2817,23 +2575,14 @@ impl EditorElement { }); cx.on_mouse_event({ - let position_map = layout.position_map.clone(); let editor = self.editor.clone(); - let stacking_order = cx.stacking_order().clone(); - let interactive_bounds = interactive_bounds.clone(); + let position_map = layout.position_map.clone(); + let text_hitbox = layout.text_hitbox.clone(); move |event: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Bubble { editor.update(cx, |editor, cx| { - Self::mouse_up( - editor, - event, - &position_map, - text_bounds, - &interactive_bounds, - &stacking_order, - cx, - ) + Self::mouse_up(editor, event, &position_map, &text_hitbox, cx) }); } } @@ -2841,11 +2590,10 @@ impl EditorElement { cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); - let stacking_order = cx.stacking_order().clone(); + let text_hitbox = layout.text_hitbox.clone(); + let gutter_hitbox = layout.gutter_hitbox.clone(); move |event: &MouseMoveEvent, phase, cx| { - // if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) { - if phase == DispatchPhase::Bubble { editor.update(cx, |editor, cx| { if event.pressed_button == Some(MouseButton::Left) { @@ -2853,29 +2601,55 @@ impl EditorElement { editor, event, &position_map, - text_bounds, - gutter_bounds, - &stacking_order, + text_hitbox.bounds, cx, ) } - if interactive_bounds.visibly_contains(&event.position, cx) { - Self::mouse_moved( - editor, - event, - &position_map, - text_bounds, - gutter_bounds, - &stacking_order, - cx, - ) - } + Self::mouse_moved( + editor, + event, + &position_map, + &text_hitbox, + &gutter_hitbox, + cx, + ) }); } } }); } + + fn scrollbar_left(&self, bounds: &Bounds) -> Pixels { + bounds.upper_right().x - self.style.scrollbar_width + } + + fn column_pixels(&self, column: usize, cx: &WindowContext) -> Pixels { + let style = &self.style; + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let layout = cx + .text_system() + .shape_line( + SharedString::from(" ".repeat(column)), + font_size, + &[TextRun { + len: column, + font: style.text.font(), + color: Hsla::default(), + background_color: None, + underline: None, + strikethrough: None, + }], + ) + .unwrap(); + + layout.width + } + + fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels { + let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; + self.column_pixels(digit_count, cx) + } } #[derive(Debug)] @@ -2995,7 +2769,7 @@ impl LineWithInvisibles { fn draw( &self, - layout: &LayoutState, + layout: &EditorLayout, row: u32, content_origin: gpui::Point, whitespace_setting: ShowWhitespaceSetting, @@ -3003,11 +2777,11 @@ impl LineWithInvisibles { cx: &mut ElementContext, ) { let line_height = layout.position_map.line_height; - let line_y = line_height * row as f32 - layout.position_map.scroll_position.y; + let line_y = line_height * row as f32 - layout.position_map.scroll_pixel_position.y; self.line .paint( - content_origin + gpui::point(-layout.position_map.scroll_position.x, line_y), + content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y), line_height, cx, ) @@ -3029,7 +2803,7 @@ impl LineWithInvisibles { fn draw_invisibles( &self, selection_ranges: &[Range], - layout: &LayoutState, + layout: &EditorLayout, content_origin: gpui::Point, line_y: Pixels, row: u32, @@ -3054,7 +2828,7 @@ impl LineWithInvisibles { (layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0; let origin = content_origin + gpui::point( - x_offset + invisible_offset - layout.position_map.scroll_position.x, + x_offset + invisible_offset - layout.position_map.scroll_pixel_position.x, line_y, ); @@ -3079,57 +2853,448 @@ enum Invisible { } impl Element for EditorElement { - type State = (); + type BeforeLayout = (); + type AfterLayout = EditorLayout; - fn request_layout( + fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, ()) { + self.editor.update(cx, |editor, cx| { + editor.set_style(self.style.clone(), cx); + + let layout_id = match editor.mode { + EditorMode::SingleLine => { + let rem_size = cx.rem_size(); + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); + cx.with_element_context(|cx| cx.request_layout(&style, None)) + } + EditorMode::AutoHeight { max_lines } => { + let editor_handle = cx.view().clone(); + let max_line_number_width = + self.max_line_number_width(&editor.snapshot(cx), cx); + cx.with_element_context(|cx| { + cx.request_measured_layout( + Style::default(), + move |known_dimensions, _, cx| { + editor_handle + .update(cx, |editor, cx| { + compute_auto_height_layout( + editor, + max_lines, + max_line_number_width, + known_dimensions, + cx, + ) + }) + .unwrap_or_default() + }, + ) + }) + } + EditorMode::Full => { + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + cx.with_element_context(|cx| cx.request_layout(&style, None)) + } + }; + + (layout_id, ()) + }) + } + + fn after_layout( &mut self, - _element_state: Option, - cx: &mut gpui::ElementContext, - ) -> (gpui::LayoutId, Self::State) { - cx.with_view_id(self.editor.entity_id(), |cx| { - self.editor.update(cx, |editor, cx| { - editor.set_style(self.style.clone(), cx); + bounds: Bounds, + _: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) -> Self::AfterLayout { + let text_style = TextStyleRefinement { + font_size: Some(self.style.text.font_size), + line_height: Some(self.style.text.line_height), + ..Default::default() + }; + cx.with_text_style(Some(text_style), |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + let mut snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx)); + let style = self.style.clone(); - let layout_id = match editor.mode { - EditorMode::SingleLine => { - let rem_size = cx.rem_size(); - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); - cx.with_element_context(|cx| cx.request_layout(&style, None)) - } - EditorMode::AutoHeight { max_lines } => { - let editor_handle = cx.view().clone(); - let max_line_number_width = - self.max_line_number_width(&editor.snapshot(cx), cx); - cx.with_element_context(|cx| { - cx.request_measured_layout( - Style::default(), - move |known_dimensions, _, cx| { - editor_handle - .update(cx, |editor, cx| { - compute_auto_height_layout( - editor, - max_lines, - max_line_number_width, - known_dimensions, - cx, - ) - }) - .unwrap_or_default() - }, - ) - }) - } - EditorMode::Full => { - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = relative(1.).into(); - cx.with_element_context(|cx| cx.request_layout(&style, None)) + let font_id = cx.text_system().resolve_font(&style.text.font()); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let line_height = style.text.line_height_in_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + let em_advance = cx + .text_system() + .advance(font_id, font_size, 'm') + .unwrap() + .width; + + let gutter_dimensions = snapshot.gutter_dimensions( + font_id, + font_size, + em_width, + self.max_line_number_width(&snapshot, cx), + cx, + ); + let text_width = bounds.size.width - gutter_dimensions.width; + let overscroll = size(em_width, px(0.)); + + snapshot = self.editor.update(cx, |editor, cx| { + editor.gutter_width = gutter_dimensions.width; + editor.set_visible_line_count(bounds.size.height / line_height, cx); + + let editor_width = + text_width - gutter_dimensions.margin - overscroll.width - em_width; + let wrap_width = match editor.soft_wrap_mode(cx) { + SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, + SoftWrap::EditorWidth => editor_width, + SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), + }; + + if editor.set_wrap_width(Some(wrap_width), cx) { + editor.snapshot(cx) + } else { + snapshot } + }); + + let wrap_guides = self + .editor + .read(cx) + .wrap_guides(cx) + .iter() + .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) + .collect::>(); + + let hitbox = cx.insert_hitbox(bounds, false); + let gutter_hitbox = cx.insert_hitbox( + Bounds { + origin: bounds.origin, + size: size(gutter_dimensions.width, bounds.size.height), + }, + false, + ); + let text_hitbox = cx.insert_hitbox( + Bounds { + origin: gutter_hitbox.upper_right(), + size: size(text_width, bounds.size.height), + }, + false, + ); + // Offset the content_bounds from the text_bounds by the gutter margin (which + // is roughly half a character wide) to make hit testing work more like how we want. + let content_origin = + text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO); + + let autoscroll_horizontally = self.editor.update(cx, |editor, cx| { + let autoscroll_horizontally = + editor.autoscroll_vertically(bounds.size.height, line_height, cx); + snapshot = editor.snapshot(cx); + autoscroll_horizontally + }); + + let mut scroll_position = snapshot.scroll_position(); + // The scroll position is a fractional point, the whole number of which represents + // the top of the window in terms of display rows. + let start_row = scroll_position.y as u32; + let height_in_lines = bounds.size.height / line_height; + let max_row = snapshot.max_point().row(); + + // Add 1 to ensure selections bleed off screen + let end_row = + 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row); + + let start_anchor = if start_row == 0 { + Anchor::min() + } else { + snapshot.buffer_snapshot.anchor_before( + DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left), + ) + }; + let end_anchor = if end_row > max_row { + Anchor::max() + } else { + snapshot.buffer_snapshot.anchor_before( + DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right), + ) }; - (layout_id, ()) + let highlighted_rows = self + .editor + .update(cx, |editor, cx| editor.highlighted_display_rows(cx)); + let highlighted_ranges = self.editor.read(cx).background_highlights_in_range( + start_anchor..end_anchor, + &snapshot.display_snapshot, + cx.theme().colors(), + ); + + let redacted_ranges = self.editor.read(cx).redacted_ranges( + start_anchor..end_anchor, + &snapshot.display_snapshot, + cx, + ); + + let (selections, active_rows, newest_selection_head) = self.layout_selections( + start_anchor, + end_anchor, + &snapshot, + start_row, + end_row, + cx, + ); + + let (line_numbers, fold_statuses) = self.layout_line_numbers( + start_row..end_row, + &active_rows, + newest_selection_head, + &snapshot, + cx, + ); + + let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); + + let mut max_visible_line_width = Pixels::ZERO; + let line_layouts = + self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx); + for line_with_invisibles in &line_layouts { + if line_with_invisibles.line.width > max_visible_line_width { + max_visible_line_width = line_with_invisibles.line.width; + } + } + + let longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx) + .unwrap() + .width; + let mut scroll_width = + longest_line_width.max(max_visible_line_width) + overscroll.width; + let mut blocks = self.build_blocks( + start_row..end_row, + &snapshot, + &hitbox, + &text_hitbox, + &mut scroll_width, + &gutter_dimensions, + em_width, + gutter_dimensions.width + gutter_dimensions.margin, + line_height, + &line_layouts, + cx, + ); + + let scroll_max = point( + ((scroll_width - text_hitbox.size.width) / em_width).max(0.0), + max_row as f32, + ); + + self.editor.update(cx, |editor, cx| { + let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); + + let autoscrolled = if autoscroll_horizontally { + editor.autoscroll_horizontally( + start_row, + text_hitbox.size.width, + scroll_width, + em_width, + &line_layouts, + cx, + ) + } else { + false + }; + + if clamped || autoscrolled { + snapshot = editor.snapshot(cx); + scroll_position = snapshot.scroll_position(); + } + }); + + let scroll_pixel_position = point( + scroll_position.x * em_width, + scroll_position.y * line_height, + ); + + cx.with_element_id(Some("blocks"), |cx| { + self.layout_blocks( + &mut blocks, + &hitbox, + line_height, + scroll_pixel_position, + cx, + ); + }); + + let cursors = self.layout_cursors( + &snapshot, + &selections, + start_row..end_row, + &line_layouts, + &text_hitbox, + content_origin, + scroll_pixel_position, + line_height, + em_width, + cx, + ); + + let scrollbar_layout = self.layout_scrollbar( + &snapshot, + bounds, + scroll_position, + line_height, + height_in_lines, + cx, + ); + + let folds = cx.with_element_id(Some("folds"), |cx| { + self.layout_folds( + &snapshot, + content_origin, + start_anchor..end_anchor, + start_row..end_row, + scroll_pixel_position, + line_height, + &line_layouts, + cx, + ) + }); + + let gutter_settings = EditorSettings::get_global(cx).gutter; + + let mut context_menu_visible = false; + let mut code_actions_indicator = None; + if let Some(newest_selection_head) = newest_selection_head { + if (start_row..end_row).contains(&newest_selection_head.row()) { + context_menu_visible = self.layout_context_menu( + line_height, + &hitbox, + &text_hitbox, + content_origin, + start_row, + scroll_pixel_position, + &line_layouts, + newest_selection_head, + cx, + ); + if gutter_settings.code_actions { + code_actions_indicator = self.layout_code_actions_indicator( + line_height, + newest_selection_head, + scroll_pixel_position, + &gutter_dimensions, + &gutter_hitbox, + cx, + ); + } + } + } + + if !context_menu_visible && !cx.has_active_drag() { + self.layout_hover_popovers( + &snapshot, + &hitbox, + &text_hitbox, + start_row..end_row, + content_origin, + scroll_pixel_position, + &line_layouts, + line_height, + em_width, + cx, + ); + } + + let mouse_context_menu = self.layout_mouse_context_menu(cx); + + let fold_indicators = if gutter_settings.folds { + cx.with_element_id(Some("gutter_fold_indicators"), |cx| { + self.layout_gutter_fold_indicators( + fold_statuses, + line_height, + &gutter_dimensions, + gutter_settings, + scroll_pixel_position, + &gutter_hitbox, + cx, + ) + }) + } else { + Vec::new() + }; + + let invisible_symbol_font_size = font_size / 2.; + let tab_invisible = cx + .text_system() + .shape_line( + "→".into(), + invisible_symbol_font_size, + &[TextRun { + len: "→".len(), + font: self.style.text.font(), + color: cx.theme().colors().editor_invisible, + background_color: None, + underline: None, + strikethrough: None, + }], + ) + .unwrap(); + let space_invisible = cx + .text_system() + .shape_line( + "•".into(), + invisible_symbol_font_size, + &[TextRun { + len: "•".len(), + font: self.style.text.font(), + color: cx.theme().colors().editor_invisible, + background_color: None, + underline: None, + strikethrough: None, + }], + ) + .unwrap(); + + EditorLayout { + mode: snapshot.mode, + position_map: Arc::new(PositionMap { + size: bounds.size, + scroll_pixel_position, + scroll_max, + line_layouts, + line_height, + em_width, + em_advance, + snapshot, + }), + visible_display_row_range: start_row..end_row, + wrap_guides, + hitbox, + text_hitbox, + gutter_hitbox, + gutter_dimensions, + content_origin, + scrollbar_layout, + max_row, + active_rows, + highlighted_rows, + highlighted_ranges, + redacted_ranges, + line_numbers, + display_hunks, + folds, + blocks, + cursors, + selections, + mouse_context_menu, + code_actions_indicator, + fold_indicators, + tab_invisible, + space_invisible, + } }) }) } @@ -3137,73 +3302,46 @@ impl Element for EditorElement { fn paint( &mut self, bounds: Bounds, - _element_state: &mut Self::State, - cx: &mut gpui::ElementContext, + _: &mut Self::BeforeLayout, + layout: &mut Self::AfterLayout, + cx: &mut ElementContext, ) { - let editor = self.editor.clone(); + let focus_handle = self.editor.focus_handle(cx); + let key_context = self.editor.read(cx).key_context(cx); + cx.set_focus_handle(&focus_handle); + cx.set_key_context(key_context); + cx.set_view_id(self.editor.entity_id()); + cx.handle_input( + &focus_handle, + ElementInputHandler::new(bounds, self.editor.clone()), + ); + self.register_actions(cx); + self.register_key_listeners(cx, layout); - cx.paint_view(self.editor.entity_id(), |cx| { - cx.with_text_style( - Some(gpui::TextStyleRefinement { - font_size: Some(self.style.text.font_size), - line_height: Some(self.style.text.line_height), - ..Default::default() - }), - |cx| { - let mut layout = self.compute_layout(bounds, cx); - let gutter_bounds = Bounds { - origin: bounds.origin, - size: layout.gutter_size, - }; - let text_bounds = Bounds { - origin: gutter_bounds.upper_right(), - size: layout.text_size, - }; + let text_style = TextStyleRefinement { + font_size: Some(self.style.text.font_size), + line_height: Some(self.style.text.line_height), + ..Default::default() + }; + cx.with_text_style(Some(text_style), |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + self.paint_mouse_listeners(layout, cx); - let focus_handle = editor.focus_handle(cx); - let key_context = self.editor.read(cx).key_context(cx); - cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| { - self.register_actions(cx); + self.paint_background(layout, cx); + if layout.gutter_hitbox.size.width > Pixels::ZERO { + self.paint_gutter(layout, cx); + } + self.paint_text(layout, cx); - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - self.register_key_listeners(cx, text_bounds, &layout); - cx.handle_input( - &focus_handle, - ElementInputHandler::new(bounds, self.editor.clone()), - ); + if !layout.blocks.is_empty() { + cx.with_element_id(Some("blocks"), |cx| { + self.paint_blocks(layout, cx); + }); + } - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &mut layout, cx); - } - self.paint_text(text_bounds, &mut layout, cx); - - cx.with_z_index(0, |cx| { - self.paint_mouse_listeners( - bounds, - gutter_bounds, - text_bounds, - &layout, - cx, - ); - }); - if !layout.blocks.is_empty() { - cx.with_z_index(0, |cx| { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.paint_blocks(bounds, &mut layout, cx); - }); - }) - } - - cx.with_z_index(1, |cx| { - self.paint_overlays(text_bounds, &mut layout, cx); - }); - - cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx)); - }); - }) - }, - ) + self.paint_scrollbar(layout, cx); + self.paint_mouse_context_menu(layout, cx); + }); }) } } @@ -3211,10 +3349,6 @@ impl Element for EditorElement { impl IntoElement for EditorElement { type Element = Self; - fn element_id(&self) -> Option { - self.editor.element_id() - } - fn into_element(self) -> Self::Element { self } @@ -3222,50 +3356,75 @@ impl IntoElement for EditorElement { type BufferRow = u32; -pub struct LayoutState { +pub struct EditorLayout { position_map: Arc, - gutter_size: Size, + hitbox: Hitbox, + text_hitbox: Hitbox, + gutter_hitbox: Hitbox, gutter_dimensions: GutterDimensions, - text_size: gpui::Size, + content_origin: gpui::Point, + scrollbar_layout: Option, mode: EditorMode, wrap_guides: SmallVec<[(Pixels, bool); 2]>, - visible_anchor_range: Range, visible_display_row_range: Range, active_rows: BTreeMap, highlighted_rows: BTreeMap, line_numbers: Vec>, display_hunks: Vec, + folds: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, redacted_ranges: Vec>, + cursors: Vec, selections: Vec<(PlayerColor, Vec)>, - scrollbar_row_range: Range, - show_scrollbars: bool, - is_singleton: bool, max_row: u32, - context_menu: Option<(DisplayPoint, AnyElement)>, - code_actions_indicator: Option, - hover_popovers: Option<(DisplayPoint, Vec)>, - fold_indicators: Vec>, + code_actions_indicator: Option, + fold_indicators: Vec>, + mouse_context_menu: Option, tab_invisible: ShapedLine, space_invisible: ShapedLine, } -impl LayoutState { +impl EditorLayout { fn line_end_overshoot(&self) -> Pixels { 0.15 * self.position_map.line_height } } -struct CodeActionsIndicator { - row: u32, - button: IconButton, +struct ScrollbarLayout { + hitbox: Hitbox, + visible_row_range: Range, + visible: bool, + height: Pixels, + scroll_height: f32, + first_row_y_offset: Pixels, + row_height: Pixels, +} + +impl ScrollbarLayout { + fn thumb_bounds(&self) -> Bounds { + let thumb_top = self.y_for_row(self.visible_row_range.start) - self.first_row_y_offset; + let thumb_bottom = self.y_for_row(self.visible_row_range.end) + self.first_row_y_offset; + Bounds::from_corners( + point(self.hitbox.left(), thumb_top), + point(self.hitbox.right(), thumb_bottom), + ) + } + + fn y_for_row(&self, row: f32) -> Pixels { + self.hitbox.top() + self.first_row_y_offset + row * self.row_height + } +} + +struct FoldLayout { + display_range: Range, + hover_element: AnyElement, } struct PositionMap { size: Size, line_height: Pixels, - scroll_position: gpui::Point, + scroll_pixel_position: gpui::Point, scroll_max: gpui::Point, em_width: Pixels, em_advance: Pixels, @@ -3370,15 +3529,14 @@ fn layout_line( ) } -#[derive(Debug)] -pub struct Cursor { +pub struct CursorLayout { origin: gpui::Point, block_width: Pixels, line_height: Pixels, color: Hsla, shape: CursorShape, block_text: Option, - cursor_name: Option, + cursor_name: Option, } #[derive(Debug)] @@ -3386,10 +3544,9 @@ pub struct CursorName { string: SharedString, color: Hsla, is_top_row: bool, - z_index: u16, } -impl Cursor { +impl CursorLayout { pub fn new( origin: gpui::Point, block_width: Pixels, @@ -3397,16 +3554,15 @@ impl Cursor { color: Hsla, shape: CursorShape, block_text: Option, - cursor_name: Option, - ) -> Cursor { - Cursor { + ) -> CursorLayout { + CursorLayout { origin, block_width, line_height, color, shape, block_text, - cursor_name, + cursor_name: None, } } @@ -3417,8 +3573,8 @@ impl Cursor { } } - pub fn paint(&self, origin: gpui::Point, cx: &mut ElementContext) { - let bounds = match self.shape { + fn bounds(&self, origin: gpui::Point) -> Bounds { + match self.shape { CursorShape::Bar => Bounds { origin: self.origin + origin, size: size(px(2.0), self.line_height), @@ -3433,7 +3589,45 @@ impl Cursor { + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)), size: size(self.block_width, px(2.0)), }, - }; + } + } + + pub fn layout( + &mut self, + origin: gpui::Point, + cursor_name: Option, + cx: &mut ElementContext, + ) { + if let Some(cursor_name) = cursor_name { + let bounds = self.bounds(origin); + let text_size = self.line_height / 1.5; + + let name_origin = if cursor_name.is_top_row { + point(bounds.right() - px(1.), bounds.top()) + } else { + point(bounds.left(), bounds.top() - text_size / 2. - px(1.)) + }; + let mut name_element = div() + .bg(self.color) + .text_size(text_size) + .px_0p5() + .line_height(text_size + px(2.)) + .text_color(cursor_name.color) + .child(cursor_name.string.clone()) + .into_any_element(); + + name_element.layout( + name_origin, + size(AvailableSpace::MinContent, AvailableSpace::MinContent), + cx, + ); + + self.cursor_name = Some(name_element); + } + } + + pub fn paint(&mut self, origin: gpui::Point, cx: &mut ElementContext) { + let bounds = self.bounds(origin); //Draw background or border quad let cursor = if matches!(self.shape, CursorShape::Hollow) { @@ -3442,29 +3636,8 @@ impl Cursor { fill(bounds, self.color) }; - if let Some(name) = &self.cursor_name { - let text_size = self.line_height / 1.5; - - let name_origin = if name.is_top_row { - point(bounds.right() - px(1.), bounds.top()) - } else { - point(bounds.left(), bounds.top() - text_size / 2. - px(1.)) - }; - cx.with_z_index(name.z_index, |cx| { - div() - .bg(self.color) - .text_size(text_size) - .px_0p5() - .line_height(text_size + px(2.)) - .text_color(name.color) - .child(name.string.clone()) - .into_any_element() - .draw( - name_origin, - size(AvailableSpace::MinContent, AvailableSpace::MinContent), - cx, - ) - }) + if let Some(name) = &mut self.cursor_name { + name.paint(cx); } cx.paint_quad(cursor); @@ -3650,20 +3823,21 @@ mod tests { let editor = window.root(cx).unwrap(); let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); let element = EditorElement::new(&editor, style); + let snapshot = window.update(cx, |editor, cx| editor.snapshot(cx)).unwrap(); - let layouts = window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - element - .shape_line_numbers( - 0..6, - &Default::default(), - DisplayPoint::new(0, 0), - false, - &snapshot, - cx, - ) - .0 + let layouts = cx + .update_window(*window, |_, cx| { + cx.with_element_context(|cx| { + element + .layout_line_numbers( + 0..6, + &Default::default(), + Some(DisplayPoint::new(0, 0)), + &snapshot, + cx, + ) + .0 + }) }) .unwrap(); assert_eq!(layouts.len(), 6); @@ -3733,17 +3907,16 @@ mod tests { }) .unwrap(); let state = cx - .update_window(window.into(), |view, cx| { + .update_window(window.into(), |_view, cx| { cx.with_element_context(|cx| { - cx.with_view_id(view.entity_id(), |cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) - }) + element.after_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + &mut (), + cx, + ) }) }) .unwrap(); @@ -3829,17 +4002,16 @@ mod tests { }); let state = cx - .update_window(window.into(), |view, cx| { + .update_window(window.into(), |_view, cx| { cx.with_element_context(|cx| { - cx.with_view_id(view.entity_id(), |cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) - }) + element.after_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + &mut (), + cx, + ) }) }) .unwrap(); @@ -3895,21 +4067,19 @@ mod tests { let mut element = EditorElement::new(&editor, style); let state = cx - .update_window(window.into(), |view, cx| { + .update_window(window.into(), |_view, cx| { cx.with_element_context(|cx| { - cx.with_view_id(view.entity_id(), |cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) - }) + element.after_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + &mut (), + cx, + ) }) }) .unwrap(); - let size = state.position_map.size; assert_eq!(state.position_map.line_layouts.len(), 4); assert_eq!( @@ -3920,13 +4090,6 @@ mod tests { .collect::>(), &[false, false, false, true] ); - - // Don't panic. - let bounds = Bounds::::new(Default::default(), size); - cx.update_window(window.into(), |_, cx| { - cx.with_element_context(|cx| element.paint(bounds, &mut (), cx)) - }) - .unwrap() } #[gpui::test] @@ -4102,11 +4265,12 @@ mod tests { let layout_state = cx .update_window(window.into(), |_, cx| { cx.with_element_context(|cx| { - element.compute_layout( + element.after_layout( Bounds { origin: point(px(500.), px(500.)), size: size(px(500.), px(500.)), }, + &mut (), cx, ) }) diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index ae15d79ce1..51db071bcd 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -1,7 +1,7 @@ use crate::{ - element::PointForPosition, hover_popover::{self, InlayHover}, - Anchor, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, SelectPhase, + Anchor, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, PointForPosition, + SelectPhase, }; use gpui::{px, AsyncWindowContext, Model, Modifiers, Task, ViewContext}; use language::{Bias, ToOffset}; diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index fd0e145c37..02be847f6d 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -499,9 +499,10 @@ impl InfoPopover { .overflow_y_scroll() .max_w(max_size.width) .max_h(max_size.height) - // Prevent a mouse move on the popover from being propagated to the editor, + // Prevent a mouse down/move on the popover from being propagated to the editor, // because that would dismiss the popover. .on_mouse_move(|_, cx| cx.stop_propagation()) + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) .child(crate::render_parsed_markdown( "content", &self.parsed_content, @@ -563,6 +564,7 @@ impl DiagnosticPopover { div() .id("diagnostic") + .block() .elevation_2(cx) .overflow_y_scroll() .px_2() @@ -602,11 +604,10 @@ mod tests { use super::*; use crate::{ editor_tests::init_test, - element::PointForPosition, hover_links::update_inlay_link_and_hover_points, inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, test::editor_lsp_test_context::EditorLspTestContext, - InlayId, + InlayId, PointForPosition, }; use collections::BTreeSet; use gpui::{FontWeight, HighlightStyle, UnderlineStyle}; diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index d3337db258..f8d096db54 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -6,7 +6,7 @@ use crate::{ DisplayPoint, Editor, EditorMode, MultiBuffer, }; -use gpui::{Context, Model, Pixels, ViewContext}; +use gpui::{Context, Font, FontFeatures, FontStyle, FontWeight, Model, Pixels, ViewContext}; use project::Project; use util::test::{marked_text_offsets, marked_text_ranges}; @@ -26,7 +26,12 @@ pub fn marked_display_snapshot( ) -> (DisplaySnapshot, Vec) { let (unmarked_text, markers) = marked_text_offsets(text); - let font = cx.text_style().font(); + let font = Font { + family: "Courier".into(), + features: FontFeatures::default(), + weight: FontWeight::default(), + style: FontStyle::default(), + }; let font_size: Pixels = 14usize.into(); let buffer = MultiBuffer::build_simple(&unmarked_text, cx); diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 13d381ed99..8562859800 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -6,10 +6,9 @@ use editor::{Editor, EditorElement, EditorStyle}; use extension::{ExtensionApiResponse, ExtensionManifest, ExtensionStatus, ExtensionStore}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, canvas, uniform_list, AnyElement, AppContext, AvailableSpace, EventEmitter, - FocusableView, FontStyle, FontWeight, InteractiveElement, KeyContext, ParentElement, Render, - Styled, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WhiteSpace, - WindowContext, + actions, canvas, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle, + FontWeight, InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WhiteSpace, WindowContext, }; use settings::Settings; use std::ops::DerefMut; @@ -729,12 +728,12 @@ impl Render for ExtensionsPage { return this.py_4().child(self.render_empty_state(cx)); } + let view = cx.view().clone(); + let scroll_handle = self.list.clone(); this.child( - canvas({ - let view = cx.view().clone(); - let scroll_handle = self.list.clone(); + canvas( move |bounds, cx| { - uniform_list::<_, ExtensionCard, _>( + let mut list = uniform_list::<_, ExtensionCard, _>( view, "entries", count, @@ -743,14 +742,12 @@ impl Render for ExtensionsPage { .size_full() .pb_4() .track_scroll(scroll_handle) - .into_any_element() - .draw( - bounds.origin, - bounds.size.map(AvailableSpace::Definite), - cx, - ) - } - }) + .into_any_element(); + list.layout(bounds.origin, bounds.size.into(), cx); + list + }, + |_bounds, mut list, cx| list.paint(cx), + ) .size_full(), ) })) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0591a07866..5c7db5d92d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -20,7 +20,6 @@ pub use async_context::*; use collections::{FxHashMap, FxHashSet, VecDeque}; pub use entity_map::*; pub use model_context::*; -use refineable::Refineable; #[cfg(any(test, feature = "test-support"))] pub use test_context::*; use util::{ @@ -34,8 +33,8 @@ use crate::{ DispatchPhase, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, SharedString, SubscriberSet, - Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, - Window, WindowAppearance, WindowContext, WindowHandle, WindowId, + Subscription, SvgRenderer, Task, TextSystem, View, ViewContext, Window, WindowAppearance, + WindowContext, WindowHandle, WindowId, }; mod async_context; @@ -216,7 +215,6 @@ pub struct AppContext { pub(crate) svg_renderer: SvgRenderer, asset_source: Arc, pub(crate) image_cache: ImageCache, - pub(crate) text_style_stack: Vec, pub(crate) globals_by_type: FxHashMap>, pub(crate) entities: EntityMap, pub(crate) new_view_observers: SubscriberSet, @@ -278,7 +276,6 @@ impl AppContext { svg_renderer: SvgRenderer::new(asset_source.clone()), asset_source, image_cache: ImageCache::new(http_client), - text_style_stack: Vec::new(), globals_by_type: FxHashMap::default(), entities, new_view_observers: SubscriberSet::new(), @@ -829,15 +826,6 @@ impl AppContext { &self.text_system } - /// The current text style. Which is composed of all the style refinements provided to `with_text_style`. - pub fn text_style(&self) -> TextStyle { - let mut style = TextStyle::default(); - for refinement in &self.text_style_stack { - style.refine(refinement); - } - style - } - /// Check whether a global of the given type has been assigned. pub fn has_global(&self) -> bool { self.globals_by_type.contains_key(&TypeId::of::()) @@ -1021,14 +1009,6 @@ impl AppContext { inner(&mut self.keystroke_observers, Box::new(f)) } - pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) { - self.text_style_stack.push(text_style); - } - - pub(crate) fn pop_text_style(&mut self) { - self.text_style_stack.pop(); - } - /// Register key bindings. pub fn bind_keys(&mut self, bindings: impl IntoIterator) { self.keymap.borrow_mut().add_bindings(bindings); @@ -1127,16 +1107,19 @@ impl AppContext { /// Checks if the given action is bound in the current context, as defined by the app's current focus, /// the bindings in the element tree, and any global action listeners. pub fn is_action_available(&mut self, action: &dyn Action) -> bool { + let mut action_available = false; if let Some(window) = self.active_window() { if let Ok(window_action_available) = window.update(self, |_, cx| cx.is_action_available(action)) { - return window_action_available; + action_available = window_action_available; } } - self.global_action_listeners - .contains_key(&action.as_any().type_id()) + action_available + || self + .global_action_listeners + .contains_key(&action.as_any().type_id()) } /// Sets the menu bar for this application. This will replace any existing menu bar. @@ -1152,14 +1135,41 @@ impl AppContext { .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) .log_err(); } else { - self.propagate_event = true; + self.dispatch_global_action(action); + } + } + pub(crate) fn dispatch_global_action(&mut self, action: &dyn Action) { + self.propagate_event = true; + + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in &global_listeners { + listener(action.as_any(), DispatchPhase::Capture, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + + if self.propagate_event { if let Some(mut global_listeners) = self .global_action_listeners .remove(&action.as_any().type_id()) { - for listener in &global_listeners { - listener(action.as_any(), DispatchPhase::Capture, self); + for listener in global_listeners.iter().rev() { + listener(action.as_any(), DispatchPhase::Bubble, self); if !self.propagate_event { break; } @@ -1174,29 +1184,6 @@ impl AppContext { self.global_action_listeners .insert(action.as_any().type_id(), global_listeners); } - - if self.propagate_event { - if let Some(mut global_listeners) = self - .global_action_listeners - .remove(&action.as_any().type_id()) - { - for listener in global_listeners.iter().rev() { - listener(action.as_any(), DispatchPhase::Bubble, self); - if !self.propagate_event { - break; - } - } - - global_listeners.extend( - self.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - self.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } - } } } diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 2b7d170e04..62f82f9cbd 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -674,17 +674,10 @@ impl VisualTestContext { f: impl FnOnce(&mut WindowContext) -> AnyElement, ) { self.update(|cx| { - let entity_id = cx - .window - .root_view - .as_ref() - .expect("Can't draw to this window without a root view") - .entity_id(); - cx.with_element_context(|cx| { - cx.with_view_id(entity_id, |cx| { - f(cx).draw(origin, space, cx); - }) + let mut element = f(cx); + element.layout(origin, space, cx); + element.paint(cx); }); cx.refresh(); diff --git a/crates/gpui/src/bounds_tree.rs b/crates/gpui/src/bounds_tree.rs new file mode 100644 index 0000000000..789dc6e50f --- /dev/null +++ b/crates/gpui/src/bounds_tree.rs @@ -0,0 +1,292 @@ +use crate::{Bounds, Half}; +use std::{ + cmp, + fmt::Debug, + ops::{Add, Sub}, +}; + +#[derive(Debug)] +pub(crate) struct BoundsTree +where + U: Default + Clone + Debug, +{ + root: Option, + nodes: Vec>, + stack: Vec, +} + +impl BoundsTree +where + U: Clone + Debug + PartialOrd + Add + Sub + Half + Default, +{ + pub fn clear(&mut self) { + self.root = None; + self.nodes.clear(); + self.stack.clear(); + } + + pub fn insert(&mut self, new_bounds: Bounds) -> u32 { + // If the tree is empty, make the root the new leaf. + if self.root.is_none() { + let new_node = self.push_leaf(new_bounds, 1); + self.root = Some(new_node); + return 1; + } + + // Search for the best place to add the new leaf based on heuristics. + let mut max_intersecting_ordering = 0; + let mut index = self.root.unwrap(); + while let Node::Internal { + left, + right, + bounds: node_bounds, + .. + } = &mut self.nodes[index] + { + let left = *left; + let right = *right; + *node_bounds = node_bounds.union(&new_bounds); + self.stack.push(index); + + // Descend to the best-fit child, based on which one would increase + // the surface area the least. This attempts to keep the tree balanced + // in terms of surface area. If there is an intersection with the other child, + // add its keys to the intersections vector. + let left_cost = new_bounds + .union(&self.nodes[left].bounds()) + .half_perimeter(); + let right_cost = new_bounds + .union(&self.nodes[right].bounds()) + .half_perimeter(); + if left_cost < right_cost { + max_intersecting_ordering = + self.find_max_ordering(right, &new_bounds, max_intersecting_ordering); + index = left; + } else { + max_intersecting_ordering = + self.find_max_ordering(left, &new_bounds, max_intersecting_ordering); + index = right; + } + } + + // We've found a leaf ('index' now refers to a leaf node). + // We'll insert a new parent node above the leaf and attach our new leaf to it. + let sibling = index; + + // Check for collision with the located leaf node + let Node::Leaf { + bounds: sibling_bounds, + order: sibling_ordering, + .. + } = &self.nodes[index] + else { + unreachable!(); + }; + if sibling_bounds.intersects(&new_bounds) { + max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering); + } + + let ordering = max_intersecting_ordering + 1; + let new_node = self.push_leaf(new_bounds, ordering); + let new_parent = self.push_internal(sibling, new_node); + + // If there was an old parent, we need to update its children indices. + if let Some(old_parent) = self.stack.last().copied() { + let Node::Internal { left, right, .. } = &mut self.nodes[old_parent] else { + unreachable!(); + }; + + if *left == sibling { + *left = new_parent; + } else { + *right = new_parent; + } + } else { + // If the old parent was the root, the new parent is the new root. + self.root = Some(new_parent); + } + + for node_index in self.stack.drain(..) { + let Node::Internal { + max_order: max_ordering, + .. + } = &mut self.nodes[node_index] + else { + unreachable!() + }; + *max_ordering = cmp::max(*max_ordering, ordering); + } + + ordering + } + + fn find_max_ordering(&self, index: usize, bounds: &Bounds, mut max_ordering: u32) -> u32 { + match &self.nodes[index] { + Node::Leaf { + bounds: node_bounds, + order: ordering, + .. + } => { + if bounds.intersects(node_bounds) { + max_ordering = cmp::max(*ordering, max_ordering); + } + } + Node::Internal { + left, + right, + bounds: node_bounds, + max_order: node_max_ordering, + .. + } => { + if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering { + let left_max_ordering = self.nodes[*left].max_ordering(); + let right_max_ordering = self.nodes[*right].max_ordering(); + if left_max_ordering > right_max_ordering { + max_ordering = self.find_max_ordering(*left, bounds, max_ordering); + max_ordering = self.find_max_ordering(*right, bounds, max_ordering); + } else { + max_ordering = self.find_max_ordering(*right, bounds, max_ordering); + max_ordering = self.find_max_ordering(*left, bounds, max_ordering); + } + } + } + } + max_ordering + } + + fn push_leaf(&mut self, bounds: Bounds, order: u32) -> usize { + self.nodes.push(Node::Leaf { bounds, order }); + self.nodes.len() - 1 + } + + fn push_internal(&mut self, left: usize, right: usize) -> usize { + let left_node = &self.nodes[left]; + let right_node = &self.nodes[right]; + let new_bounds = left_node.bounds().union(right_node.bounds()); + let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering()); + self.nodes.push(Node::Internal { + bounds: new_bounds, + left, + right, + max_order: max_ordering, + }); + self.nodes.len() - 1 + } +} + +impl Default for BoundsTree +where + U: Default + Clone + Debug, +{ + fn default() -> Self { + BoundsTree { + root: None, + nodes: Vec::new(), + stack: Vec::new(), + } + } +} + +#[derive(Debug, Clone)] +enum Node +where + U: Clone + Default + Debug, +{ + Leaf { + bounds: Bounds, + order: u32, + }, + Internal { + left: usize, + right: usize, + bounds: Bounds, + max_order: u32, + }, +} + +impl Node +where + U: Clone + Default + Debug, +{ + fn bounds(&self) -> &Bounds { + match self { + Node::Leaf { bounds, .. } => bounds, + Node::Internal { bounds, .. } => bounds, + } + } + + fn max_ordering(&self) -> u32 { + match self { + Node::Leaf { + order: ordering, .. + } => *ordering, + Node::Internal { + max_order: max_ordering, + .. + } => *max_ordering, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Bounds, Point, Size}; + + #[test] + fn test_insert() { + let mut tree = BoundsTree::::default(); + let bounds1 = Bounds { + origin: Point { x: 0.0, y: 0.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds2 = Bounds { + origin: Point { x: 5.0, y: 5.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds3 = Bounds { + origin: Point { x: 10.0, y: 10.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + + // Insert the bounds into the tree and verify the order is correct + assert_eq!(tree.insert(bounds1), 1); + assert_eq!(tree.insert(bounds2), 2); + assert_eq!(tree.insert(bounds3), 3); + + // Insert non-overlapping bounds and verify they can reuse orders + let bounds4 = Bounds { + origin: Point { x: 20.0, y: 20.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds5 = Bounds { + origin: Point { x: 40.0, y: 40.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds6 = Bounds { + origin: Point { x: 25.0, y: 25.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + assert_eq!(tree.insert(bounds4), 1); // bounds4 does not overlap with bounds1, bounds2, or bounds3 + assert_eq!(tree.insert(bounds5), 1); // bounds5 does not overlap with any other bounds + assert_eq!(tree.insert(bounds6), 2); // bounds6 overlaps with bounds4, so it should have a different order + } +} diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index dd343387ba..ccb4a6249d 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -15,9 +15,6 @@ //! //! But some state is too simple and voluminous to store in every view that needs it, e.g. //! whether a hover has been started or not. For this, GPUI provides the [`Element::State`], associated type. -//! If an element returns an [`ElementId`] from [`IntoElement::element_id()`], and that element id -//! appears in the same place relative to other views and ElementIds in the frame, then the previous -//! frame's state will be passed to the element's layout and paint methods. //! //! # Implementing your own elements //! @@ -35,33 +32,48 @@ //! your own custom layout algorithm or rendering a code editor. use crate::{ - util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, ElementContext, ElementId, LayoutId, - Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA, + util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementContext, + ElementId, LayoutId, Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA, }; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; -use std::{any::Any, fmt::Debug, ops::DerefMut}; +use std::{any::Any, fmt::Debug, mem, ops::DerefMut}; /// Implemented by types that participate in laying out and painting the contents of a window. /// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy. /// You can create custom elements by implementing this trait, see the module-level documentation /// for more details. pub trait Element: 'static + IntoElement { - /// The type of state to store for this element between frames. See the module-level documentation - /// for details. - type State: 'static; + /// The type of state returned from [`Element::before_layout`]. A mutable reference to this state is subsequently + /// provided to [`Element::after_layout`] and [`Element::paint`]. + type BeforeLayout: 'static; + + /// The type of state returned from [`Element::after_layout`]. A mutable reference to this state is subsequently + /// provided to [`Element::paint`]. + type AfterLayout: 'static; /// Before an element can be painted, we need to know where it's going to be and how big it is. /// Use this method to request a layout from Taffy and initialize the element's state. - fn request_layout( + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout); + + /// After laying out an element, we need to commit its bounds to the current frame for hitbox + /// purposes. The state argument is the same state that was returned from [`Element::before_layout()`]. + fn after_layout( &mut self, - state: Option, + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, cx: &mut ElementContext, - ) -> (LayoutId, Self::State); + ) -> Self::AfterLayout; /// Once layout has been completed, this method will be called to paint the element to the screen. - /// The state argument is the same state that was returned from [`Element::request_layout()`]. - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext); + /// The state argument is the same state that was returned from [`Element::before_layout()`]. + fn paint( + &mut self, + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, + after_layout: &mut Self::AfterLayout, + cx: &mut ElementContext, + ); /// Convert this element into a dynamically-typed [`AnyElement`]. fn into_any(self) -> AnyElement { @@ -75,10 +87,6 @@ pub trait IntoElement: Sized { /// Useful for converting other types into elements automatically, like Strings type Element: Element; - /// The [`ElementId`] of self once converted into an [`Element`]. - /// If present, the resulting element's state will be carried across frames. - fn element_id(&self) -> Option; - /// Convert self into a type that implements [`Element`]. fn into_element(self) -> Self::Element; @@ -86,41 +94,6 @@ pub trait IntoElement: Sized { fn into_any_element(self) -> AnyElement { self.into_element().into_any() } - - /// Convert into an element, then draw in the current window at the given origin. - /// The available space argument is provided to the layout engine to determine the size of the - // root element. Once the element is drawn, its associated element state is yielded to the - // given callback. - fn draw_and_update_state( - self, - origin: Point, - available_space: Size, - cx: &mut ElementContext, - f: impl FnOnce(&mut ::State, &mut ElementContext) -> R, - ) -> R - where - T: Clone + Default + Debug + Into, - { - let element = self.into_element(); - let element_id = element.element_id(); - let element = DrawableElement { - element: Some(element), - phase: ElementDrawPhase::Start, - }; - - let frame_state = - DrawableElement::draw(element, origin, available_space.map(Into::into), cx); - - if let Some(mut frame_state) = frame_state { - f(&mut frame_state, cx) - } else { - cx.with_element_state(element_id.unwrap(), |element_state, cx| { - let mut element_state = element_state.unwrap(); - let result = f(&mut element_state, cx); - (result, element_state) - }) - } - } } impl FluentBuilder for T {} @@ -188,24 +161,36 @@ impl Component { } impl Element for Component { - type State = AnyElement; + type BeforeLayout = AnyElement; + type AfterLayout = (); - fn request_layout( - &mut self, - _: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { let mut element = self .0 .take() .unwrap() .render(cx.deref_mut()) .into_any_element(); - let layout_id = element.request_layout(cx); + let layout_id = element.before_layout(cx); (layout_id, element) } - fn paint(&mut self, _: Bounds, element: &mut Self::State, cx: &mut ElementContext) { + fn after_layout( + &mut self, + _: Bounds, + element: &mut AnyElement, + cx: &mut ElementContext, + ) { + element.after_layout(cx); + } + + fn paint( + &mut self, + _: Bounds, + element: &mut Self::BeforeLayout, + _: &mut Self::AfterLayout, + cx: &mut ElementContext, + ) { element.paint(cx) } } @@ -213,10 +198,6 @@ impl Element for Component { impl IntoElement for Component { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self } @@ -227,9 +208,11 @@ impl IntoElement for Component { pub(crate) struct GlobalElementId(SmallVec<[ElementId; 32]>); trait ElementObject { - fn element_id(&self) -> Option; + fn inner_element(&mut self) -> &mut dyn Any; - fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId; + fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId; + + fn after_layout(&mut self, cx: &mut ElementContext); fn paint(&mut self, cx: &mut ElementContext); @@ -238,110 +221,102 @@ trait ElementObject { available_space: Size, cx: &mut ElementContext, ) -> Size; - - fn draw( - &mut self, - origin: Point, - available_space: Size, - cx: &mut ElementContext, - ); } /// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. -pub(crate) struct DrawableElement { - element: Option, - phase: ElementDrawPhase, +pub struct Drawable { + /// The drawn element. + pub element: E, + phase: ElementDrawPhase, } #[derive(Default)] -enum ElementDrawPhase { +enum ElementDrawPhase { #[default] Start, - LayoutRequested { + BeforeLayout { layout_id: LayoutId, - frame_state: Option, + before_layout: BeforeLayout, }, LayoutComputed { layout_id: LayoutId, available_space: Size, - frame_state: Option, + before_layout: BeforeLayout, }, + AfterLayout { + node_id: DispatchNodeId, + bounds: Bounds, + before_layout: BeforeLayout, + after_layout: AfterLayout, + }, + Painted, } /// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. -impl DrawableElement { +impl Drawable { fn new(element: E) -> Self { - DrawableElement { - element: Some(element), + Drawable { + element, phase: ElementDrawPhase::Start, } } - fn element_id(&self) -> Option { - self.element.as_ref()?.element_id() + fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId { + match mem::take(&mut self.phase) { + ElementDrawPhase::Start => { + let (layout_id, before_layout) = self.element.before_layout(cx); + self.phase = ElementDrawPhase::BeforeLayout { + layout_id, + before_layout, + }; + layout_id + } + _ => panic!("must call before_layout only once"), + } } - fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { - let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id() - { - let layout_id = cx.with_element_state(id, |element_state, cx| { - self.element - .as_mut() - .unwrap() - .request_layout(element_state, cx) - }); - (layout_id, None) - } else { - let (layout_id, frame_state) = self.element.as_mut().unwrap().request_layout(None, cx); - (layout_id, Some(frame_state)) - }; - - self.phase = ElementDrawPhase::LayoutRequested { - layout_id, - frame_state, - }; - layout_id - } - - fn paint(mut self, cx: &mut ElementContext) -> Option { - match self.phase { - ElementDrawPhase::LayoutRequested { + fn after_layout(&mut self, cx: &mut ElementContext) { + match mem::take(&mut self.phase) { + ElementDrawPhase::BeforeLayout { layout_id, - frame_state, + mut before_layout, } | ElementDrawPhase::LayoutComputed { layout_id, - frame_state, + mut before_layout, .. } => { let bounds = cx.layout_bounds(layout_id); - - if let Some(mut frame_state) = frame_state { - self.element - .take() - .unwrap() - .paint(bounds, &mut frame_state, cx); - Some(frame_state) - } else { - let element_id = self - .element - .as_ref() - .unwrap() - .element_id() - .expect("if we don't have frame state, we should have element state"); - cx.with_element_state(element_id, |element_state, cx| { - let mut element_state = element_state.unwrap(); - self.element - .take() - .unwrap() - .paint(bounds, &mut element_state, cx); - ((), element_state) - }); - None - } + let node_id = cx.window.next_frame.dispatch_tree.push_node(); + let after_layout = self.element.after_layout(bounds, &mut before_layout, cx); + self.phase = ElementDrawPhase::AfterLayout { + node_id, + bounds, + before_layout, + after_layout, + }; + cx.window.next_frame.dispatch_tree.pop_node(); } + _ => panic!("must call before_layout before after_layout"), + } + } - _ => panic!("must call layout before paint"), + fn paint(&mut self, cx: &mut ElementContext) -> E::BeforeLayout { + match mem::take(&mut self.phase) { + ElementDrawPhase::AfterLayout { + node_id, + bounds, + mut before_layout, + mut after_layout, + .. + } => { + cx.window.next_frame.dispatch_tree.set_active_node(node_id); + self.element + .paint(bounds, &mut before_layout, &mut after_layout, cx); + self.phase = ElementDrawPhase::Painted; + before_layout + } + _ => panic!("must call after_layout before paint"), } } @@ -351,66 +326,63 @@ impl DrawableElement { cx: &mut ElementContext, ) -> Size { if matches!(&self.phase, ElementDrawPhase::Start) { - self.request_layout(cx); + self.before_layout(cx); } - let layout_id = match &mut self.phase { - ElementDrawPhase::LayoutRequested { + let layout_id = match mem::take(&mut self.phase) { + ElementDrawPhase::BeforeLayout { layout_id, - frame_state, + before_layout, } => { - cx.compute_layout(*layout_id, available_space); - let layout_id = *layout_id; + cx.compute_layout(layout_id, available_space); self.phase = ElementDrawPhase::LayoutComputed { layout_id, available_space, - frame_state: frame_state.take(), + before_layout, }; layout_id } ElementDrawPhase::LayoutComputed { layout_id, available_space: prev_available_space, - .. + before_layout, } => { - if available_space != *prev_available_space { - cx.compute_layout(*layout_id, available_space); - *prev_available_space = available_space; + if available_space != prev_available_space { + cx.compute_layout(layout_id, available_space); } - *layout_id + self.phase = ElementDrawPhase::LayoutComputed { + layout_id, + available_space, + before_layout, + }; + layout_id } _ => panic!("cannot measure after painting"), }; cx.layout_bounds(layout_id).size } - - fn draw( - mut self, - origin: Point, - available_space: Size, - cx: &mut ElementContext, - ) -> Option { - self.measure(available_space, cx); - cx.with_absolute_element_offset(origin, |cx| self.paint(cx)) - } } -impl ElementObject for Option> +impl ElementObject for Drawable where E: Element, - E::State: 'static, + E::BeforeLayout: 'static, { - fn element_id(&self) -> Option { - self.as_ref().unwrap().element_id() + fn inner_element(&mut self) -> &mut dyn Any { + &mut self.element } - fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { - DrawableElement::request_layout(self.as_mut().unwrap(), cx) + fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId { + Drawable::before_layout(self, cx) + } + + fn after_layout(&mut self, cx: &mut ElementContext) { + Drawable::after_layout(self, cx); } fn paint(&mut self, cx: &mut ElementContext) { - DrawableElement::paint(self.take().unwrap(), cx); + Drawable::paint(self, cx); } fn measure( @@ -418,16 +390,7 @@ where available_space: Size, cx: &mut ElementContext, ) -> Size { - DrawableElement::measure(self.as_mut().unwrap(), available_space, cx) - } - - fn draw( - &mut self, - origin: Point, - available_space: Size, - cx: &mut ElementContext, - ) { - DrawableElement::draw(self.take().unwrap(), origin, available_space, cx); + Drawable::measure(self, available_space, cx) } } @@ -438,18 +401,28 @@ impl AnyElement { pub(crate) fn new(element: E) -> Self where E: 'static + Element, - E::State: Any, + E::BeforeLayout: Any, { let element = ELEMENT_ARENA - .with_borrow_mut(|arena| arena.alloc(|| Some(DrawableElement::new(element)))) + .with_borrow_mut(|arena| arena.alloc(|| Drawable::new(element))) .map(|element| element as &mut dyn ElementObject); AnyElement(element) } + /// Attempt to downcast a reference to the boxed element to a specific type. + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.0.inner_element().downcast_mut::() + } + /// Request the layout ID of the element stored in this `AnyElement`. /// Used for laying out child elements in a parent element. - pub fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { - self.0.request_layout(cx) + pub fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId { + self.0.before_layout(cx) + } + + /// Commits the element bounds of this [AnyElement] for hitbox purposes. + pub fn after_layout(&mut self, cx: &mut ElementContext) { + self.0.after_layout(cx) } /// Paints the element stored in this `AnyElement`. @@ -466,35 +439,44 @@ impl AnyElement { self.0.measure(available_space, cx) } - /// Initializes this element and performs layout in the available space, then paints it at the given origin. - pub fn draw( + /// Initializes this element, performs layout if needed and commits its bounds for hitbox purposes. + pub fn layout( &mut self, - origin: Point, + absolute_offset: Point, available_space: Size, cx: &mut ElementContext, - ) { - self.0.draw(origin, available_space, cx) - } - - /// Returns the element ID of the element stored in this `AnyElement`, if any. - pub fn inner_id(&self) -> Option { - self.0.element_id() + ) -> Size { + let size = self.measure(available_space, cx); + cx.with_absolute_element_offset(absolute_offset, |cx| self.after_layout(cx)); + size } } impl Element for AnyElement { - type State = (); + type BeforeLayout = (); + type AfterLayout = (); - fn request_layout( - &mut self, - _: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { - let layout_id = self.request_layout(cx); + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + let layout_id = self.before_layout(cx); (layout_id, ()) } - fn paint(&mut self, _: Bounds, _: &mut Self::State, cx: &mut ElementContext) { + fn after_layout( + &mut self, + _: Bounds, + _: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) { + self.after_layout(cx) + } + + fn paint( + &mut self, + _: Bounds, + _: &mut Self::BeforeLayout, + _: &mut Self::AfterLayout, + cx: &mut ElementContext, + ) { self.paint(cx) } } @@ -502,10 +484,6 @@ impl Element for AnyElement { impl IntoElement for AnyElement { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self } @@ -521,30 +499,32 @@ pub struct Empty; impl IntoElement for Empty { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self } } impl Element for Empty { - type State = (); + type BeforeLayout = (); + type AfterLayout = (); - fn request_layout( - &mut self, - _state: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { (cx.request_layout(&crate::Style::default(), None), ()) } + fn after_layout( + &mut self, + _bounds: Bounds, + _state: &mut Self::BeforeLayout, + _cx: &mut ElementContext, + ) { + } + fn paint( &mut self, _bounds: Bounds, - _state: &mut Self::State, + _before_layout: &mut Self::BeforeLayout, + _after_layout: &mut Self::AfterLayout, _cx: &mut ElementContext, ) { } diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index 8011f51e0c..623dfa2280 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/crates/gpui/src/elements/canvas.rs @@ -4,54 +4,68 @@ use crate::{Bounds, Element, ElementContext, IntoElement, Pixels, Style, StyleRe /// Construct a canvas element with the given paint callback. /// Useful for adding short term custom drawing to a view. -pub fn canvas(callback: impl 'static + FnOnce(&Bounds, &mut ElementContext)) -> Canvas { +pub fn canvas( + after_layout: impl 'static + FnOnce(Bounds, &mut ElementContext) -> T, + paint: impl 'static + FnOnce(Bounds, T, &mut ElementContext), +) -> Canvas { Canvas { - paint_callback: Some(Box::new(callback)), + after_layout: Some(Box::new(after_layout)), + paint: Some(Box::new(paint)), style: StyleRefinement::default(), } } /// A canvas element, meant for accessing the low level paint API without defining a whole /// custom element -pub struct Canvas { - paint_callback: Option, &mut ElementContext)>>, +pub struct Canvas { + after_layout: Option, &mut ElementContext) -> T>>, + paint: Option, T, &mut ElementContext)>>, style: StyleRefinement, } -impl IntoElement for Canvas { +impl IntoElement for Canvas { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self } } -impl Element for Canvas { - type State = Style; +impl Element for Canvas { + type BeforeLayout = Style; + type AfterLayout = Option; - fn request_layout( - &mut self, - _: Option, - cx: &mut ElementContext, - ) -> (crate::LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (crate::LayoutId, Self::BeforeLayout) { let mut style = Style::default(); style.refine(&self.style); let layout_id = cx.request_layout(&style, []); (layout_id, style) } - fn paint(&mut self, bounds: Bounds, style: &mut Style, cx: &mut ElementContext) { + fn after_layout( + &mut self, + bounds: Bounds, + _before_layout: &mut Style, + cx: &mut ElementContext, + ) -> Option { + Some(self.after_layout.take().unwrap()(bounds, cx)) + } + + fn paint( + &mut self, + bounds: Bounds, + style: &mut Style, + after_layout: &mut Self::AfterLayout, + cx: &mut ElementContext, + ) { + let after_layout = after_layout.take().unwrap(); style.paint(bounds, cx, |cx| { - (self.paint_callback.take().unwrap())(&bounds, cx) + (self.paint.take().unwrap())(bounds, after_layout, cx) }); } } -impl Styled for Canvas { +impl Styled for Canvas { fn style(&mut self) -> &mut crate::StyleRefinement { &mut self.style } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index c2b80b56c1..5b8aab174a 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -17,13 +17,12 @@ use crate::{ point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds, - ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, Global, - IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, + ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, Global, Hitbox, + HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, - ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, - View, Visibility, WindowContext, + ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, + WindowContext, }; - use collections::HashMap; use refineable::Refineable; use smallvec::SmallVec; @@ -85,10 +84,8 @@ impl Interactivity { listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) { self.mouse_down_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && bounds.visibly_contains(&event.position, cx) + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && event.button == button && hitbox.is_hovered(cx) { (listener)(event, cx) } @@ -104,8 +101,8 @@ impl Interactivity { listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) { self.mouse_down_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Capture && bounds.visibly_contains(&event.position, cx) { + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(cx) { (listener)(event, cx) } })); @@ -120,8 +117,8 @@ impl Interactivity { listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) { self.mouse_down_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { (listener)(event, cx) } })); @@ -137,10 +134,8 @@ impl Interactivity { listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, ) { self.mouse_up_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && bounds.visibly_contains(&event.position, cx) + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && event.button == button && hitbox.is_hovered(cx) { (listener)(event, cx) } @@ -156,8 +151,8 @@ impl Interactivity { listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, ) { self.mouse_up_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Capture && bounds.visibly_contains(&event.position, cx) { + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(cx) { (listener)(event, cx) } })); @@ -172,8 +167,8 @@ impl Interactivity { listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, ) { self.mouse_up_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { (listener)(event, cx) } })); @@ -189,9 +184,8 @@ impl Interactivity { listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) { self.mouse_down_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Capture && !bounds.visibly_contains(&event.position, cx) - { + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Capture && !hitbox.is_hovered(cx) { (listener)(event, cx) } })); @@ -208,10 +202,10 @@ impl Interactivity { listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, ) { self.mouse_up_listeners - .push(Box::new(move |event, bounds, phase, cx| { + .push(Box::new(move |event, phase, hitbox, cx| { if phase == DispatchPhase::Capture && event.button == button - && !bounds.visibly_contains(&event.position, cx) + && !hitbox.is_hovered(cx) { (listener)(event, cx); } @@ -227,8 +221,8 @@ impl Interactivity { listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, ) { self.mouse_move_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { (listener)(event, cx); } })); @@ -248,7 +242,7 @@ impl Interactivity { T: 'static, { self.mouse_move_listeners - .push(Box::new(move |event, bounds, phase, cx| { + .push(Box::new(move |event, phase, hitbox, cx| { if phase == DispatchPhase::Capture && cx .active_drag @@ -258,7 +252,7 @@ impl Interactivity { (listener)( &DragMoveEvent { event: event.clone(), - bounds: bounds.bounds, + bounds: hitbox.bounds, drag: PhantomData, }, cx, @@ -276,8 +270,8 @@ impl Interactivity { listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, ) { self.scroll_wheel_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { + .push(Box::new(move |event, phase, hitbox, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { (listener)(event, cx); } })); @@ -482,8 +476,8 @@ impl Interactivity { /// Block the mouse from interacting with this element or any of its children /// The imperative API equivalent to [`InteractiveElement::block_mouse`] - pub fn block_mouse(&mut self) { - self.block_mouse = true; + pub fn occlude_mouse(&mut self) { + self.occlude_mouse = true; } } @@ -836,8 +830,8 @@ pub trait InteractiveElement: Sized { /// Block the mouse from interacting with this element or any of its children /// The fluent API equivalent to [`Interactivity::block_mouse`] - fn block_mouse(mut self) -> Self { - self.interactivity().block_mouse(); + fn occlude(mut self) -> Self { + self.interactivity().occlude_mouse(); self } } @@ -872,7 +866,7 @@ pub trait StatefulInteractiveElement: InteractiveElement { /// Track the scroll state of this element with the given handle. fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { - self.interactivity().scroll_handle = Some(scroll_handle.clone()); + self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone()); self } @@ -979,15 +973,15 @@ pub trait FocusableElement: InteractiveElement { } pub(crate) type MouseDownListener = - Box; + Box; pub(crate) type MouseUpListener = - Box; + Box; pub(crate) type MouseMoveListener = - Box; + Box; pub(crate) type ScrollWheelListener = - Box; + Box; pub(crate) type ClickListener = Box; @@ -1031,6 +1025,16 @@ pub struct Div { children: SmallVec<[AnyElement; 2]>, } +/// A frame state for a `Div` element, which contains layout IDs for its children. +/// +/// This struct is used internally by the `Div` element to manage the layout state of its children +/// during the UI update cycle. It holds a small vector of `LayoutId` values, each corresponding to +/// a child element of the `Div`. These IDs are used to query the layout engine for the computed +/// bounds of the children after the layout phase is complete. +pub struct DivFrameState { + child_layout_ids: SmallVec<[LayoutId; 2]>, +} + impl Styled for Div { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style @@ -1050,54 +1054,41 @@ impl ParentElement for Div { } impl Element for Div { - type State = DivState; + type BeforeLayout = DivFrameState; + type AfterLayout = Option; - fn request_layout( - &mut self, - element_state: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { let mut child_layout_ids = SmallVec::new(); - let (layout_id, interactive_state) = self.interactivity.layout( - element_state.map(|s| s.interactive_state), - cx, - |style, cx| { - cx.with_text_style(style.text_style().cloned(), |cx| { - child_layout_ids = self - .children - .iter_mut() - .map(|child| child.request_layout(cx)) - .collect::>(); - cx.request_layout(&style, child_layout_ids.iter().copied()) - }) - }, - ); - ( - layout_id, - DivState { - interactive_state, - child_layout_ids, - }, - ) + let layout_id = self.interactivity.before_layout(cx, |style, cx| { + cx.with_text_style(style.text_style().cloned(), |cx| { + child_layout_ids = self + .children + .iter_mut() + .map(|child| child.before_layout(cx)) + .collect::>(); + cx.request_layout(&style, child_layout_ids.iter().copied()) + }) + }); + (layout_id, DivFrameState { child_layout_ids }) } - fn paint( + fn after_layout( &mut self, bounds: Bounds, - element_state: &mut Self::State, + before_layout: &mut Self::BeforeLayout, cx: &mut ElementContext, - ) { + ) -> Option { let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); - let content_size = if element_state.child_layout_ids.is_empty() { + let content_size = if before_layout.child_layout_ids.is_empty() { bounds.size - } else if let Some(scroll_handle) = self.interactivity.scroll_handle.as_ref() { + } else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { let mut state = scroll_handle.0.borrow_mut(); - state.child_bounds = Vec::with_capacity(element_state.child_layout_ids.len()); + state.child_bounds = Vec::with_capacity(before_layout.child_layout_ids.len()); state.bounds = bounds; let requested = state.requested_scroll_top.take(); - for (ix, child_layout_id) in element_state.child_layout_ids.iter().enumerate() { + for (ix, child_layout_id) in before_layout.child_layout_ids.iter().enumerate() { let child_bounds = cx.layout_bounds(*child_layout_id); child_min = child_min.min(&child_bounds.origin); child_max = child_max.max(&child_bounds.lower_right()); @@ -1112,7 +1103,7 @@ impl Element for Div { } (child_max - child_min).into() } else { - for child_layout_id in &element_state.child_layout_ids { + for child_layout_id in &before_layout.child_layout_ids { let child_bounds = cx.layout_bounds(*child_layout_id); child_min = child_min.min(&child_bounds.origin); child_max = child_max.max(&child_bounds.lower_right()); @@ -1120,60 +1111,62 @@ impl Element for Div { (child_max - child_min).into() }; - self.interactivity.paint( + self.interactivity.after_layout( bounds, content_size, - &mut element_state.interactive_state, cx, - |_style, scroll_offset, cx| { + |_style, scroll_offset, hitbox, cx| { cx.with_element_offset(scroll_offset, |cx| { for child in &mut self.children { - child.paint(cx); + child.after_layout(cx); } - }) + }); + hitbox }, - ); + ) + } + + fn paint( + &mut self, + bounds: Bounds, + _before_layout: &mut Self::BeforeLayout, + hitbox: &mut Option, + cx: &mut ElementContext, + ) { + self.interactivity + .paint(bounds, hitbox.as_ref(), cx, |_style, cx| { + for child in &mut self.children { + child.paint(cx); + } + }); } } impl IntoElement for Div { type Element = Self; - fn element_id(&self) -> Option { - self.interactivity.element_id.clone() - } - fn into_element(self) -> Self::Element { self } } -/// The state a div needs to keep track of between frames. -pub struct DivState { - child_layout_ids: SmallVec<[LayoutId; 2]>, - interactive_state: InteractiveElementState, -} - -impl DivState { - /// Is the div currently being clicked on? - pub fn is_active(&self) -> bool { - self.interactive_state - .pending_mouse_down - .as_ref() - .map_or(false, |pending| pending.borrow().is_some()) - } -} - /// The interactivity struct. Powers all of the general-purpose /// interactivity in the `Div` element. #[derive(Default)] pub struct Interactivity { /// The element ID of the element pub element_id: Option, + /// Whether the element was clicked. This will only be present after layout. + pub active: Option, + /// Whether the element was hovered. This will only be present after paint if an hitbox + /// was created for the interactive element. + pub hovered: Option, + pub(crate) content_size: Size, pub(crate) key_context: Option, pub(crate) focusable: bool, pub(crate) tracked_focus_handle: Option, - pub(crate) scroll_handle: Option, + pub(crate) tracked_scroll_handle: Option, + pub(crate) scroll_offset: Option>>>, pub(crate) group: Option, /// The base style of the element, before any modifications are applied /// by focus, active, etc. @@ -1202,7 +1195,7 @@ pub struct Interactivity { pub(crate) drag_listener: Option<(Box, DragListener)>, pub(crate) hover_listener: Option>, pub(crate) tooltip_builder: Option, - pub(crate) block_mouse: bool, + pub(crate) occlude_mouse: bool, #[cfg(debug_assertions)] pub(crate) location: Option>, @@ -1211,68 +1204,176 @@ pub struct Interactivity { pub(crate) debug_selector: Option, } -/// The bounds and depth of an element in the computed element tree. -#[derive(Clone, Debug)] -pub struct InteractiveBounds { - /// The 2D bounds of the element - pub bounds: Bounds, - /// The 'stacking order', or depth, for this element - pub stacking_order: StackingOrder, -} - -impl InteractiveBounds { - /// Checks whether this point was inside these bounds in the rendered frame, and that these bounds where the topmost layer - /// Never call this during paint to perform hover calculations. It will reference the previous frame and could cause flicker. - pub fn visibly_contains(&self, point: &Point, cx: &WindowContext) -> bool { - self.bounds.contains(point) && cx.was_top_layer(point, &self.stacking_order) - } - - /// Checks whether this point was inside these bounds, and that these bounds where the topmost layer - /// under an active drag - pub fn drag_target_contains(&self, point: &Point, cx: &WindowContext) -> bool { - self.bounds.contains(point) - && cx.was_top_layer_under_active_drag(point, &self.stacking_order) - } -} - impl Interactivity { /// Layout this element according to this interactivity state's configured styles - pub fn layout( + pub fn before_layout( &mut self, - element_state: Option, cx: &mut ElementContext, f: impl FnOnce(Style, &mut ElementContext) -> LayoutId, - ) -> (LayoutId, InteractiveElementState) { - let mut element_state = element_state.unwrap_or_default(); + ) -> LayoutId { + cx.with_element_state::( + self.element_id.clone(), + |element_state, cx| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); - if cx.has_active_drag() { - if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() { - *pending_mouse_down.borrow_mut() = None; + if let Some(element_state) = element_state.as_ref() { + if cx.has_active_drag() { + if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() + { + *pending_mouse_down.borrow_mut() = None; + } + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + *clicked_state.borrow_mut() = ElementClickedState::default(); + } + } + } + + // Ensure we store a focus handle in our element state if we're focusable. + // If there's an explicit focus handle we're tracking, use that. Otherwise + // create a new handle and store it in the element state, which lives for as + // as frames contain an element with this id. + if self.focusable { + if self.tracked_focus_handle.is_none() { + if let Some(element_state) = element_state.as_mut() { + self.tracked_focus_handle = Some( + element_state + .focus_handle + .get_or_insert_with(|| cx.focus_handle()) + .clone(), + ); + } + } + } + + if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() { + self.scroll_offset = Some(scroll_handle.0.borrow().offset.clone()); + } else if self.base_style.overflow.x == Some(Overflow::Scroll) + || self.base_style.overflow.y == Some(Overflow::Scroll) + { + if let Some(element_state) = element_state.as_mut() { + self.scroll_offset = Some( + element_state + .scroll_offset + .get_or_insert_with(|| Rc::default()) + .clone(), + ); + } + } + + let style = self.compute_style_internal(None, element_state.as_mut(), cx); + let layout_id = f(style, cx); + (layout_id, element_state) + }, + ) + } + + /// Commit the bounds of this element according to this interactivity state's configured styles. + pub fn after_layout( + &mut self, + bounds: Bounds, + content_size: Size, + cx: &mut ElementContext, + f: impl FnOnce(&Style, Point, Option, &mut ElementContext) -> R, + ) -> R { + self.content_size = content_size; + cx.with_element_state::( + self.element_id.clone(), + |element_state, cx| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + let style = self.compute_style_internal(None, element_state.as_mut(), cx); + + if let Some(element_state) = element_state.as_ref() { + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + let clicked_state = clicked_state.borrow(); + self.active = Some(clicked_state.element); + } + + if let Some(active_tooltip) = element_state.active_tooltip.as_ref() { + if let Some(active_tooltip) = active_tooltip.borrow().as_ref() { + if let Some(tooltip) = active_tooltip.tooltip.clone() { + cx.set_tooltip(tooltip); + } + } + } + } + + cx.with_text_style(style.text_style().cloned(), |cx| { + cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| { + let hitbox = if self.occlude_mouse + || style.mouse_cursor.is_some() + || self.group.is_some() + || self.has_hover_styles() + || self.has_mouse_listeners() + { + Some(cx.insert_hitbox(bounds, self.occlude_mouse)) + } else { + None + }; + + let scroll_offset = self.clamp_scroll_position(bounds, &style, cx); + let result = f(&style, scroll_offset, hitbox, cx); + (result, element_state) + }) + }) + }, + ) + } + + fn has_hover_styles(&self) -> bool { + self.hover_style.is_some() || self.group_hover_style.is_some() + } + + fn has_mouse_listeners(&self) -> bool { + !self.mouse_up_listeners.is_empty() + || !self.mouse_down_listeners.is_empty() + || !self.mouse_move_listeners.is_empty() + || !self.scroll_wheel_listeners.is_empty() + || self.drag_listener.is_some() + || !self.drop_listeners.is_empty() + } + + fn clamp_scroll_position( + &mut self, + bounds: Bounds, + style: &Style, + cx: &mut ElementContext, + ) -> Point { + if let Some(scroll_offset) = self.scroll_offset.as_ref() { + if let Some(scroll_handle) = &self.tracked_scroll_handle { + scroll_handle.0.borrow_mut().overflow = style.overflow; } - if let Some(clicked_state) = element_state.clicked_state.as_ref() { - *clicked_state.borrow_mut() = ElementClickedState::default(); - } - } - // Ensure we store a focus handle in our element state if we're focusable. - // If there's an explicit focus handle we're tracking, use that. Otherwise - // create a new handle and store it in the element state, which lives for as - // as frames contain an element with this id. - if self.focusable { - element_state.focus_handle.get_or_insert_with(|| { - self.tracked_focus_handle - .clone() - .unwrap_or_else(|| cx.focus_handle()) - }); + let rem_size = cx.rem_size(); + let padding_size = size( + style + .padding + .left + .to_pixels(bounds.size.width.into(), rem_size) + + style + .padding + .right + .to_pixels(bounds.size.width.into(), rem_size), + style + .padding + .top + .to_pixels(bounds.size.height.into(), rem_size) + + style + .padding + .bottom + .to_pixels(bounds.size.height.into(), rem_size), + ); + let scroll_max = (self.content_size + padding_size - bounds.size).max(&Size::default()); + // Clamp scroll offset in case scroll max is smaller now (e.g., if children + // were removed or the bounds became larger). + let mut scroll_offset = scroll_offset.borrow_mut(); + scroll_offset.x = scroll_offset.x.clamp(-scroll_max.width, px(0.)); + scroll_offset.y = scroll_offset.y.clamp(-scroll_max.height, px(0.)); + *scroll_offset + } else { + Point::default() } - - if let Some(scroll_handle) = self.scroll_handle.as_ref() { - element_state.scroll_offset = Some(scroll_handle.0.borrow().offset.clone()); - } - - let style = self.compute_style(None, &mut element_state, cx); - let layout_id = f(style, cx); - (layout_id, element_state) } /// Paint this element according to this interactivity state's configured styles @@ -1286,731 +1387,660 @@ impl Interactivity { pub fn paint( &mut self, bounds: Bounds, - content_size: Size, - element_state: &mut InteractiveElementState, + hitbox: Option<&Hitbox>, cx: &mut ElementContext, - f: impl FnOnce(&Style, Point, &mut ElementContext), + f: impl FnOnce(&Style, &mut ElementContext), ) { - let style = self.compute_style(Some(bounds), element_state, cx); - let z_index = style.z_index.unwrap_or(0); + self.hovered = hitbox.map(|hitbox| hitbox.is_hovered(cx)); + cx.with_element_state::( + self.element_id.clone(), + |element_state, cx| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); - #[cfg(any(feature = "test-support", test))] - if let Some(debug_selector) = &self.debug_selector { - cx.window - .next_frame - .debug_bounds - .insert(debug_selector.clone(), bounds); - } + let style = self.compute_style_internal(hitbox, element_state.as_mut(), cx); - let paint_hover_group_handler = |cx: &mut ElementContext| { - let hover_group_bounds = self - .group_hover_style - .as_ref() - .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); + #[cfg(any(feature = "test-support", test))] + if let Some(debug_selector) = &self.debug_selector { + cx.window + .next_frame + .debug_bounds + .insert(debug_selector.clone(), bounds); + } - if let Some(group_bounds) = hover_group_bounds { - let hovered = group_bounds.contains(&cx.mouse_position()); - cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture - && group_bounds.contains(&event.position) != hovered - { - cx.refresh(); - } - }); - } - }; + self.paint_hover_group_handler(cx); - if style.visibility == Visibility::Hidden { - cx.with_z_index(z_index, |cx| paint_hover_group_handler(cx)); - return; - } + if style.visibility == Visibility::Hidden { + return ((), element_state); + } - cx.with_z_index(z_index, |cx| { - style.paint(bounds, cx, |cx: &mut ElementContext| { - cx.with_text_style(style.text_style().cloned(), |cx| { - cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| { - #[cfg(debug_assertions)] - if self.element_id.is_some() - && (style.debug - || style.debug_below - || cx.has_global::()) - && bounds.contains(&cx.mouse_position()) - { - const FONT_SIZE: crate::Pixels = crate::Pixels(10.); - let element_id = format!("{:?}", self.element_id.as_ref().unwrap()); - let str_len = element_id.len(); + style.paint(bounds, cx, |cx: &mut ElementContext| { + cx.with_text_style(style.text_style().cloned(), |cx| { + cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| { + if let Some(hitbox) = hitbox { + #[cfg(debug_assertions)] + self.paint_debug_info(hitbox, &style, cx); - let render_debug_text = |cx: &mut ElementContext| { - if let Some(text) = cx - .text_system() - .shape_text( - element_id.into(), - FONT_SIZE, - &[cx.text_style().to_run(str_len)], - None, - ) - .ok() - .and_then(|mut text| text.pop()) - { - text.paint(bounds.origin, FONT_SIZE, cx).ok(); - - let text_bounds = crate::Bounds { - origin: bounds.origin, - size: text.size(FONT_SIZE), - }; - if self.location.is_some() - && text_bounds.contains(&cx.mouse_position()) - && cx.modifiers().command - { - let command_held = cx.modifiers().command; - cx.on_key_event({ - move |e: &crate::ModifiersChangedEvent, _phase, cx| { - if e.modifiers.command != command_held - && text_bounds.contains(&cx.mouse_position()) - { - cx.refresh(); - } - } - }); - - let hovered = bounds.contains(&cx.mouse_position()); - cx.on_mouse_event( - move |event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture - && bounds.contains(&event.position) != hovered - { - cx.refresh(); - } - }, - ); - - cx.on_mouse_event({ - let location = self.location.unwrap(); - move |e: &crate::MouseDownEvent, phase, cx| { - if text_bounds.contains(&e.position) - && phase.capture() - { - cx.stop_propagation(); - let Ok(dir) = std::env::current_dir() else { - return; - }; - - eprintln!( - "This element was created at:\n{}:{}:{}", - dir.join(location.file()).to_string_lossy(), - location.line(), - location.column() - ); - } - } - }); - cx.paint_quad(crate::outline( - crate::Bounds { - origin: bounds.origin - + crate::point( - crate::px(0.), - FONT_SIZE - px(2.), - ), - size: crate::Size { - width: text_bounds.size.width, - height: crate::px(1.), - }, - }, - crate::red(), - )) + if !cx.has_active_drag() { + if let Some(mouse_cursor) = style.mouse_cursor { + cx.set_cursor_style(mouse_cursor, hitbox); } } - }; - cx.with_z_index(1, |cx| { - cx.with_text_style( - Some(crate::TextStyleRefinement { - color: Some(crate::red()), - line_height: Some(FONT_SIZE.into()), - background_color: Some(crate::white()), - ..Default::default() - }), - render_debug_text, - ) - }); - } + if let Some(group) = self.group.clone() { + GroupHitboxes::push(group, hitbox.id, cx); + } - let interactive_bounds = InteractiveBounds { - bounds: bounds.intersect(&cx.content_mask().bounds), - stacking_order: cx.stacking_order().clone(), - }; + self.paint_mouse_listeners(hitbox, element_state.as_mut(), cx); + self.paint_scroll_listener(hitbox, &style, cx); + } - if self.block_mouse - || style.background.as_ref().is_some_and(|fill| { - fill.color().is_some_and(|color| !color.is_transparent()) - }) - { - cx.add_opaque_layer(interactive_bounds.bounds); - } + self.paint_keyboard_listeners(cx); + f(&style, cx); - if !cx.has_active_drag() { - if let Some(mouse_cursor) = style.mouse_cursor { - let hovered = bounds.contains(&cx.mouse_position()); - if hovered { - cx.set_cursor_style( - mouse_cursor, - interactive_bounds.stacking_order.clone(), + if hitbox.is_some() { + if let Some(group) = self.group.as_ref() { + GroupHitboxes::pop(group, cx); + } + } + }); + }); + }); + + ((), element_state) + }, + ); + } + + #[cfg(debug_assertions)] + fn paint_debug_info(&mut self, hitbox: &Hitbox, style: &Style, cx: &mut ElementContext) { + if self.element_id.is_some() + && (style.debug || style.debug_below || cx.has_global::()) + && hitbox.is_hovered(cx) + { + const FONT_SIZE: crate::Pixels = crate::Pixels(10.); + let element_id = format!("{:?}", self.element_id.as_ref().unwrap()); + let str_len = element_id.len(); + + let render_debug_text = |cx: &mut ElementContext| { + if let Some(text) = cx + .text_system() + .shape_text( + element_id.into(), + FONT_SIZE, + &[cx.text_style().to_run(str_len)], + None, + ) + .ok() + .and_then(|mut text| text.pop()) + { + text.paint(hitbox.origin, FONT_SIZE, cx).ok(); + + let text_bounds = crate::Bounds { + origin: hitbox.origin, + size: text.size(FONT_SIZE), + }; + if self.location.is_some() + && text_bounds.contains(&cx.mouse_position()) + && cx.modifiers().command + { + let command_held = cx.modifiers().command; + cx.on_key_event({ + move |e: &crate::ModifiersChangedEvent, _phase, cx| { + if e.modifiers.command != command_held + && text_bounds.contains(&cx.mouse_position()) + { + cx.refresh(); + } + } + }); + + let was_hovered = hitbox.is_hovered(cx); + cx.on_mouse_event({ + let hitbox = hitbox.clone(); + move |_: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { + let hovered = hitbox.is_hovered(cx); + if hovered != was_hovered { + cx.refresh(); + } + } + } + }); + + cx.on_mouse_event({ + let hitbox = hitbox.clone(); + let location = self.location.unwrap(); + move |e: &crate::MouseDownEvent, phase, cx| { + if text_bounds.contains(&e.position) + && phase.capture() + && hitbox.is_hovered(cx) + { + cx.stop_propagation(); + let Ok(dir) = std::env::current_dir() else { + return; + }; + + eprintln!( + "This element was created at:\n{}:{}:{}", + dir.join(location.file()).to_string_lossy(), + location.line(), + location.column() ); } } - } + }); + cx.paint_quad(crate::outline( + crate::Bounds { + origin: hitbox.origin + + crate::point(crate::px(0.), FONT_SIZE - px(2.)), + size: crate::Size { + width: text_bounds.size.width, + height: crate::px(1.), + }, + }, + crate::red(), + )) + } + } + }; - // If this element can be focused, register a mouse down listener - // that will automatically transfer focus when hitting the element. - // This behavior can be suppressed by using `cx.prevent_default()`. - if let Some(focus_handle) = element_state.focus_handle.clone() { - cx.on_mouse_event({ - let interactive_bounds = interactive_bounds.clone(); - move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && !cx.default_prevented() - && interactive_bounds.visibly_contains(&event.position, cx) - { - cx.focus(&focus_handle); - // If there is a parent that is also focusable, prevent it - // from transferring focus because we already did so. - cx.prevent_default(); - } - } - }); - } + cx.with_text_style( + Some(crate::TextStyleRefinement { + color: Some(crate::red()), + line_height: Some(FONT_SIZE.into()), + background_color: Some(crate::white()), + ..Default::default() + }), + render_debug_text, + ) + } + } - for listener in self.mouse_down_listeners.drain(..) { - let interactive_bounds = interactive_bounds.clone(); - cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - listener(event, &interactive_bounds, phase, cx); - }) - } + fn paint_mouse_listeners( + &mut self, + hitbox: &Hitbox, + element_state: Option<&mut InteractiveElementState>, + cx: &mut ElementContext, + ) { + // If this element can be focused, register a mouse down listener + // that will automatically transfer focus when hitting the element. + // This behavior can be suppressed by using `cx.prevent_default()`. + if let Some(focus_handle) = self.tracked_focus_handle.clone() { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && hitbox.is_hovered(cx) + && !cx.default_prevented() + { + cx.focus(&focus_handle); + // If there is a parent that is also focusable, prevent it + // from transferring focus because we already did so. + cx.prevent_default(); + } + }); + } - for listener in self.mouse_up_listeners.drain(..) { - let interactive_bounds = interactive_bounds.clone(); - cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - listener(event, &interactive_bounds, phase, cx); - }) - } + for listener in self.mouse_down_listeners.drain(..) { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + listener(event, phase, &hitbox, cx); + }) + } - for listener in self.mouse_move_listeners.drain(..) { - let interactive_bounds = interactive_bounds.clone(); - cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - listener(event, &interactive_bounds, phase, cx); - }) - } + for listener in self.mouse_up_listeners.drain(..) { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + listener(event, phase, &hitbox, cx); + }) + } - for listener in self.scroll_wheel_listeners.drain(..) { - let interactive_bounds = interactive_bounds.clone(); - cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - listener(event, &interactive_bounds, phase, cx); - }) - } + for listener in self.mouse_move_listeners.drain(..) { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + listener(event, phase, &hitbox, cx); + }) + } - paint_hover_group_handler(cx); + for listener in self.scroll_wheel_listeners.drain(..) { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { + listener(event, phase, &hitbox, cx); + }) + } - if self.hover_style.is_some() - || self.base_style.mouse_cursor.is_some() - || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() - { - let bounds = bounds.intersect(&cx.content_mask().bounds); - let hovered = bounds.contains(&cx.mouse_position()); - cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture - && bounds.contains(&event.position) != hovered - { - cx.refresh(); - } - }); - } + if self.hover_style.is_some() + || self.base_style.mouse_cursor.is_some() + || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() + { + let hitbox = hitbox.clone(); + let was_hovered = hitbox.is_hovered(cx); + cx.on_mouse_event(move |_: &MouseMoveEvent, phase, cx| { + let hovered = hitbox.is_hovered(cx); + if phase == DispatchPhase::Capture && hovered != was_hovered { + cx.refresh(); + } + }); + } - let mut drag_listener = mem::take(&mut self.drag_listener); - let drop_listeners = mem::take(&mut self.drop_listeners); - let click_listeners = mem::take(&mut self.click_listeners); - let can_drop_predicate = mem::take(&mut self.can_drop_predicate); + let mut drag_listener = mem::take(&mut self.drag_listener); + let drop_listeners = mem::take(&mut self.drop_listeners); + let click_listeners = mem::take(&mut self.click_listeners); + let can_drop_predicate = mem::take(&mut self.can_drop_predicate); - if !drop_listeners.is_empty() { - cx.on_mouse_event({ - let interactive_bounds = interactive_bounds.clone(); - move |event: &MouseUpEvent, phase, cx| { - if let Some(drag) = &cx.active_drag { - if phase == DispatchPhase::Bubble - && interactive_bounds - .drag_target_contains(&event.position, cx) - { - let drag_state_type = drag.value.as_ref().type_id(); - for (drop_state_type, listener) in &drop_listeners { - if *drop_state_type == drag_state_type { - let drag = cx.active_drag.take().expect( - "checked for type drag state type above", - ); + if !drop_listeners.is_empty() { + let hitbox = hitbox.clone(); + cx.on_mouse_event({ + move |_: &MouseUpEvent, phase, cx| { + if let Some(drag) = &cx.active_drag { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + let drag_state_type = drag.value.as_ref().type_id(); + for (drop_state_type, listener) in &drop_listeners { + if *drop_state_type == drag_state_type { + let drag = cx + .active_drag + .take() + .expect("checked for type drag state type above"); - let mut can_drop = true; - if let Some(predicate) = &can_drop_predicate { - can_drop = predicate( - drag.value.as_ref(), - cx.deref_mut(), - ); - } - - if can_drop { - listener( - drag.value.as_ref(), - cx.deref_mut(), - ); - cx.refresh(); - cx.stop_propagation(); - } - } - } - } - } - } - }); - } - - if !click_listeners.is_empty() || drag_listener.is_some() { - let pending_mouse_down = element_state - .pending_mouse_down - .get_or_insert_with(Default::default) - .clone(); - - let clicked_state = element_state - .clicked_state - .get_or_insert_with(Default::default) - .clone(); - - cx.on_mouse_event({ - let interactive_bounds = interactive_bounds.clone(); - let pending_mouse_down = pending_mouse_down.clone(); - move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == MouseButton::Left - && interactive_bounds.visibly_contains(&event.position, cx) - { - *pending_mouse_down.borrow_mut() = Some(event.clone()); - cx.refresh(); - } - } - }); - - cx.on_mouse_event({ - let pending_mouse_down = pending_mouse_down.clone(); - move |event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture { - return; + let mut can_drop = true; + if let Some(predicate) = &can_drop_predicate { + can_drop = predicate(drag.value.as_ref(), cx.deref_mut()); } - let mut pending_mouse_down = pending_mouse_down.borrow_mut(); - if let Some(mouse_down) = pending_mouse_down.clone() { - if !cx.has_active_drag() - && (event.position - mouse_down.position).magnitude() - > DRAG_THRESHOLD - { - if let Some((drag_value, drag_listener)) = - drag_listener.take() - { - *clicked_state.borrow_mut() = - ElementClickedState::default(); - let cursor_offset = event.position - bounds.origin; - let drag = (drag_listener)(drag_value.as_ref(), cx); - cx.active_drag = Some(AnyDrag { - view: drag, - value: drag_value, - cursor_offset, - }); - pending_mouse_down.take(); - cx.refresh(); - cx.stop_propagation(); - } - } - } - } - }); - - cx.on_mouse_event({ - let interactive_bounds = interactive_bounds.clone(); - let mut captured_mouse_down = None; - move |event: &MouseUpEvent, phase, cx| match phase { - // Clear the pending mouse down during the capture phase, - // so that it happens even if another event handler stops - // propagation. - DispatchPhase::Capture => { - let mut pending_mouse_down = - pending_mouse_down.borrow_mut(); - if pending_mouse_down.is_some() { - captured_mouse_down = pending_mouse_down.take(); - cx.refresh(); - } - } - // Fire click handlers during the bubble phase. - DispatchPhase::Bubble => { - if let Some(mouse_down) = captured_mouse_down.take() { - if interactive_bounds - .visibly_contains(&event.position, cx) - { - let mouse_click = ClickEvent { - down: mouse_down, - up: event.clone(), - }; - for listener in &click_listeners { - listener(&mouse_click, cx); - } - } - } - } - } - }); - } - - if let Some(hover_listener) = self.hover_listener.take() { - let was_hovered = element_state - .hover_state - .get_or_insert_with(Default::default) - .clone(); - let has_mouse_down = element_state - .pending_mouse_down - .get_or_insert_with(Default::default) - .clone(); - let interactive_bounds = interactive_bounds.clone(); - - cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - if phase != DispatchPhase::Bubble { - return; - } - let is_hovered = interactive_bounds - .visibly_contains(&event.position, cx) - && has_mouse_down.borrow().is_none() - && !cx.has_active_drag(); - let mut was_hovered = was_hovered.borrow_mut(); - - if is_hovered != *was_hovered { - *was_hovered = is_hovered; - drop(was_hovered); - - hover_listener(&is_hovered, cx.deref_mut()); - } - }); - } - - if let Some(tooltip_builder) = self.tooltip_builder.take() { - let active_tooltip = element_state - .active_tooltip - .get_or_insert_with(Default::default) - .clone(); - let pending_mouse_down = element_state - .pending_mouse_down - .get_or_insert_with(Default::default) - .clone(); - let interactive_bounds = interactive_bounds.clone(); - - cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - let is_hovered = interactive_bounds - .visibly_contains(&event.position, cx) - && pending_mouse_down.borrow().is_none(); - if !is_hovered { - active_tooltip.borrow_mut().take(); - return; - } - - if phase != DispatchPhase::Bubble { - return; - } - - if active_tooltip.borrow().is_none() { - let task = cx.spawn({ - let active_tooltip = active_tooltip.clone(); - let tooltip_builder = tooltip_builder.clone(); - - move |mut cx| async move { - cx.background_executor().timer(TOOLTIP_DELAY).await; - cx.update(|cx| { - active_tooltip.borrow_mut().replace( - ActiveTooltip { - tooltip: Some(AnyTooltip { - view: tooltip_builder(cx), - cursor_offset: cx.mouse_position(), - }), - _task: None, - }, - ); - cx.refresh(); - }) - .ok(); - } - }); - active_tooltip.borrow_mut().replace(ActiveTooltip { - tooltip: None, - _task: Some(task), - }); - } - }); - - let active_tooltip = element_state - .active_tooltip - .get_or_insert_with(Default::default) - .clone(); - cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { - active_tooltip.borrow_mut().take(); - }); - - if let Some(active_tooltip) = element_state - .active_tooltip - .get_or_insert_with(Default::default) - .borrow() - .as_ref() - { - if let Some(tooltip) = active_tooltip.tooltip.clone() { - cx.set_tooltip(tooltip); - } - } - } - - let active_state = element_state - .clicked_state - .get_or_insert_with(Default::default) - .clone(); - if active_state.borrow().is_clicked() { - cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Capture { - *active_state.borrow_mut() = ElementClickedState::default(); - cx.refresh(); - } - }); - } else { - let active_group_bounds = self - .group_active_style - .as_ref() - .and_then(|group_active| GroupBounds::get(&group_active.group, cx)); - let interactive_bounds = interactive_bounds.clone(); - cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble && !cx.default_prevented() { - let group = active_group_bounds - .map_or(false, |bounds| bounds.contains(&down.position)); - let element = - interactive_bounds.visibly_contains(&down.position, cx); - if group || element { - *active_state.borrow_mut() = - ElementClickedState { group, element }; - cx.refresh(); - } - } - }); - } - - let overflow = style.overflow; - if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll { - if let Some(scroll_handle) = &self.scroll_handle { - scroll_handle.0.borrow_mut().overflow = overflow; - } - - let scroll_offset = element_state - .scroll_offset - .get_or_insert_with(Rc::default) - .clone(); - let line_height = cx.line_height(); - let rem_size = cx.rem_size(); - let padding_size = size( - style - .padding - .left - .to_pixels(bounds.size.width.into(), rem_size) - + style - .padding - .right - .to_pixels(bounds.size.width.into(), rem_size), - style - .padding - .top - .to_pixels(bounds.size.height.into(), rem_size) - + style - .padding - .bottom - .to_pixels(bounds.size.height.into(), rem_size), - ); - let scroll_max = - (content_size + padding_size - bounds.size).max(&Size::default()); - // Clamp scroll offset in case scroll max is smaller now (e.g., if children - // were removed or the bounds became larger). - { - let mut scroll_offset = scroll_offset.borrow_mut(); - scroll_offset.x = scroll_offset.x.clamp(-scroll_max.width, px(0.)); - scroll_offset.y = scroll_offset.y.clamp(-scroll_max.height, px(0.)); - } - - let interactive_bounds = interactive_bounds.clone(); - cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && interactive_bounds.visibly_contains(&event.position, cx) - { - let mut scroll_offset = scroll_offset.borrow_mut(); - let old_scroll_offset = *scroll_offset; - let delta = event.delta.pixel_delta(line_height); - - if overflow.x == Overflow::Scroll { - let mut delta_x = Pixels::ZERO; - if !delta.x.is_zero() { - delta_x = delta.x; - } else if overflow.y != Overflow::Scroll { - delta_x = delta.y; - } - - scroll_offset.x = (scroll_offset.x + delta_x) - .clamp(-scroll_max.width, px(0.)); - } - - if overflow.y == Overflow::Scroll { - let mut delta_y = Pixels::ZERO; - if !delta.y.is_zero() { - delta_y = delta.y; - } else if overflow.x != Overflow::Scroll { - delta_y = delta.x; - } - - scroll_offset.y = (scroll_offset.y + delta_y) - .clamp(-scroll_max.height, px(0.)); - } - - if *scroll_offset != old_scroll_offset { + if can_drop { + listener(drag.value.as_ref(), cx.deref_mut()); cx.refresh(); cx.stop_propagation(); } } - }); + } } - - if let Some(group) = self.group.clone() { - GroupBounds::push(group, bounds, cx); - } - - let scroll_offset = element_state - .scroll_offset - .as_ref() - .map(|scroll_offset| *scroll_offset.borrow()); - - let key_down_listeners = mem::take(&mut self.key_down_listeners); - let key_up_listeners = mem::take(&mut self.key_up_listeners); - let action_listeners = mem::take(&mut self.action_listeners); - cx.with_key_dispatch( - self.key_context.clone(), - element_state.focus_handle.clone(), - |_, cx| { - for listener in key_down_listeners { - cx.on_key_event(move |event: &KeyDownEvent, phase, cx| { - listener(event, phase, cx); - }) - } - - for listener in key_up_listeners { - cx.on_key_event(move |event: &KeyUpEvent, phase, cx| { - listener(event, phase, cx); - }) - } - - for (action_type, listener) in action_listeners { - cx.on_action(action_type, listener) - } - - f(&style, scroll_offset.unwrap_or_default(), cx) - }, - ); - - if let Some(group) = self.group.as_ref() { - GroupBounds::pop(group, cx); - } - }); - }); + } + } }); - }); + } + + if let Some(element_state) = element_state { + if !click_listeners.is_empty() || drag_listener.is_some() { + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + let clicked_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); + + cx.on_mouse_event({ + let pending_mouse_down = pending_mouse_down.clone(); + let hitbox = hitbox.clone(); + move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == MouseButton::Left + && hitbox.is_hovered(cx) + { + *pending_mouse_down.borrow_mut() = Some(event.clone()); + cx.refresh(); + } + } + }); + + cx.on_mouse_event({ + let pending_mouse_down = pending_mouse_down.clone(); + let hitbox = hitbox.clone(); + move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { + return; + } + + let mut pending_mouse_down = pending_mouse_down.borrow_mut(); + if let Some(mouse_down) = pending_mouse_down.clone() { + if !cx.has_active_drag() + && (event.position - mouse_down.position).magnitude() + > DRAG_THRESHOLD + { + if let Some((drag_value, drag_listener)) = drag_listener.take() { + *clicked_state.borrow_mut() = ElementClickedState::default(); + let cursor_offset = event.position - hitbox.origin; + let drag = (drag_listener)(drag_value.as_ref(), cx); + cx.active_drag = Some(AnyDrag { + view: drag, + value: drag_value, + cursor_offset, + }); + pending_mouse_down.take(); + cx.refresh(); + cx.stop_propagation(); + } + } + } + } + }); + + cx.on_mouse_event({ + let mut captured_mouse_down = None; + let hitbox = hitbox.clone(); + move |event: &MouseUpEvent, phase, cx| match phase { + // Clear the pending mouse down during the capture phase, + // so that it happens even if another event handler stops + // propagation. + DispatchPhase::Capture => { + let mut pending_mouse_down = pending_mouse_down.borrow_mut(); + if pending_mouse_down.is_some() && hitbox.is_hovered(cx) { + captured_mouse_down = pending_mouse_down.take(); + cx.refresh(); + } + } + // Fire click handlers during the bubble phase. + DispatchPhase::Bubble => { + if let Some(mouse_down) = captured_mouse_down.take() { + let mouse_click = ClickEvent { + down: mouse_down, + up: event.clone(), + }; + for listener in &click_listeners { + listener(&mouse_click, cx); + } + } + } + } + }); + } + + if let Some(hover_listener) = self.hover_listener.take() { + let hitbox = hitbox.clone(); + let was_hovered = element_state + .hover_state + .get_or_insert_with(Default::default) + .clone(); + let has_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + cx.on_mouse_event(move |_: &MouseMoveEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + let is_hovered = has_mouse_down.borrow().is_none() + && !cx.has_active_drag() + && hitbox.is_hovered(cx); + let mut was_hovered = was_hovered.borrow_mut(); + + if is_hovered != *was_hovered { + *was_hovered = is_hovered; + drop(was_hovered); + + hover_listener(&is_hovered, cx.deref_mut()); + } + }); + } + + if let Some(tooltip_builder) = self.tooltip_builder.take() { + let active_tooltip = element_state + .active_tooltip + .get_or_insert_with(Default::default) + .clone(); + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + let hitbox = hitbox.clone(); + + cx.on_mouse_event(move |_: &MouseMoveEvent, phase, cx| { + let is_hovered = pending_mouse_down.borrow().is_none() && hitbox.is_hovered(cx); + if !is_hovered { + active_tooltip.borrow_mut().take(); + return; + } + + if phase != DispatchPhase::Bubble { + return; + } + + if active_tooltip.borrow().is_none() { + let task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + let tooltip_builder = tooltip_builder.clone(); + + move |mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + cx.update(|cx| { + active_tooltip.borrow_mut().replace(ActiveTooltip { + tooltip: Some(AnyTooltip { + view: tooltip_builder(cx), + cursor_offset: cx.mouse_position(), + }), + _task: None, + }); + cx.refresh(); + }) + .ok(); + } + }); + active_tooltip.borrow_mut().replace(ActiveTooltip { + tooltip: None, + _task: Some(task), + }); + } + }); + + let active_tooltip = element_state + .active_tooltip + .get_or_insert_with(Default::default) + .clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { + active_tooltip.borrow_mut().take(); + }); + } + + let active_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); + if active_state.borrow().is_clicked() { + cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Capture { + *active_state.borrow_mut() = ElementClickedState::default(); + cx.refresh(); + } + }); + } else { + let active_group_hitbox = self + .group_active_style + .as_ref() + .and_then(|group_active| GroupHitboxes::get(&group_active.group, cx)); + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble && !cx.default_prevented() { + let group_hovered = active_group_hitbox + .map_or(false, |group_hitbox_id| group_hitbox_id.is_hovered(cx)); + let element_hovered = hitbox.is_hovered(cx); + if group_hovered || element_hovered { + *active_state.borrow_mut() = ElementClickedState { + group: group_hovered, + element: element_hovered, + }; + cx.refresh(); + } + } + }); + } + } + } + + fn paint_keyboard_listeners(&mut self, cx: &mut ElementContext) { + let key_down_listeners = mem::take(&mut self.key_down_listeners); + let key_up_listeners = mem::take(&mut self.key_up_listeners); + let action_listeners = mem::take(&mut self.action_listeners); + if let Some(context) = self.key_context.clone() { + cx.set_key_context(context); + } + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + cx.set_focus_handle(focus_handle); + } + + for listener in key_down_listeners { + cx.on_key_event(move |event: &KeyDownEvent, phase, cx| { + listener(event, phase, cx); + }) + } + + for listener in key_up_listeners { + cx.on_key_event(move |event: &KeyUpEvent, phase, cx| { + listener(event, phase, cx); + }) + } + + for (action_type, listener) in action_listeners { + cx.on_action(action_type, listener) + } + } + + fn paint_hover_group_handler(&self, cx: &mut ElementContext) { + let group_hitbox = self + .group_hover_style + .as_ref() + .and_then(|group_hover| GroupHitboxes::get(&group_hover.group, cx)); + + if let Some(group_hitbox) = group_hitbox { + let was_hovered = group_hitbox.is_hovered(cx); + cx.on_mouse_event(move |_: &MouseMoveEvent, phase, cx| { + let hovered = group_hitbox.is_hovered(cx); + if phase == DispatchPhase::Capture && hovered != was_hovered { + cx.refresh(); + } + }); + } + } + + fn paint_scroll_listener(&self, hitbox: &Hitbox, style: &Style, cx: &mut ElementContext) { + if let Some(scroll_offset) = self.scroll_offset.clone() { + let overflow = style.overflow; + let line_height = cx.line_height(); + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + let mut scroll_offset = scroll_offset.borrow_mut(); + let old_scroll_offset = *scroll_offset; + let delta = event.delta.pixel_delta(line_height); + + if overflow.x == Overflow::Scroll { + let mut delta_x = Pixels::ZERO; + if !delta.x.is_zero() { + delta_x = delta.x; + } else if overflow.y != Overflow::Scroll { + delta_x = delta.y; + } + + scroll_offset.x += delta_x; + } + + if overflow.y == Overflow::Scroll { + let mut delta_y = Pixels::ZERO; + if !delta.y.is_zero() { + delta_y = delta.y; + } else if overflow.x != Overflow::Scroll { + delta_y = delta.x; + } + + scroll_offset.y += delta_y; + } + + if *scroll_offset != old_scroll_offset { + cx.refresh(); + cx.stop_propagation(); + } + } + }); + } } /// Compute the visual style for this element, based on the current bounds and the element's state. - pub fn compute_style( + pub fn compute_style(&self, hitbox: Option<&Hitbox>, cx: &mut ElementContext) -> Style { + cx.with_element_state(self.element_id.clone(), |element_state, cx| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + let style = self.compute_style_internal(hitbox, element_state.as_mut(), cx); + (style, element_state) + }) + } + + /// Called from internal methods that have already called with_element_state. + fn compute_style_internal( &self, - bounds: Option>, - element_state: &mut InteractiveElementState, + hitbox: Option<&Hitbox>, + element_state: Option<&mut InteractiveElementState>, cx: &mut ElementContext, ) -> Style { let mut style = Style::default(); style.refine(&self.base_style); - cx.with_z_index(style.z_index.unwrap_or(0), |cx| { - if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - if let Some(in_focus_style) = self.in_focus_style.as_ref() { - if focus_handle.within_focused(cx) { - style.refine(in_focus_style); + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + if let Some(in_focus_style) = self.in_focus_style.as_ref() { + if focus_handle.within_focused(cx) { + style.refine(in_focus_style); + } + } + + if let Some(focus_style) = self.focus_style.as_ref() { + if focus_handle.is_focused(cx) { + style.refine(focus_style); + } + } + } + + if let Some(hitbox) = hitbox { + if !cx.has_active_drag() { + if let Some(group_hover) = self.group_hover_style.as_ref() { + if let Some(group_hitbox_id) = + GroupHitboxes::get(&group_hover.group, cx.deref_mut()) + { + if group_hitbox_id.is_hovered(cx) { + style.refine(&group_hover.style); + } } } - if let Some(focus_style) = self.focus_style.as_ref() { - if focus_handle.is_focused(cx) { - style.refine(focus_style); + if let Some(hover_style) = self.hover_style.as_ref() { + if hitbox.is_hovered(cx) { + style.refine(hover_style); } } } - if let Some(bounds) = bounds { - let mouse_position = cx.mouse_position(); - if !cx.has_active_drag() { - if let Some(group_hover) = self.group_hover_style.as_ref() { - if let Some(group_bounds) = - GroupBounds::get(&group_hover.group, cx.deref_mut()) - { - if group_bounds.contains(&mouse_position) { - style.refine(&group_hover.style); - } - } - } - - if let Some(hover_style) = self.hover_style.as_ref() { - if bounds - .intersect(&cx.content_mask().bounds) - .contains(&mouse_position) - { - style.refine(hover_style); - } - } + if let Some(drag) = cx.active_drag.take() { + let mut can_drop = true; + if let Some(can_drop_predicate) = &self.can_drop_predicate { + can_drop = can_drop_predicate(drag.value.as_ref(), cx.deref_mut()); } - if let Some(drag) = cx.active_drag.take() { - let mut can_drop = true; - if let Some(can_drop_predicate) = &self.can_drop_predicate { - can_drop = can_drop_predicate(drag.value.as_ref(), cx.deref_mut()); - } - - if can_drop { - for (state_type, group_drag_style) in &self.group_drag_over_styles { - if let Some(group_bounds) = - GroupBounds::get(&group_drag_style.group, cx.deref_mut()) - { - if *state_type == drag.value.as_ref().type_id() - && group_bounds.contains(&mouse_position) - { - style.refine(&group_drag_style.style); - } - } - } - - for (state_type, build_drag_over_style) in &self.drag_over_styles { + if can_drop { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_hitbox_id) = + GroupHitboxes::get(&group_drag_style.group, cx.deref_mut()) + { if *state_type == drag.value.as_ref().type_id() - && bounds - .intersect(&cx.content_mask().bounds) - .contains(&mouse_position) - && cx.was_top_layer_under_active_drag( - &mouse_position, - cx.stacking_order(), - ) + && group_hitbox_id.is_hovered(cx) { - style.refine(&build_drag_over_style(drag.value.as_ref(), cx)); + style.refine(&group_drag_style.style); } } } - cx.active_drag = Some(drag); + for (state_type, build_drag_over_style) in &self.drag_over_styles { + if *state_type == drag.value.as_ref().type_id() && hitbox.is_hovered(cx) { + style.refine(&build_drag_over_style(drag.value.as_ref(), cx)); + } + } } - } + cx.active_drag = Some(drag); + } + } + + if let Some(element_state) = element_state { let clicked_state = element_state .clicked_state .get_or_insert_with(Default::default) @@ -2026,7 +2056,7 @@ impl Interactivity { style.refine(active_style) } } - }); + } style } @@ -2067,12 +2097,12 @@ impl ElementClickedState { } #[derive(Default)] -pub(crate) struct GroupBounds(HashMap; 1]>>); +pub(crate) struct GroupHitboxes(HashMap>); -impl Global for GroupBounds {} +impl Global for GroupHitboxes {} -impl GroupBounds { - pub fn get(name: &SharedString, cx: &mut AppContext) -> Option> { +impl GroupHitboxes { + pub fn get(name: &SharedString, cx: &mut AppContext) -> Option { cx.default_global::() .0 .get(name) @@ -2080,12 +2110,12 @@ impl GroupBounds { .cloned() } - pub fn push(name: SharedString, bounds: Bounds, cx: &mut AppContext) { + pub fn push(name: SharedString, hitbox_id: HitboxId, cx: &mut AppContext) { cx.default_global::() .0 .entry(name) .or_default() - .push(bounds); + .push(hitbox_id); } pub fn pop(name: &SharedString, cx: &mut AppContext) { @@ -2125,18 +2155,30 @@ impl Element for Focusable where E: Element, { - type State = E::State; + type BeforeLayout = E::BeforeLayout; + type AfterLayout = E::AfterLayout; - fn request_layout( - &mut self, - state: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { - self.element.request_layout(state, cx) + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + self.element.before_layout(cx) } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { - self.element.paint(bounds, state, cx) + fn after_layout( + &mut self, + bounds: Bounds, + state: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) -> E::AfterLayout { + self.element.after_layout(bounds, state, cx) + } + + fn paint( + &mut self, + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, + after_layout: &mut Self::AfterLayout, + cx: &mut ElementContext, + ) { + self.element.paint(bounds, before_layout, after_layout, cx) } } @@ -2146,10 +2188,6 @@ where { type Element = E::Element; - fn element_id(&self) -> Option { - self.element.element_id() - } - fn into_element(self) -> Self::Element { self.element.into_element() } @@ -2200,18 +2238,30 @@ impl Element for Stateful where E: Element, { - type State = E::State; + type BeforeLayout = E::BeforeLayout; + type AfterLayout = E::AfterLayout; - fn request_layout( - &mut self, - state: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { - self.element.request_layout(state, cx) + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + self.element.before_layout(cx) } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { - self.element.paint(bounds, state, cx) + fn after_layout( + &mut self, + bounds: Bounds, + state: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) -> E::AfterLayout { + self.element.after_layout(bounds, state, cx) + } + + fn paint( + &mut self, + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, + after_layout: &mut Self::AfterLayout, + cx: &mut ElementContext, + ) { + self.element.paint(bounds, before_layout, after_layout, cx); } } @@ -2221,10 +2271,6 @@ where { type Element = Self; - fn element_id(&self) -> Option { - self.element.element_id() - } - fn into_element(self) -> Self::Element { self } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 32009e04db..a5c781a9a4 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; use std::sync::Arc; use crate::{ - point, size, Bounds, DevicePixels, Element, ElementContext, ImageData, InteractiveElement, - InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUri, Size, + point, size, Bounds, DevicePixels, Element, ElementContext, Hitbox, ImageData, + InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, SharedUri, Size, StyleRefinement, Styled, UriOrPath, }; use futures::FutureExt; @@ -88,86 +88,85 @@ impl Img { } impl Element for Img { - type State = InteractiveElementState; + type BeforeLayout = (); + type AfterLayout = Option; - fn request_layout( + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + let layout_id = self + .interactivity + .before_layout(cx, |style, cx| cx.request_layout(&style, [])); + (layout_id, ()) + } + + fn after_layout( &mut self, - element_state: Option, + bounds: Bounds, + _before_layout: &mut Self::BeforeLayout, cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { + ) -> Option { self.interactivity - .layout(element_state, cx, |style, cx| cx.request_layout(&style, [])) + .after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) } fn paint( &mut self, bounds: Bounds, - element_state: &mut Self::State, + _: &mut Self::BeforeLayout, + hitbox: &mut Self::AfterLayout, cx: &mut ElementContext, ) { let source = self.source.clone(); - self.interactivity.paint( - bounds, - bounds.size, - element_state, - cx, - |style, _scroll_offset, cx| { + self.interactivity + .paint(bounds, hitbox.as_ref(), cx, |style, cx| { let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size()); - cx.with_z_index(1, |cx| { - match source { - ImageSource::Uri(_) | ImageSource::File(_) => { - let uri_or_path: UriOrPath = match source { - ImageSource::Uri(uri) => uri.into(), - ImageSource::File(path) => path.into(), - _ => unreachable!(), - }; + match source { + ImageSource::Uri(_) | ImageSource::File(_) => { + let uri_or_path: UriOrPath = match source { + ImageSource::Uri(uri) => uri.into(), + ImageSource::File(path) => path.into(), + _ => unreachable!(), + }; - let image_future = cx.image_cache.get(uri_or_path.clone(), cx); - if let Some(data) = image_future - .clone() - .now_or_never() - .and_then(|result| result.ok()) - { - let new_bounds = preserve_aspect_ratio(bounds, data.size()); - cx.paint_image(new_bounds, corner_radii, data, self.grayscale) - .log_err(); - } else { - cx.spawn(|mut cx| async move { - if image_future.await.ok().is_some() { - cx.on_next_frame(|cx| cx.refresh()); - } - }) - .detach(); - } - } - - ImageSource::Data(data) => { + let image_future = cx.image_cache.get(uri_or_path.clone(), cx); + if let Some(data) = image_future + .clone() + .now_or_never() + .and_then(|result| result.ok()) + { let new_bounds = preserve_aspect_ratio(bounds, data.size()); cx.paint_image(new_bounds, corner_radii, data, self.grayscale) .log_err(); + } else { + cx.spawn(|mut cx| async move { + if image_future.await.ok().is_some() { + cx.on_next_frame(|cx| cx.refresh()); + } + }) + .detach(); } + } - #[cfg(target_os = "macos")] - ImageSource::Surface(surface) => { - let size = size(surface.width().into(), surface.height().into()); - let new_bounds = preserve_aspect_ratio(bounds, size); - // TODO: Add support for corner_radii and grayscale. - cx.paint_surface(new_bounds, surface); - } - }; - }); - }, - ) + ImageSource::Data(data) => { + let new_bounds = preserve_aspect_ratio(bounds, data.size()); + cx.paint_image(new_bounds, corner_radii, data, self.grayscale) + .log_err(); + } + + #[cfg(target_os = "macos")] + ImageSource::Surface(surface) => { + let size = size(surface.width().into(), surface.height().into()); + let new_bounds = preserve_aspect_ratio(bounds, size); + // TODO: Add support for corner_radii and grayscale. + cx.paint_surface(new_bounds, surface); + } + } + }) } } impl IntoElement for Img { type Element = Self; - fn element_id(&self) -> Option { - self.interactivity.element_id.clone() - } - fn into_element(self) -> Self::Element { self } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 90dd8da0c2..7d99704bf7 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -8,11 +8,12 @@ use crate::{ point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, - Element, ElementContext, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, + Element, ElementContext, HitboxId, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext, }; use collections::VecDeque; use refineable::Refineable as _; +use smallvec::SmallVec; use std::{cell::RefCell, ops::Range, rc::Rc}; use sum_tree::{Bias, SumTree}; use taffy::style::Overflow; @@ -96,6 +97,13 @@ struct LayoutItemsResponse { item_elements: VecDeque, } +/// Frame state used by the [List] element. +#[derive(Default)] +pub struct ListFrameState { + scroll_top: ListOffset, + items: SmallVec<[AnyElement; 32]>, +} + #[derive(Clone)] enum ListItem { Unrendered, @@ -302,7 +310,6 @@ impl StateInner { height: Pixels, delta: Point, cx: &mut WindowContext, - padding: Edges, ) { // Drop scroll events after a reset, since we can't calculate // the new logical scroll top without the item heights @@ -310,6 +317,7 @@ impl StateInner { return; } + let padding = self.last_padding.unwrap_or_default(); let scroll_max = (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.)); let new_scroll_top = (self.scroll_top(scroll_top) - delta.y) @@ -516,13 +524,13 @@ pub struct ListOffset { } impl Element for List { - type State = (); + type BeforeLayout = ListFrameState; + type AfterLayout = HitboxId; - fn request_layout( + fn before_layout( &mut self, - _state: Option, cx: &mut crate::ElementContext, - ) -> (crate::LayoutId, Self::State) { + ) -> (crate::LayoutId, Self::BeforeLayout) { let layout_id = match self.sizing_behavior { ListSizingBehavior::Infer => { let mut style = Style::default(); @@ -580,15 +588,15 @@ impl Element for List { }) } }; - (layout_id, ()) + (layout_id, ListFrameState::default()) } - fn paint( + fn after_layout( &mut self, - bounds: Bounds, - _state: &mut Self::State, - cx: &mut crate::ElementContext, - ) { + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) -> HitboxId { let state = &mut *self.state.0.borrow_mut(); state.reset = false; @@ -615,12 +623,11 @@ impl Element for List { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); item_origin.y -= layout_response.scroll_top.offset_in_item; - for item_element in &mut layout_response.item_elements { - let item_height = item_element - .measure(layout_response.available_item_space, cx) - .height; - item_element.draw(item_origin, layout_response.available_item_space, cx); - item_origin.y += item_height; + for mut item_element in layout_response.item_elements { + let item_size = item_element.measure(layout_response.available_item_space, cx); + item_element.layout(item_origin, layout_response.available_item_space, cx); + before_layout.items.push(item_element); + item_origin.y += item_size.height; } }); } @@ -628,20 +635,33 @@ impl Element for List { state.last_layout_bounds = Some(bounds); state.last_padding = Some(padding); + cx.insert_hitbox(bounds, false).id + } + + fn paint( + &mut self, + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, + hitbox_id: &mut HitboxId, + cx: &mut crate::ElementContext, + ) { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + for item in &mut before_layout.items { + item.paint(cx); + } + }); + let list_state = self.state.clone(); let height = bounds.size.height; - + let scroll_top = before_layout.scroll_top; + let hitbox_id = *hitbox_id; cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && bounds.contains(&event.position) - && cx.was_top_layer(&event.position, cx.stacking_order()) - { + if phase == DispatchPhase::Bubble && hitbox_id.is_hovered(cx) { list_state.0.borrow_mut().scroll( - &layout_response.scroll_top, + &scroll_top, height, event.delta.pixel_delta(px(20.)), cx, - padding, ) } }); @@ -651,10 +671,6 @@ impl Element for List { impl IntoElement for List { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self } @@ -761,7 +777,7 @@ mod test { cx.draw( point(px(0.), px(0.)), size(px(100.), px(20.)).into(), - |_| list(state.clone()).w_full().h_full().z_index(10).into_any(), + |_| list(state.clone()).w_full().h_full().into_any(), ); // Reset diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index ed23205ae7..cd60e8de6b 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -9,6 +9,7 @@ use crate::{ /// The state that the overlay element uses to track its children. pub struct OverlayState { child_layout_ids: SmallVec<[LayoutId; 4]>, + offset: Point, } /// An overlay element that can be used to display UI that @@ -69,17 +70,14 @@ impl ParentElement for Overlay { } impl Element for Overlay { - type State = OverlayState; + type BeforeLayout = OverlayState; + type AfterLayout = (); - fn request_layout( - &mut self, - _: Option, - cx: &mut ElementContext, - ) -> (crate::LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (crate::LayoutId, Self::BeforeLayout) { let child_layout_ids = self .children .iter_mut() - .map(|child| child.request_layout(cx)) + .map(|child| child.before_layout(cx)) .collect::>(); let overlay_style = Style { @@ -90,22 +88,28 @@ impl Element for Overlay { let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied()); - (layout_id, OverlayState { child_layout_ids }) + ( + layout_id, + OverlayState { + child_layout_ids, + offset: Point::default(), + }, + ) } - fn paint( + fn after_layout( &mut self, - bounds: crate::Bounds, - element_state: &mut Self::State, + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, cx: &mut ElementContext, ) { - if element_state.child_layout_ids.is_empty() { + if before_layout.child_layout_ids.is_empty() { return; } let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); - for child_layout_id in &element_state.child_layout_ids { + for child_layout_id in &before_layout.child_layout_ids { let child_bounds = cx.layout_bounds(*child_layout_id); child_min = child_min.min(&child_bounds.origin); child_max = child_max.max(&child_bounds.lower_right()); @@ -165,25 +169,30 @@ impl Element for Overlay { desired.origin.y = limits.origin.y; } - let mut offset = cx.element_offset() + desired.origin - bounds.origin; - offset = point(offset.x.round(), offset.y.round()); - cx.with_absolute_element_offset(offset, |cx| { - cx.break_content_mask(|cx| { - for child in &mut self.children { - child.paint(cx); - } - }) - }) + before_layout.offset = cx.element_offset() + desired.origin - bounds.origin; + before_layout.offset = point( + before_layout.offset.x.round(), + before_layout.offset.y.round(), + ); + + for child in self.children.drain(..) { + cx.defer_draw(child, before_layout.offset, 1); + } + } + + fn paint( + &mut self, + _bounds: crate::Bounds, + _before_layout: &mut Self::BeforeLayout, + _after_layout: &mut Self::AfterLayout, + _cx: &mut ElementContext, + ) { } } impl IntoElement for Overlay { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self } diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index 2ef0888563..cd215ebac1 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -1,6 +1,6 @@ use crate::{ - Bounds, Element, ElementContext, ElementId, InteractiveElement, InteractiveElementState, - Interactivity, IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, + Bounds, Element, ElementContext, Hitbox, InteractiveElement, Interactivity, IntoElement, + LayoutId, Pixels, SharedString, StyleRefinement, Styled, }; use util::ResultExt; @@ -27,28 +27,37 @@ impl Svg { } impl Element for Svg { - type State = InteractiveElementState; + type BeforeLayout = (); + type AfterLayout = Option; - fn request_layout( + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + let layout_id = self + .interactivity + .before_layout(cx, |style, cx| cx.request_layout(&style, None)); + (layout_id, ()) + } + + fn after_layout( &mut self, - element_state: Option, + bounds: Bounds, + _before_layout: &mut Self::BeforeLayout, cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { - self.interactivity.layout(element_state, cx, |style, cx| { - cx.request_layout(&style, None) - }) + ) -> Option { + self.interactivity + .after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) } fn paint( &mut self, bounds: Bounds, - element_state: &mut Self::State, + _before_layout: &mut Self::BeforeLayout, + hitbox: &mut Option, cx: &mut ElementContext, ) where Self: Sized, { self.interactivity - .paint(bounds, bounds.size, element_state, cx, |style, _, cx| { + .paint(bounds, hitbox.as_ref(), cx, |style, cx| { if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { cx.paint_svg(bounds, path.clone(), color).log_err(); } @@ -59,10 +68,6 @@ impl Element for Svg { impl IntoElement for Svg { type Element = Self; - fn element_id(&self) -> Option { - self.interactivity.element_id.clone() - } - fn into_element(self) -> Self::Element { self } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index c07f581910..75452e701e 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -1,7 +1,7 @@ use crate::{ ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementContext, ElementId, - HighlightStyle, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine, + HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Pixels, Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine, TOOLTIP_DELAY, }; use anyhow::anyhow; @@ -17,30 +17,37 @@ use std::{ use util::ResultExt; impl Element for &'static str { - type State = TextState; + type BeforeLayout = TextState; + type AfterLayout = (); - fn request_layout( - &mut self, - _: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { let mut state = TextState::default(); let layout_id = state.layout(SharedString::from(*self), None, cx); (layout_id, state) } - fn paint(&mut self, bounds: Bounds, state: &mut TextState, cx: &mut ElementContext) { - state.paint(bounds, self, cx) + fn after_layout( + &mut self, + _bounds: Bounds, + _text_state: &mut Self::BeforeLayout, + _cx: &mut ElementContext, + ) { + } + + fn paint( + &mut self, + bounds: Bounds, + text_state: &mut TextState, + _: &mut (), + cx: &mut ElementContext, + ) { + text_state.paint(bounds, self, cx) } } impl IntoElement for &'static str { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self } @@ -49,41 +56,44 @@ impl IntoElement for &'static str { impl IntoElement for String { type Element = SharedString; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self.into() } } impl Element for SharedString { - type State = TextState; + type BeforeLayout = TextState; + type AfterLayout = (); - fn request_layout( - &mut self, - _: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { let mut state = TextState::default(); let layout_id = state.layout(self.clone(), None, cx); (layout_id, state) } - fn paint(&mut self, bounds: Bounds, state: &mut TextState, cx: &mut ElementContext) { + fn after_layout( + &mut self, + _bounds: Bounds, + _text_state: &mut Self::BeforeLayout, + _cx: &mut ElementContext, + ) { + } + + fn paint( + &mut self, + bounds: Bounds, + text_state: &mut Self::BeforeLayout, + _: &mut Self::AfterLayout, + cx: &mut ElementContext, + ) { let text_str: &str = self.as_ref(); - state.paint(bounds, text_str, cx) + text_state.paint(bounds, text_str, cx) } } impl IntoElement for SharedString { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self } @@ -138,30 +148,37 @@ impl StyledText { } impl Element for StyledText { - type State = TextState; + type BeforeLayout = TextState; + type AfterLayout = (); - fn request_layout( - &mut self, - _: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { let mut state = TextState::default(); let layout_id = state.layout(self.text.clone(), self.runs.take(), cx); (layout_id, state) } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { - state.paint(bounds, &self.text, cx) + fn after_layout( + &mut self, + _bounds: Bounds, + _state: &mut Self::BeforeLayout, + _cx: &mut ElementContext, + ) { + } + + fn paint( + &mut self, + bounds: Bounds, + text_state: &mut Self::BeforeLayout, + _: &mut Self::AfterLayout, + cx: &mut ElementContext, + ) { + text_state.paint(bounds, &self.text, cx) } } impl IntoElement for StyledText { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self } @@ -324,8 +341,8 @@ struct InteractiveTextClickEvent { } #[doc(hidden)] +#[derive(Default)] pub struct InteractiveTextState { - text_state: TextState, mouse_down_index: Rc>>, hovered_index: Rc>>, active_tooltip: Rc>>, @@ -385,179 +402,184 @@ impl InteractiveText { } impl Element for InteractiveText { - type State = InteractiveTextState; + type BeforeLayout = TextState; + type AfterLayout = Hitbox; - fn request_layout( - &mut self, - state: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { - if let Some(InteractiveTextState { - mouse_down_index, - hovered_index, - active_tooltip, - .. - }) = state - { - let (layout_id, text_state) = self.text.request_layout(None, cx); - let element_state = InteractiveTextState { - text_state, - mouse_down_index, - hovered_index, - active_tooltip, - }; - (layout_id, element_state) - } else { - let (layout_id, text_state) = self.text.request_layout(None, cx); - let element_state = InteractiveTextState { - text_state, - mouse_down_index: Rc::default(), - hovered_index: Rc::default(), - active_tooltip: Rc::default(), - }; - (layout_id, element_state) - } + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + self.text.before_layout(cx) } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { - if let Some(click_listener) = self.click_listener.take() { - let mouse_position = cx.mouse_position(); - if let Some(ix) = state.text_state.index_for_position(bounds, mouse_position) { - if self - .clickable_ranges - .iter() - .any(|range| range.contains(&ix)) - { - let stacking_order = cx.stacking_order().clone(); - cx.set_cursor_style(crate::CursorStyle::PointingHand, stacking_order); - } - } + fn after_layout( + &mut self, + bounds: Bounds, + state: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) -> Hitbox { + self.text.after_layout(bounds, state, cx); + cx.insert_hitbox(bounds, false) + } - let text_state = state.text_state.clone(); - let mouse_down = state.mouse_down_index.clone(); - if let Some(mouse_down_index) = mouse_down.get() { - let clickable_ranges = mem::take(&mut self.clickable_ranges); - cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble { - if let Some(mouse_up_index) = - text_state.index_for_position(bounds, event.position) + fn paint( + &mut self, + bounds: Bounds, + text_state: &mut Self::BeforeLayout, + hitbox: &mut Hitbox, + cx: &mut ElementContext, + ) { + cx.with_element_state::( + Some(self.element_id.clone()), + |interactive_state, cx| { + let mut interactive_state = interactive_state.unwrap().unwrap_or_default(); + if let Some(click_listener) = self.click_listener.take() { + let mouse_position = cx.mouse_position(); + if let Some(ix) = text_state.index_for_position(bounds, mouse_position) { + if self + .clickable_ranges + .iter() + .any(|range| range.contains(&ix)) { - click_listener( - &clickable_ranges, - InteractiveTextClickEvent { - mouse_down_index, - mouse_up_index, - }, - cx, - ) - } - - mouse_down.take(); - cx.refresh(); - } - }); - } else { - cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble { - if let Some(mouse_down_index) = - text_state.index_for_position(bounds, event.position) - { - mouse_down.set(Some(mouse_down_index)); - cx.refresh(); + cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) } } - }); - } - } - if let Some(hover_listener) = self.hover_listener.take() { - let text_state = state.text_state.clone(); - let hovered_index = state.hovered_index.clone(); - cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Bubble { - let current = hovered_index.get(); - let updated = text_state.index_for_position(bounds, event.position); - if current != updated { - hovered_index.set(updated); - hover_listener(updated, event.clone(), cx); - cx.refresh(); - } - } - }); - } - if let Some(tooltip_builder) = self.tooltip_builder.clone() { - let active_tooltip = state.active_tooltip.clone(); - let pending_mouse_down = state.mouse_down_index.clone(); - let text_state = state.text_state.clone(); - cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - let position = text_state.index_for_position(bounds, event.position); - let is_hovered = position.is_some() && pending_mouse_down.get().is_none(); - if !is_hovered { - active_tooltip.take(); - return; - } - let position = position.unwrap(); + let text_state = text_state.clone(); + let mouse_down = interactive_state.mouse_down_index.clone(); + if let Some(mouse_down_index) = mouse_down.get() { + let hitbox = hitbox.clone(); + let clickable_ranges = mem::take(&mut self.clickable_ranges); + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + if let Some(mouse_up_index) = + text_state.index_for_position(bounds, event.position) + { + click_listener( + &clickable_ranges, + InteractiveTextClickEvent { + mouse_down_index, + mouse_up_index, + }, + cx, + ) + } - if phase != DispatchPhase::Bubble { - return; - } - - if active_tooltip.borrow().is_none() { - let task = cx.spawn({ - let active_tooltip = active_tooltip.clone(); - let tooltip_builder = tooltip_builder.clone(); - - move |mut cx| async move { - cx.background_executor().timer(TOOLTIP_DELAY).await; - cx.update(|cx| { - let new_tooltip = - tooltip_builder(position, cx).map(|tooltip| ActiveTooltip { - tooltip: Some(AnyTooltip { - view: tooltip, - cursor_offset: cx.mouse_position(), - }), - _task: None, - }); - *active_tooltip.borrow_mut() = new_tooltip; + mouse_down.take(); cx.refresh(); - }) - .ok(); + } + }); + } else { + let hitbox = hitbox.clone(); + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + if let Some(mouse_down_index) = + text_state.index_for_position(bounds, event.position) + { + mouse_down.set(Some(mouse_down_index)); + cx.refresh(); + } + } + }); + } + } + + cx.on_mouse_event({ + let mut hover_listener = self.hover_listener.take(); + let hitbox = hitbox.clone(); + let text_state = text_state.clone(); + let hovered_index = interactive_state.hovered_index.clone(); + move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) { + let current = hovered_index.get(); + let updated = text_state.index_for_position(bounds, event.position); + if current != updated { + hovered_index.set(updated); + if let Some(hover_listener) = hover_listener.as_ref() { + hover_listener(updated, event.clone(), cx); + } + cx.refresh(); + } + } + } + }); + + if let Some(tooltip_builder) = self.tooltip_builder.clone() { + let hitbox = hitbox.clone(); + let active_tooltip = interactive_state.active_tooltip.clone(); + let pending_mouse_down = interactive_state.mouse_down_index.clone(); + let text_state = text_state.clone(); + + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + let position = text_state.index_for_position(bounds, event.position); + let is_hovered = position.is_some() + && hitbox.is_hovered(cx) + && pending_mouse_down.get().is_none(); + if !is_hovered { + active_tooltip.take(); + return; + } + let position = position.unwrap(); + + if phase != DispatchPhase::Bubble { + return; + } + + if active_tooltip.borrow().is_none() { + let task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + let tooltip_builder = tooltip_builder.clone(); + + move |mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + cx.update(|cx| { + let new_tooltip = + tooltip_builder(position, cx).map(|tooltip| { + ActiveTooltip { + tooltip: Some(AnyTooltip { + view: tooltip, + cursor_offset: cx.mouse_position(), + }), + _task: None, + } + }); + *active_tooltip.borrow_mut() = new_tooltip; + cx.refresh(); + }) + .ok(); + } + }); + *active_tooltip.borrow_mut() = Some(ActiveTooltip { + tooltip: None, + _task: Some(task), + }); } }); - *active_tooltip.borrow_mut() = Some(ActiveTooltip { - tooltip: None, - _task: Some(task), + + let active_tooltip = interactive_state.active_tooltip.clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { + active_tooltip.take(); }); + + if let Some(tooltip) = interactive_state + .active_tooltip + .clone() + .borrow() + .as_ref() + .and_then(|at| at.tooltip.clone()) + { + cx.set_tooltip(tooltip); + } } - }); - let active_tooltip = state.active_tooltip.clone(); - cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { - active_tooltip.take(); - }); + self.text.paint(bounds, text_state, &mut (), cx); - if let Some(tooltip) = state - .active_tooltip - .clone() - .borrow() - .as_ref() - .and_then(|at| at.tooltip.clone()) - { - cx.set_tooltip(tooltip); - } - } - - self.text.paint(bounds, &mut state.text_state, cx) + ((), Some(interactive_state)) + }, + ); } } impl IntoElement for InteractiveText { type Element = Self; - fn element_id(&self) -> Option { - Some(self.element_id.clone()) - } - fn into_element(self) -> Self::Element { self } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 7a69ef98e3..55cd48b9cc 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -6,8 +6,8 @@ use crate::{ point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementContext, - ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId, - Pixels, Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, + ElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Render, + ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -42,13 +42,13 @@ where }; UniformList { - id: id.clone(), item_count, item_to_measure_index: 0, render_items: Box::new(render_range), interactivity: Interactivity { element_id: Some(id), base_style: Box::new(base_style), + occlude_mouse: true, #[cfg(debug_assertions)] location: Some(*core::panic::Location::caller()), @@ -61,7 +61,6 @@ where /// A list element for efficiently laying out and displaying a list of uniform-height elements. pub struct UniformList { - id: ElementId, item_count: usize, item_to_measure_index: usize, render_items: @@ -70,6 +69,12 @@ pub struct UniformList { scroll_handle: Option, } +/// Frame state used by the [UniformList]. +pub struct UniformListFrameState { + item_size: Size, + items: SmallVec<[AnyElement; 32]>, +} + /// A handle for controlling the scroll position of a uniform list. /// This should be stored in your view and passed to the uniform_list on each frame. #[derive(Clone, Default)] @@ -99,72 +104,47 @@ impl Styled for UniformList { } } -#[doc(hidden)] -#[derive(Default)] -pub struct UniformListState { - interactive: InteractiveElementState, - item_size: Size, -} - impl Element for UniformList { - type State = UniformListState; + type BeforeLayout = UniformListFrameState; + type AfterLayout = Option; - fn request_layout( - &mut self, - state: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { let max_items = self.item_count; - let item_size = state - .as_ref() - .map(|s| s.item_size) - .unwrap_or_else(|| self.measure_item(None, cx)); + let item_size = self.measure_item(None, cx); + let layout_id = self.interactivity.before_layout(cx, |style, cx| { + cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| { + let desired_height = item_size.height * max_items; + let width = known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width, + }); - let (layout_id, interactive) = - self.interactivity - .layout(state.map(|s| s.interactive), cx, |style, cx| { - cx.request_measured_layout( - style, - move |known_dimensions, available_space, _cx| { - let desired_height = item_size.height * max_items; - let width = - known_dimensions - .width - .unwrap_or(match available_space.width { - AvailableSpace::Definite(x) => x, - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - item_size.width - } - }); + let height = match available_space.height { + AvailableSpace::Definite(height) => desired_height.min(height), + AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height, + }; + size(width, height) + }) + }); - let height = match available_space.height { - AvailableSpace::Definite(height) => desired_height.min(height), - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - desired_height - } - }; - size(width, height) - }, - ) - }); - - let element_state = UniformListState { - interactive, - item_size, - }; - - (layout_id, element_state) + ( + layout_id, + UniformListFrameState { + item_size, + items: SmallVec::new(), + }, + ) } - fn paint( + fn after_layout( &mut self, - bounds: Bounds, - element_state: &mut Self::State, + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, cx: &mut ElementContext, - ) { - let style = - self.interactivity - .compute_style(Some(bounds), &mut element_state.interactive, cx); + ) -> Option { + let style = self.interactivity.compute_style(None, cx); let border = style.border_widths.to_pixels(cx.rem_size()); let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); @@ -174,17 +154,12 @@ impl Element for UniformList { - point(border.right + padding.right, border.bottom + padding.bottom), ); - let item_size = element_state.item_size; let content_size = Size { width: padded_bounds.size.width, - height: item_size.height * self.item_count + padding.top + padding.bottom, + height: before_layout.item_size.height * self.item_count + padding.top + padding.bottom, }; - let shared_scroll_offset = element_state - .interactive - .scroll_offset - .get_or_insert_with(Rc::default) - .clone(); + let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap(); let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height; let shared_scroll_to_item = self @@ -192,12 +167,11 @@ impl Element for UniformList { .as_mut() .and_then(|handle| handle.deferred_scroll_to_item.take()); - self.interactivity.paint( + self.interactivity.after_layout( bounds, content_size, - &mut element_state.interactive, cx, - |style, mut scroll_offset, cx| { + |style, mut scroll_offset, hitbox, cx| { let border = style.border_widths.to_pixels(cx.rem_size()); let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); @@ -240,36 +214,45 @@ impl Element for UniformList { ..cmp::min(last_visible_element_ix, self.item_count); let mut items = (self.render_items)(visible_range.clone(), cx); - cx.with_z_index(1, |cx| { - let content_mask = ContentMask { bounds }; - cx.with_content_mask(Some(content_mask), |cx| { - for (item, ix) in items.iter_mut().zip(visible_range) { - let item_origin = padded_bounds.origin - + point( - px(0.), - item_height * ix + scroll_offset.y + padding.top, - ); - let available_space = size( - AvailableSpace::Definite(padded_bounds.size.width), - AvailableSpace::Definite(item_height), - ); - item.draw(item_origin, available_space, cx); - } - }); + let content_mask = ContentMask { bounds }; + cx.with_content_mask(Some(content_mask), |cx| { + for (mut item, ix) in items.into_iter().zip(visible_range) { + let item_origin = padded_bounds.origin + + point(px(0.), item_height * ix + scroll_offset.y + padding.top); + let available_space = size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_height), + ); + item.layout(item_origin, available_space, cx); + before_layout.items.push(item); + } }); } + + hitbox }, ) } + + fn paint( + &mut self, + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, + hitbox: &mut Option, + cx: &mut ElementContext, + ) { + self.interactivity + .paint(bounds, hitbox.as_ref(), cx, |_, cx| { + for item in &mut before_layout.items { + item.paint(cx); + } + }) + } } impl IntoElement for UniformList { type Element = Self; - fn element_id(&self) -> Option { - Some(self.id.clone()) - } - fn into_element(self) -> Self::Element { self } @@ -301,7 +284,7 @@ impl UniformList { /// Track and render scroll state of this list with reference to the given scroll handle. pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { - self.interactivity.scroll_handle = Some(handle.base_handle.clone()); + self.interactivity.tracked_scroll_handle = Some(handle.base_handle.clone()); self.scroll_handle = Some(handle); self } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index dd826b68f5..c216e0684d 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -828,6 +828,28 @@ where y: self.origin.y.clone() + self.size.height.clone().half(), } } + + /// Calculates the half perimeter of a rectangle defined by the bounds. + /// + /// The half perimeter is calculated as the sum of the width and the height of the rectangle. + /// This method is generic over the type `T` which must implement the `Sub` trait to allow + /// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait + /// to sum the width and height for the half perimeter. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let half_perimeter = bounds.half_perimeter(); + /// assert_eq!(half_perimeter, 30); + /// ``` + pub fn half_perimeter(&self) -> T { + self.size.width.clone() + self.size.height.clone() + } } impl + Sub> Bounds { @@ -1145,6 +1167,22 @@ where } } +/// Checks if the bounds represent an empty area. +/// +/// # Returns +/// +/// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. +impl Bounds { + /// Checks if the bounds represent an empty area. + /// + /// # Returns + /// + /// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. + pub fn is_empty(&self) -> bool { + self.size.width <= T::default() || self.size.height <= T::default() + } +} + impl Bounds { /// Scales the bounds by a given factor, typically used to adjust for display scaling. /// @@ -2617,6 +2655,12 @@ pub trait Half { fn half(&self) -> Self; } +impl Half for i32 { + fn half(&self) -> Self { + self / 2 + } +} + impl Half for f32 { fn half(&self) -> Self { self / 2. diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index ab9898d3da..b3a30a305f 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -70,6 +70,7 @@ mod app; mod arena; mod assets; +mod bounds_tree; mod color; mod element; mod elements; diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index af56f4344f..e85b170a2a 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -54,11 +54,12 @@ use crate::{ KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext, }; use collections::FxHashMap; -use smallvec::{smallvec, SmallVec}; +use smallvec::SmallVec; use std::{ any::{Any, TypeId}, cell::RefCell, mem, + ops::Range, rc::Rc, }; @@ -68,6 +69,7 @@ pub(crate) struct DispatchNodeId(usize); pub(crate) struct DispatchTree { node_stack: Vec, pub(crate) context_stack: Vec, + view_stack: Vec, nodes: Vec, focusable_node_ids: FxHashMap, view_node_ids: FxHashMap, @@ -81,11 +83,28 @@ pub(crate) struct DispatchNode { pub key_listeners: Vec, pub action_listeners: Vec, pub context: Option, - focus_id: Option, + pub focus_id: Option, view_id: Option, parent: Option, } +pub(crate) struct ReusedSubtree { + old_range: Range, + new_range: Range, +} + +impl ReusedSubtree { + pub fn refresh_node_id(&self, node_id: DispatchNodeId) -> DispatchNodeId { + debug_assert!( + self.old_range.contains(&node_id.0), + "node {} was not part of the reused subtree {:?}", + node_id.0, + self.old_range + ); + DispatchNodeId((node_id.0 - self.old_range.start) + self.new_range.start) + } +} + type KeyListener = Rc; #[derive(Clone)] @@ -99,6 +118,7 @@ impl DispatchTree { Self { node_stack: Vec::new(), context_stack: Vec::new(), + view_stack: Vec::new(), nodes: Vec::new(), focusable_node_ids: FxHashMap::default(), view_node_ids: FxHashMap::default(), @@ -111,72 +131,124 @@ impl DispatchTree { pub fn clear(&mut self) { self.node_stack.clear(); self.context_stack.clear(); + self.view_stack.clear(); self.nodes.clear(); self.focusable_node_ids.clear(); self.view_node_ids.clear(); self.keystroke_matchers.clear(); } - pub fn push_node( - &mut self, - context: Option, - focus_id: Option, - view_id: Option, - ) { + pub fn len(&self) -> usize { + self.nodes.len() + } + + pub fn push_node(&mut self) -> DispatchNodeId { let parent = self.node_stack.last().copied(); let node_id = DispatchNodeId(self.nodes.len()); + self.nodes.push(DispatchNode { parent, - focus_id, - view_id, ..Default::default() }); self.node_stack.push(node_id); + node_id + } - if let Some(context) = context { - self.active_node().context = Some(context.clone()); - self.context_stack.push(context); + pub fn set_active_node(&mut self, node_id: DispatchNodeId) { + let next_node_parent = self.nodes[node_id.0].parent; + while self.node_stack.last().copied() != next_node_parent && !self.node_stack.is_empty() { + self.pop_node(); } - if let Some(focus_id) = focus_id { - self.focusable_node_ids.insert(focus_id, node_id); - } + if self.node_stack.last().copied() == next_node_parent { + self.node_stack.push(node_id); + let active_node = &self.nodes[node_id.0]; + if let Some(view_id) = active_node.view_id { + self.view_stack.push(view_id) + } + if let Some(context) = active_node.context.clone() { + self.context_stack.push(context); + } + } else { + debug_assert_eq!(self.node_stack.len(), 0); - if let Some(view_id) = view_id { + let mut current_node_id = Some(node_id); + while let Some(node_id) = current_node_id { + let node = &self.nodes[node_id.0]; + if let Some(context) = node.context.clone() { + self.context_stack.push(context); + } + if node.view_id.is_some() { + self.view_stack.push(node.view_id.unwrap()); + } + self.node_stack.push(node_id); + current_node_id = node.parent; + } + + self.context_stack.reverse(); + self.view_stack.reverse(); + self.node_stack.reverse(); + } + } + + pub fn set_key_context(&mut self, context: KeyContext) { + self.active_node().context = Some(context.clone()); + self.context_stack.push(context); + } + + pub fn set_focus_id(&mut self, focus_id: FocusId) { + let node_id = *self.node_stack.last().unwrap(); + self.nodes[node_id.0].focus_id = Some(focus_id); + self.focusable_node_ids.insert(focus_id, node_id); + } + + pub fn set_view_id(&mut self, view_id: EntityId) { + if self.view_stack.last().copied() != Some(view_id) { + let node_id = *self.node_stack.last().unwrap(); + self.nodes[node_id.0].view_id = Some(view_id); self.view_node_ids.insert(view_id, node_id); + self.view_stack.push(view_id); } } pub fn pop_node(&mut self) { - let node = &self.nodes[self.active_node_id().0]; + let node = &self.nodes[self.active_node_id().unwrap().0]; if node.context.is_some() { self.context_stack.pop(); } + if node.view_id.is_some() { + self.view_stack.pop(); + } self.node_stack.pop(); } fn move_node(&mut self, source: &mut DispatchNode) { - self.push_node(source.context.take(), source.focus_id, source.view_id); + self.push_node(); + if let Some(context) = source.context.clone() { + self.set_key_context(context); + } + if let Some(focus_id) = source.focus_id { + self.set_focus_id(focus_id); + } + if let Some(view_id) = source.view_id { + self.set_view_id(view_id); + } + let target = self.active_node(); target.key_listeners = mem::take(&mut source.key_listeners); target.action_listeners = mem::take(&mut source.action_listeners); } - pub fn reuse_view(&mut self, view_id: EntityId, source: &mut Self) -> SmallVec<[EntityId; 8]> { - let view_source_node_id = source - .view_node_ids - .get(&view_id) - .expect("view should exist in previous dispatch tree"); - let view_source_node = &mut source.nodes[view_source_node_id.0]; - self.move_node(view_source_node); + pub fn reuse_subtree(&mut self, old_range: Range, source: &mut Self) -> ReusedSubtree { + let new_range = self.nodes.len()..self.nodes.len() + old_range.len(); - let mut grafted_view_ids = smallvec![view_id]; - let mut source_stack = vec![*view_source_node_id]; + let mut source_stack = vec![]; for (source_node_id, source_node) in source .nodes .iter_mut() .enumerate() - .skip(view_source_node_id.0 + 1) + .skip(old_range.start) + .take(old_range.len()) { let source_node_id = DispatchNodeId(source_node_id); while let Some(source_ancestor) = source_stack.last() { @@ -188,15 +260,8 @@ impl DispatchTree { } } - if source_stack.is_empty() { - break; - } else { - source_stack.push(source_node_id); - self.move_node(source_node); - if let Some(view_id) = source_node.view_id { - grafted_view_ids.push(view_id); - } - } + source_stack.push(source_node_id); + self.move_node(source_node); } while !source_stack.is_empty() { @@ -204,7 +269,10 @@ impl DispatchTree { self.pop_node(); } - grafted_view_ids + ReusedSubtree { + old_range, + new_range, + } } pub fn clear_pending_keystrokes(&mut self) { @@ -424,7 +492,7 @@ impl DispatchTree { } fn active_node(&mut self) -> &mut DispatchNode { - let active_node_id = self.active_node_id(); + let active_node_id = self.active_node_id().unwrap(); &mut self.nodes[active_node_id.0] } @@ -437,8 +505,8 @@ impl DispatchTree { DispatchNodeId(0) } - fn active_node_id(&self) -> DispatchNodeId { - *self.node_stack.last().unwrap() + pub fn active_node_id(&self) -> Option { + self.node_stack.last().copied() } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 9dce3781f7..176d391d82 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1,6 +1,6 @@ // todo(linux): remove #![cfg_attr(target_os = "linux", allow(dead_code))] -// todo(windows): remove +// todo("windows"): remove #![cfg_attr(windows, allow(dead_code))] mod app_menu; @@ -68,7 +68,7 @@ pub(crate) fn current_platform() -> Rc { pub(crate) fn current_platform() -> Rc { Rc::new(LinuxPlatform::new()) } -// todo(windows) +// todo("windows") #[cfg(target_os = "windows")] pub(crate) fn current_platform() -> Rc { Rc::new(WindowsPlatform::new()) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index bc862c0996..92c9b57216 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -292,6 +292,7 @@ impl MetalRenderer { znear: 0.0, zfar: 1.0, }); + for batch in scene.batches() { let ok = match batch { PrimitiveBatch::Shadows(shadows) => self.draw_shadows( diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index b56db48bd3..8726d472b7 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -126,7 +126,7 @@ impl Platform for TestPlatform { #[cfg(target_os = "macos")] return Arc::new(crate::platform::mac::MacTextSystem::new()); - // todo(windows) + // todo("windows") #[cfg(target_os = "windows")] unimplemented!() } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 61141b2c4d..21111c8bc7 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -1,48 +1,22 @@ -// todo(windows): remove +// todo("windows"): remove #![cfg_attr(windows, allow(dead_code))] use crate::{ - point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, EntityId, Hsla, Pixels, - Point, ScaledPixels, StackingOrder, + bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, + Hsla, Pixels, Point, ScaledPixels, }; -use collections::{BTreeMap, FxHashSet}; -use std::{fmt::Debug, iter::Peekable, slice}; +use std::{fmt::Debug, iter::Peekable, ops::Range, slice}; #[allow(non_camel_case_types, unused)] pub(crate) type PathVertex_ScaledPixels = PathVertex; -pub(crate) type LayerId = u32; pub(crate) type DrawOrder = u32; -#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)] -#[repr(C)] -pub(crate) struct ViewId { - low_bits: u32, - high_bits: u32, -} - -impl From for ViewId { - fn from(value: EntityId) -> Self { - let value = value.as_u64(); - Self { - low_bits: value as u32, - high_bits: (value >> 32) as u32, - } - } -} - -impl From for EntityId { - fn from(value: ViewId) -> Self { - let value = (value.low_bits as u64) | ((value.high_bits as u64) << 32); - value.into() - } -} - #[derive(Default)] pub(crate) struct Scene { - last_layer: Option<(StackingOrder, LayerId)>, - layers_by_order: BTreeMap, - orders_by_layer: BTreeMap, + pub(crate) paint_operations: Vec, + primitive_bounds: BoundsTree, + layer_stack: Vec, pub(crate) shadows: Vec, pub(crate) quads: Vec, pub(crate) paths: Vec>, @@ -54,12 +28,12 @@ pub(crate) struct Scene { impl Scene { pub fn clear(&mut self) { - self.last_layer = None; - self.layers_by_order.clear(); - self.orders_by_layer.clear(); + self.paint_operations.clear(); + self.primitive_bounds.clear(); + self.layer_stack.clear(); + self.paths.clear(); self.shadows.clear(); self.quads.clear(); - self.paths.clear(); self.underlines.clear(); self.monochrome_sprites.clear(); self.polychrome_sprites.clear(); @@ -70,6 +44,92 @@ impl Scene { &self.paths } + pub fn len(&self) -> usize { + self.paint_operations.len() + } + + pub fn push_layer(&mut self, bounds: Bounds) { + let order = self.primitive_bounds.insert(bounds); + self.layer_stack.push(order); + self.paint_operations + .push(PaintOperation::StartLayer(bounds)); + } + + pub fn pop_layer(&mut self) { + self.layer_stack.pop(); + self.paint_operations.push(PaintOperation::EndLayer); + } + + pub fn insert_primitive(&mut self, primitive: impl Into) { + let mut primitive = primitive.into(); + let clipped_bounds = primitive + .bounds() + .intersect(&primitive.content_mask().bounds); + + if clipped_bounds.is_empty() { + return; + } + + let order = self + .layer_stack + .last() + .copied() + .unwrap_or_else(|| self.primitive_bounds.insert(clipped_bounds)); + match &mut primitive { + Primitive::Shadow(shadow) => { + shadow.order = order; + self.shadows.push(shadow.clone()); + } + Primitive::Quad(quad) => { + quad.order = order; + self.quads.push(quad.clone()); + } + Primitive::Path(path) => { + path.order = order; + path.id = PathId(self.paths.len()); + self.paths.push(path.clone()); + } + Primitive::Underline(underline) => { + underline.order = order; + self.underlines.push(underline.clone()); + } + Primitive::MonochromeSprite(sprite) => { + sprite.order = order; + self.monochrome_sprites.push(sprite.clone()); + } + Primitive::PolychromeSprite(sprite) => { + sprite.order = order; + self.polychrome_sprites.push(sprite.clone()); + } + Primitive::Surface(surface) => { + surface.order = order; + self.surfaces.push(surface.clone()); + } + } + self.paint_operations + .push(PaintOperation::Primitive(primitive)); + } + + pub fn replay(&mut self, range: Range, prev_scene: &Scene) { + for operation in &prev_scene.paint_operations[range] { + match operation { + PaintOperation::Primitive(primitive) => self.insert_primitive(primitive.clone()), + PaintOperation::StartLayer(bounds) => self.push_layer(*bounds), + PaintOperation::EndLayer => self.pop_layer(), + } + } + } + + pub fn finish(&mut self) { + self.shadows.sort(); + self.quads.sort(); + self.paths.sort(); + self.underlines.sort(); + self.monochrome_sprites.sort(); + self.polychrome_sprites.sort(); + self.surfaces.sort(); + } + pub(crate) fn batches(&self) -> impl Iterator { BatchIterator { shadows: &self.shadows, @@ -95,162 +155,60 @@ impl Scene { surfaces_iter: self.surfaces.iter().peekable(), } } +} - pub(crate) fn insert(&mut self, order: &StackingOrder, primitive: impl Into) { - let primitive = primitive.into(); - let clipped_bounds = primitive - .bounds() - .intersect(&primitive.content_mask().bounds); - if clipped_bounds.size.width <= ScaledPixels(0.) - || clipped_bounds.size.height <= ScaledPixels(0.) - { - return; - } +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] +pub(crate) enum PrimitiveKind { + Shadow, + #[default] + Quad, + Path, + Underline, + MonochromeSprite, + PolychromeSprite, + Surface, +} - let layer_id = self.layer_id_for_order(order); - match primitive { - Primitive::Shadow(mut shadow) => { - shadow.layer_id = layer_id; - self.shadows.push(shadow); - } - Primitive::Quad(mut quad) => { - quad.layer_id = layer_id; - self.quads.push(quad); - } - Primitive::Path(mut path) => { - path.layer_id = layer_id; - path.id = PathId(self.paths.len()); - self.paths.push(path); - } - Primitive::Underline(mut underline) => { - underline.layer_id = layer_id; - self.underlines.push(underline); - } - Primitive::MonochromeSprite(mut sprite) => { - sprite.layer_id = layer_id; - self.monochrome_sprites.push(sprite); - } - Primitive::PolychromeSprite(mut sprite) => { - sprite.layer_id = layer_id; - self.polychrome_sprites.push(sprite); - } - Primitive::Surface(mut surface) => { - surface.layer_id = layer_id; - self.surfaces.push(surface); - } +pub(crate) enum PaintOperation { + Primitive(Primitive), + StartLayer(Bounds), + EndLayer, +} + +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)] +pub(crate) enum Primitive { + Shadow(Shadow), + Quad(Quad), + Path(Path), + Underline(Underline), + MonochromeSprite(MonochromeSprite), + PolychromeSprite(PolychromeSprite), + Surface(Surface), +} + +impl Primitive { + pub fn bounds(&self) -> &Bounds { + match self { + Primitive::Shadow(shadow) => &shadow.bounds, + Primitive::Quad(quad) => &quad.bounds, + Primitive::Path(path) => &path.bounds, + Primitive::Underline(underline) => &underline.bounds, + Primitive::MonochromeSprite(sprite) => &sprite.bounds, + Primitive::PolychromeSprite(sprite) => &sprite.bounds, + Primitive::Surface(surface) => &surface.bounds, } } - fn layer_id_for_order(&mut self, order: &StackingOrder) -> LayerId { - if let Some((last_order, last_layer_id)) = self.last_layer.as_ref() { - if order == last_order { - return *last_layer_id; - } + pub fn content_mask(&self) -> &ContentMask { + match self { + Primitive::Shadow(shadow) => &shadow.content_mask, + Primitive::Quad(quad) => &quad.content_mask, + Primitive::Path(path) => &path.content_mask, + Primitive::Underline(underline) => &underline.content_mask, + Primitive::MonochromeSprite(sprite) => &sprite.content_mask, + Primitive::PolychromeSprite(sprite) => &sprite.content_mask, + Primitive::Surface(surface) => &surface.content_mask, } - - let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) { - *layer_id - } else { - let next_id = self.layers_by_order.len() as LayerId; - self.layers_by_order.insert(order.clone(), next_id); - self.orders_by_layer.insert(next_id, order.clone()); - next_id - }; - self.last_layer = Some((order.clone(), layer_id)); - layer_id - } - - pub fn reuse_views(&mut self, views: &FxHashSet, prev_scene: &mut Self) { - for shadow in prev_scene.shadows.drain(..) { - if views.contains(&shadow.view_id.into()) { - let order = &prev_scene.orders_by_layer[&shadow.layer_id]; - self.insert(order, shadow); - } - } - - for quad in prev_scene.quads.drain(..) { - if views.contains(&quad.view_id.into()) { - let order = &prev_scene.orders_by_layer[&quad.layer_id]; - self.insert(order, quad); - } - } - - for path in prev_scene.paths.drain(..) { - if views.contains(&path.view_id.into()) { - let order = &prev_scene.orders_by_layer[&path.layer_id]; - self.insert(order, path); - } - } - - for underline in prev_scene.underlines.drain(..) { - if views.contains(&underline.view_id.into()) { - let order = &prev_scene.orders_by_layer[&underline.layer_id]; - self.insert(order, underline); - } - } - - for sprite in prev_scene.monochrome_sprites.drain(..) { - if views.contains(&sprite.view_id.into()) { - let order = &prev_scene.orders_by_layer[&sprite.layer_id]; - self.insert(order, sprite); - } - } - - for sprite in prev_scene.polychrome_sprites.drain(..) { - if views.contains(&sprite.view_id.into()) { - let order = &prev_scene.orders_by_layer[&sprite.layer_id]; - self.insert(order, sprite); - } - } - - for surface in prev_scene.surfaces.drain(..) { - if views.contains(&surface.view_id.into()) { - let order = &prev_scene.orders_by_layer[&surface.layer_id]; - self.insert(order, surface); - } - } - } - - pub fn finish(&mut self) { - let mut orders = vec![0; self.layers_by_order.len()]; - for (ix, layer_id) in self.layers_by_order.values().enumerate() { - orders[*layer_id as usize] = ix as u32; - } - - for shadow in &mut self.shadows { - shadow.order = orders[shadow.layer_id as usize]; - } - self.shadows.sort_by_key(|shadow| shadow.order); - - for quad in &mut self.quads { - quad.order = orders[quad.layer_id as usize]; - } - self.quads.sort_by_key(|quad| quad.order); - - for path in &mut self.paths { - path.order = orders[path.layer_id as usize]; - } - self.paths.sort_by_key(|path| path.order); - - for underline in &mut self.underlines { - underline.order = orders[underline.layer_id as usize]; - } - self.underlines.sort_by_key(|underline| underline.order); - - for monochrome_sprite in &mut self.monochrome_sprites { - monochrome_sprite.order = orders[monochrome_sprite.layer_id as usize]; - } - self.monochrome_sprites.sort_by_key(|sprite| sprite.order); - - for polychrome_sprite in &mut self.polychrome_sprites { - polychrome_sprite.order = orders[polychrome_sprite.layer_id as usize]; - } - self.polychrome_sprites.sort_by_key(|sprite| sprite.order); - - for surface in &mut self.surfaces { - surface.order = orders[surface.layer_id as usize]; - } - self.surfaces.sort_by_key(|surface| surface.order); } } @@ -439,54 +397,6 @@ impl<'a> Iterator for BatchIterator<'a> { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] -pub(crate) enum PrimitiveKind { - Shadow, - #[default] - Quad, - Path, - Underline, - MonochromeSprite, - PolychromeSprite, - Surface, -} - -pub(crate) enum Primitive { - Shadow(Shadow), - Quad(Quad), - Path(Path), - Underline(Underline), - MonochromeSprite(MonochromeSprite), - PolychromeSprite(PolychromeSprite), - Surface(Surface), -} - -impl Primitive { - pub fn bounds(&self) -> &Bounds { - match self { - Primitive::Shadow(shadow) => &shadow.bounds, - Primitive::Quad(quad) => &quad.bounds, - Primitive::Path(path) => &path.bounds, - Primitive::Underline(underline) => &underline.bounds, - Primitive::MonochromeSprite(sprite) => &sprite.bounds, - Primitive::PolychromeSprite(sprite) => &sprite.bounds, - Primitive::Surface(surface) => &surface.bounds, - } - } - - pub fn content_mask(&self) -> &ContentMask { - match self { - Primitive::Shadow(shadow) => &shadow.content_mask, - Primitive::Quad(quad) => &quad.content_mask, - Primitive::Path(path) => &path.content_mask, - Primitive::Underline(underline) => &underline.content_mask, - Primitive::MonochromeSprite(sprite) => &sprite.content_mask, - Primitive::PolychromeSprite(sprite) => &sprite.content_mask, - Primitive::Surface(surface) => &surface.content_mask, - } - } -} - #[derive(Debug)] pub(crate) enum PrimitiveBatch<'a> { Shadows(&'a [Shadow]), @@ -507,8 +417,6 @@ pub(crate) enum PrimitiveBatch<'a> { #[derive(Default, Debug, Clone, Eq, PartialEq)] #[repr(C)] pub(crate) struct Quad { - pub view_id: ViewId, - pub layer_id: LayerId, pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, @@ -539,8 +447,6 @@ impl From for Primitive { #[derive(Debug, Clone, Eq, PartialEq)] #[repr(C)] pub(crate) struct Underline { - pub view_id: ViewId, - pub layer_id: LayerId, pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, @@ -570,8 +476,6 @@ impl From for Primitive { #[derive(Debug, Clone, Eq, PartialEq)] #[repr(C)] pub(crate) struct Shadow { - pub view_id: ViewId, - pub layer_id: LayerId, pub order: DrawOrder, pub bounds: Bounds, pub corner_radii: Corners, @@ -602,8 +506,6 @@ impl From for Primitive { #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] pub(crate) struct MonochromeSprite { - pub view_id: ViewId, - pub layer_id: LayerId, pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, @@ -635,8 +537,6 @@ impl From for Primitive { #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] pub(crate) struct PolychromeSprite { - pub view_id: ViewId, - pub layer_id: LayerId, pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, @@ -669,8 +569,6 @@ impl From for Primitive { #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Surface { - pub view_id: ViewId, - pub layer_id: LayerId, pub order: DrawOrder, pub bounds: Bounds, pub content_mask: ContentMask, @@ -700,11 +598,9 @@ impl From for Primitive { pub(crate) struct PathId(pub(crate) usize); /// A line made up of a series of vertices and control points. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Path { pub(crate) id: PathId, - pub(crate) view_id: ViewId, - layer_id: LayerId, order: DrawOrder, pub(crate) bounds: Bounds

, pub(crate) content_mask: ContentMask

, @@ -720,8 +616,6 @@ impl Path { pub fn new(start: Point) -> Self { Self { id: PathId(0), - view_id: ViewId::default(), - layer_id: LayerId::default(), order: DrawOrder::default(), vertices: Vec::new(), start, @@ -740,8 +634,6 @@ impl Path { pub fn scale(&self, factor: f32) -> Path { Path { id: self.id, - view_id: self.view_id, - layer_id: self.layer_id, order: self.order, bounds: self.bounds.scale(factor), content_mask: self.content_mask.scale(factor), diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 6c8fb400f2..9a002d6700 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -115,9 +115,6 @@ pub struct Style { /// The mouse cursor style shown when the mouse pointer is over an element. pub mouse_cursor: Option, - /// The z-index to set for this element - pub z_index: Option, - /// Whether to draw a red debugging outline around this element #[cfg(debug_assertions)] pub debug: bool, @@ -323,6 +320,13 @@ pub struct HighlightStyle { impl Eq for HighlightStyle {} impl Style { + /// Returns true if the style is visible and the background is opaque. + pub fn has_opaque_background(&self) -> bool { + self.background + .as_ref() + .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent())) + } + /// Get the text style in this element style. pub fn text_style(&self) -> Option<&TextStyleRefinement> { if self.text.is_some() { @@ -402,97 +406,87 @@ impl Style { let rem_size = cx.rem_size(); - cx.with_z_index(0, |cx| { - cx.paint_shadows( - bounds, - self.corner_radii.to_pixels(bounds.size, rem_size), - &self.box_shadow, - ); - }); + cx.paint_shadows( + bounds, + self.corner_radii.to_pixels(bounds.size, rem_size), + &self.box_shadow, + ); let background_color = self.background.as_ref().and_then(Fill::color); if background_color.map_or(false, |color| !color.is_transparent()) { - cx.with_z_index(1, |cx| { - let mut border_color = background_color.unwrap_or_default(); - border_color.a = 0.; - cx.paint_quad(quad( - bounds, - self.corner_radii.to_pixels(bounds.size, rem_size), - background_color.unwrap_or_default(), - Edges::default(), - border_color, - )); - }); + let mut border_color = background_color.unwrap_or_default(); + border_color.a = 0.; + cx.paint_quad(quad( + bounds, + self.corner_radii.to_pixels(bounds.size, rem_size), + background_color.unwrap_or_default(), + Edges::default(), + border_color, + )); } - cx.with_z_index(2, |cx| { - continuation(cx); - }); + continuation(cx); if self.is_border_visible() { - cx.with_z_index(3, |cx| { - let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size); - let border_widths = self.border_widths.to_pixels(rem_size); - let max_border_width = border_widths.max(); - let max_corner_radius = corner_radii.max(); + let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size); + let border_widths = self.border_widths.to_pixels(rem_size); + let max_border_width = border_widths.max(); + let max_corner_radius = corner_radii.max(); - let top_bounds = Bounds::from_corners( - bounds.origin, - bounds.upper_right() - + point(Pixels::ZERO, max_border_width.max(max_corner_radius)), - ); - let bottom_bounds = Bounds::from_corners( - bounds.lower_left() - - point(Pixels::ZERO, max_border_width.max(max_corner_radius)), - bounds.lower_right(), - ); - let left_bounds = Bounds::from_corners( - top_bounds.lower_left(), - bottom_bounds.origin + point(max_border_width, Pixels::ZERO), - ); - let right_bounds = Bounds::from_corners( - top_bounds.lower_right() - point(max_border_width, Pixels::ZERO), - bottom_bounds.upper_right(), - ); + let top_bounds = Bounds::from_corners( + bounds.origin, + bounds.upper_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)), + ); + let bottom_bounds = Bounds::from_corners( + bounds.lower_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)), + bounds.lower_right(), + ); + let left_bounds = Bounds::from_corners( + top_bounds.lower_left(), + bottom_bounds.origin + point(max_border_width, Pixels::ZERO), + ); + let right_bounds = Bounds::from_corners( + top_bounds.lower_right() - point(max_border_width, Pixels::ZERO), + bottom_bounds.upper_right(), + ); - let mut background = self.border_color.unwrap_or_default(); - background.a = 0.; - let quad = quad( - bounds, - corner_radii, - background, - border_widths, - self.border_color.unwrap_or_default(), - ); + let mut background = self.border_color.unwrap_or_default(); + background.a = 0.; + let quad = quad( + bounds, + corner_radii, + background, + border_widths, + self.border_color.unwrap_or_default(), + ); - cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| { - cx.paint_quad(quad.clone()); - }); - cx.with_content_mask( - Some(ContentMask { - bounds: right_bounds, - }), - |cx| { - cx.paint_quad(quad.clone()); - }, - ); - cx.with_content_mask( - Some(ContentMask { - bounds: bottom_bounds, - }), - |cx| { - cx.paint_quad(quad.clone()); - }, - ); - cx.with_content_mask( - Some(ContentMask { - bounds: left_bounds, - }), - |cx| { - cx.paint_quad(quad); - }, - ); + cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| { + cx.paint_quad(quad.clone()); }); + cx.with_content_mask( + Some(ContentMask { + bounds: right_bounds, + }), + |cx| { + cx.paint_quad(quad.clone()); + }, + ); + cx.with_content_mask( + Some(ContentMask { + bounds: bottom_bounds, + }), + |cx| { + cx.paint_quad(quad.clone()); + }, + ); + cx.with_content_mask( + Some(ContentMask { + bounds: left_bounds, + }), + |cx| { + cx.paint_quad(quad); + }, + ); } #[cfg(debug_assertions)] @@ -545,7 +539,6 @@ impl Default for Style { box_shadow: Default::default(), text: TextStyleRefinement::default(), mouse_cursor: None, - z_index: None, #[cfg(debug_assertions)] debug: false, diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 928f9f0a23..854559c102 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -15,12 +15,6 @@ pub trait Styled: Sized { gpui_macros::style_helpers!(); - /// Set the z-index of this element. - fn z_index(mut self, z_index: u16) -> Self { - self.style().z_index = Some(z_index); - self - } - /// Sets the position of the element to `relative`. /// [Docs](https://tailwindcss.com/docs/position) fn relative(mut self) -> Self { diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 0797c8f3b4..248948d071 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -47,11 +47,7 @@ impl TaffyLayoutEngine { self.styles.clear(); } - pub fn requested_style(&self, layout_id: LayoutId) -> Option<&Style> { - self.styles.get(&layout_id) - } - - pub fn request_layout( + pub fn before_layout( &mut self, style: &Style, rem_size: Pixels, @@ -447,6 +443,27 @@ pub enum AvailableSpace { MaxContent, } +impl AvailableSpace { + /// Returns a `Size` with both width and height set to `AvailableSpace::MinContent`. + /// + /// This function is useful when you want to create a `Size` with the minimum content constraints + /// for both dimensions. + /// + /// # Examples + /// + /// ``` + /// let min_content_size = AvailableSpace::min_size(); + /// assert_eq!(min_content_size.width, AvailableSpace::MinContent); + /// assert_eq!(min_content_size.height, AvailableSpace::MinContent); + /// ``` + pub const fn min_size() -> Size { + Size { + width: Self::MinContent, + height: Self::MinContent, + } + } +} + impl From for TaffyAvailableSpace { fn from(space: AvailableSpace) -> TaffyAvailableSpace { match space { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index d5c7510db1..034d15ae34 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -9,11 +9,11 @@ pub use line_layout::*; pub use line_wrapper::*; use crate::{ - px, Bounds, DevicePixels, EntityId, Hsla, Pixels, PlatformTextSystem, Point, Result, - SharedString, Size, StrikethroughStyle, UnderlineStyle, + px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, + StrikethroughStyle, UnderlineStyle, }; use anyhow::anyhow; -use collections::{BTreeSet, FxHashMap, FxHashSet}; +use collections::{BTreeSet, FxHashMap}; use core::fmt; use derive_more::Deref; use itertools::Itertools; @@ -24,7 +24,7 @@ use std::{ cmp, fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, sync::Arc, }; @@ -279,7 +279,7 @@ impl TextSystem { /// The GPUI text layout subsystem. #[derive(Deref)] pub struct WindowTextSystem { - line_layout_cache: Arc, + line_layout_cache: LineLayoutCache, #[deref] text_system: Arc, } @@ -287,15 +287,17 @@ pub struct WindowTextSystem { impl WindowTextSystem { pub(crate) fn new(text_system: Arc) -> Self { Self { - line_layout_cache: Arc::new(LineLayoutCache::new( - text_system.platform_text_system.clone(), - )), + line_layout_cache: LineLayoutCache::new(text_system.platform_text_system.clone()), text_system, } } - pub(crate) fn with_view(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R { - self.line_layout_cache.with_view(view_id, f) + pub(crate) fn layout_index(&self) -> LineLayoutIndex { + self.line_layout_cache.layout_index() + } + + pub(crate) fn reuse_layouts(&self, index: Range) { + self.line_layout_cache.reuse_layouts(index) } /// Shape the given line, at the given font_size, for painting to the screen. @@ -455,8 +457,8 @@ impl WindowTextSystem { Ok(lines) } - pub(crate) fn finish_frame(&self, reused_views: &FxHashSet) { - self.line_layout_cache.finish_frame(reused_views) + pub(crate) fn finish_frame(&self) { + self.line_layout_cache.finish_frame() } /// Layout the given line of text, at the given font_size. diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index fbf34d39b2..855cfaf37e 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -107,212 +107,218 @@ fn paint_line( line_height: Pixels, decoration_runs: &[DecorationRun], wrap_boundaries: &[WrapBoundary], - cx: &mut ElementContext<'_>, + cx: &mut ElementContext, ) -> Result<()> { - let padding_top = (line_height - layout.ascent - layout.descent) / 2.; - let baseline_offset = point(px(0.), padding_top + layout.ascent); - let mut decoration_runs = decoration_runs.iter(); - let mut wraps = wrap_boundaries.iter().peekable(); - let mut run_end = 0; - let mut color = black(); - let mut current_underline: Option<(Point, UnderlineStyle)> = None; - let mut current_strikethrough: Option<(Point, StrikethroughStyle)> = None; - let mut current_background: Option<(Point, Hsla)> = None; - let text_system = cx.text_system().clone(); - let mut glyph_origin = origin; - let mut prev_glyph_position = Point::default(); - for (run_ix, run) in layout.runs.iter().enumerate() { - let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; + let line_bounds = Bounds::new(origin, size(layout.width, line_height)); + cx.paint_layer(line_bounds, |cx| { + let padding_top = (line_height - layout.ascent - layout.descent) / 2.; + let baseline_offset = point(px(0.), padding_top + layout.ascent); + let mut decoration_runs = decoration_runs.iter(); + let mut wraps = wrap_boundaries.iter().peekable(); + let mut run_end = 0; + let mut color = black(); + let mut current_underline: Option<(Point, UnderlineStyle)> = None; + let mut current_strikethrough: Option<(Point, StrikethroughStyle)> = None; + let mut current_background: Option<(Point, Hsla)> = None; + let text_system = cx.text_system().clone(); + let mut glyph_origin = origin; + let mut prev_glyph_position = Point::default(); + for (run_ix, run) in layout.runs.iter().enumerate() { + let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; - for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { - glyph_origin.x += glyph.position.x - prev_glyph_position.x; + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; - if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { - wraps.next(); - if let Some((background_origin, background_color)) = current_background.as_mut() { + if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { + wraps.next(); + if let Some((background_origin, background_color)) = current_background.as_mut() + { + cx.paint_quad(fill( + Bounds { + origin: *background_origin, + size: size(glyph_origin.x - background_origin.x, line_height), + }, + *background_color, + )); + background_origin.x = origin.x; + background_origin.y += line_height; + } + if let Some((underline_origin, underline_style)) = current_underline.as_mut() { + cx.paint_underline( + *underline_origin, + glyph_origin.x - underline_origin.x, + underline_style, + ); + underline_origin.x = origin.x; + underline_origin.y += line_height; + } + if let Some((strikethrough_origin, strikethrough_style)) = + current_strikethrough.as_mut() + { + cx.paint_strikethrough( + *strikethrough_origin, + glyph_origin.x - strikethrough_origin.x, + strikethrough_style, + ); + strikethrough_origin.x = origin.x; + strikethrough_origin.y += line_height; + } + + glyph_origin.x = origin.x; + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; + + let mut finished_background: Option<(Point, Hsla)> = None; + let mut finished_underline: Option<(Point, UnderlineStyle)> = None; + let mut finished_strikethrough: Option<(Point, StrikethroughStyle)> = None; + if glyph.index >= run_end { + if let Some(style_run) = decoration_runs.next() { + if let Some((_, background_color)) = &mut current_background { + if style_run.background_color.as_ref() != Some(background_color) { + finished_background = current_background.take(); + } + } + if let Some(run_background) = style_run.background_color { + current_background.get_or_insert(( + point(glyph_origin.x, glyph_origin.y), + run_background, + )); + } + + if let Some((_, underline_style)) = &mut current_underline { + if style_run.underline.as_ref() != Some(underline_style) { + finished_underline = current_underline.take(); + } + } + if let Some(run_underline) = style_run.underline.as_ref() { + current_underline.get_or_insert(( + point( + glyph_origin.x, + glyph_origin.y + baseline_offset.y + (layout.descent * 0.618), + ), + UnderlineStyle { + color: Some(run_underline.color.unwrap_or(style_run.color)), + thickness: run_underline.thickness, + wavy: run_underline.wavy, + }, + )); + } + if let Some((_, strikethrough_style)) = &mut current_strikethrough { + if style_run.strikethrough.as_ref() != Some(strikethrough_style) { + finished_strikethrough = current_strikethrough.take(); + } + } + if let Some(run_strikethrough) = style_run.strikethrough.as_ref() { + current_strikethrough.get_or_insert(( + point( + glyph_origin.x, + glyph_origin.y + + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5), + ), + StrikethroughStyle { + color: Some(run_strikethrough.color.unwrap_or(style_run.color)), + thickness: run_strikethrough.thickness, + }, + )); + } + + run_end += style_run.len as usize; + color = style_run.color; + } else { + run_end = layout.len; + finished_background = current_background.take(); + finished_underline = current_underline.take(); + finished_strikethrough = current_strikethrough.take(); + } + } + + if let Some((background_origin, background_color)) = finished_background { cx.paint_quad(fill( Bounds { - origin: *background_origin, + origin: background_origin, size: size(glyph_origin.x - background_origin.x, line_height), }, - *background_color, + background_color, )); - background_origin.x = origin.x; - background_origin.y += line_height; } - if let Some((underline_origin, underline_style)) = current_underline.as_mut() { + + if let Some((underline_origin, underline_style)) = finished_underline { cx.paint_underline( - *underline_origin, + underline_origin, glyph_origin.x - underline_origin.x, - underline_style, + &underline_style, ); - underline_origin.x = origin.x; - underline_origin.y += line_height; } - if let Some((strikethrough_origin, strikethrough_style)) = - current_strikethrough.as_mut() - { + + if let Some((strikethrough_origin, strikethrough_style)) = finished_strikethrough { cx.paint_strikethrough( - *strikethrough_origin, + strikethrough_origin, glyph_origin.x - strikethrough_origin.x, - strikethrough_style, + &strikethrough_style, ); - strikethrough_origin.x = origin.x; - strikethrough_origin.y += line_height; } - glyph_origin.x = origin.x; - glyph_origin.y += line_height; - } - prev_glyph_position = glyph.position; + let max_glyph_bounds = Bounds { + origin: glyph_origin, + size: max_glyph_size, + }; - let mut finished_background: Option<(Point, Hsla)> = None; - let mut finished_underline: Option<(Point, UnderlineStyle)> = None; - let mut finished_strikethrough: Option<(Point, StrikethroughStyle)> = None; - if glyph.index >= run_end { - if let Some(style_run) = decoration_runs.next() { - if let Some((_, background_color)) = &mut current_background { - if style_run.background_color.as_ref() != Some(background_color) { - finished_background = current_background.take(); - } + let content_mask = cx.content_mask(); + if max_glyph_bounds.intersects(&content_mask.bounds) { + if glyph.is_emoji { + cx.paint_emoji( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + )?; + } else { + cx.paint_glyph( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + color, + )?; } - if let Some(run_background) = style_run.background_color { - current_background - .get_or_insert((point(glyph_origin.x, glyph_origin.y), run_background)); - } - - if let Some((_, underline_style)) = &mut current_underline { - if style_run.underline.as_ref() != Some(underline_style) { - finished_underline = current_underline.take(); - } - } - if let Some(run_underline) = style_run.underline.as_ref() { - current_underline.get_or_insert(( - point( - glyph_origin.x, - glyph_origin.y + baseline_offset.y + (layout.descent * 0.618), - ), - UnderlineStyle { - color: Some(run_underline.color.unwrap_or(style_run.color)), - thickness: run_underline.thickness, - wavy: run_underline.wavy, - }, - )); - } - if let Some((_, strikethrough_style)) = &mut current_strikethrough { - if style_run.strikethrough.as_ref() != Some(strikethrough_style) { - finished_strikethrough = current_strikethrough.take(); - } - } - if let Some(run_strikethrough) = style_run.strikethrough.as_ref() { - current_strikethrough.get_or_insert(( - point( - glyph_origin.x, - glyph_origin.y - + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5), - ), - StrikethroughStyle { - color: Some(run_strikethrough.color.unwrap_or(style_run.color)), - thickness: run_strikethrough.thickness, - }, - )); - } - - run_end += style_run.len as usize; - color = style_run.color; - } else { - run_end = layout.len; - finished_background = current_background.take(); - finished_underline = current_underline.take(); - finished_strikethrough = current_strikethrough.take(); - } - } - - if let Some((background_origin, background_color)) = finished_background { - cx.paint_quad(fill( - Bounds { - origin: background_origin, - size: size(glyph_origin.x - background_origin.x, line_height), - }, - background_color, - )); - } - - if let Some((underline_origin, underline_style)) = finished_underline { - cx.paint_underline( - underline_origin, - glyph_origin.x - underline_origin.x, - &underline_style, - ); - } - - if let Some((strikethrough_origin, strikethrough_style)) = finished_strikethrough { - cx.paint_strikethrough( - strikethrough_origin, - glyph_origin.x - strikethrough_origin.x, - &strikethrough_style, - ); - } - - let max_glyph_bounds = Bounds { - origin: glyph_origin, - size: max_glyph_size, - }; - - let content_mask = cx.content_mask(); - if max_glyph_bounds.intersects(&content_mask.bounds) { - if glyph.is_emoji { - cx.paint_emoji( - glyph_origin + baseline_offset, - run.font_id, - glyph.id, - layout.font_size, - )?; - } else { - cx.paint_glyph( - glyph_origin + baseline_offset, - run.font_id, - glyph.id, - layout.font_size, - color, - )?; } } } - } - let mut last_line_end_x = origin.x + layout.width; - if let Some(boundary) = wrap_boundaries.last() { - let run = &layout.runs[boundary.run_ix]; - let glyph = &run.glyphs[boundary.glyph_ix]; - last_line_end_x -= glyph.position.x; - } + let mut last_line_end_x = origin.x + layout.width; + if let Some(boundary) = wrap_boundaries.last() { + let run = &layout.runs[boundary.run_ix]; + let glyph = &run.glyphs[boundary.glyph_ix]; + last_line_end_x -= glyph.position.x; + } - if let Some((background_origin, background_color)) = current_background.take() { - cx.paint_quad(fill( - Bounds { - origin: background_origin, - size: size(last_line_end_x - background_origin.x, line_height), - }, - background_color, - )); - } + if let Some((background_origin, background_color)) = current_background.take() { + cx.paint_quad(fill( + Bounds { + origin: background_origin, + size: size(last_line_end_x - background_origin.x, line_height), + }, + background_color, + )); + } - if let Some((underline_start, underline_style)) = current_underline.take() { - cx.paint_underline( - underline_start, - last_line_end_x - underline_start.x, - &underline_style, - ); - } + if let Some((underline_start, underline_style)) = current_underline.take() { + cx.paint_underline( + underline_start, + last_line_end_x - underline_start.x, + &underline_style, + ); + } - if let Some((strikethrough_start, strikethrough_style)) = current_strikethrough.take() { - cx.paint_strikethrough( - strikethrough_start, - last_line_end_x - strikethrough_start.x, - &strikethrough_style, - ); - } + if let Some((strikethrough_start, strikethrough_style)) = current_strikethrough.take() { + cx.paint_strikethrough( + strikethrough_start, + last_line_end_x - strikethrough_start.x, + &strikethrough_style, + ); + } - Ok(()) + Ok(()) + }) } diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index ced49068c8..877d1d48d4 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -1,10 +1,11 @@ -use crate::{px, EntityId, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size}; -use collections::{FxHashMap, FxHashSet}; +use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size}; +use collections::FxHashMap; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use smallvec::SmallVec; use std::{ borrow::Borrow, hash::{Hash, Hasher}, + ops::Range, sync::Arc, }; @@ -277,63 +278,71 @@ impl WrappedLineLayout { } pub(crate) struct LineLayoutCache { - view_stack: Mutex>, - previous_frame: Mutex>>, - current_frame: RwLock>>, - previous_frame_wrapped: Mutex>>, - current_frame_wrapped: RwLock>>, + previous_frame: Mutex, + current_frame: RwLock, platform_text_system: Arc, } +#[derive(Default)] +struct FrameCache { + lines: FxHashMap, Arc>, + wrapped_lines: FxHashMap, Arc>, + used_lines: Vec>, + used_wrapped_lines: Vec>, +} + +#[derive(Clone, Default)] +pub(crate) struct LineLayoutIndex { + lines_index: usize, + wrapped_lines_index: usize, +} + impl LineLayoutCache { pub fn new(platform_text_system: Arc) -> Self { Self { - view_stack: Mutex::default(), previous_frame: Mutex::default(), current_frame: RwLock::default(), - previous_frame_wrapped: Mutex::default(), - current_frame_wrapped: RwLock::default(), platform_text_system, } } - pub fn finish_frame(&self, reused_views: &FxHashSet) { - debug_assert_eq!(self.view_stack.lock().len(), 0); + pub fn layout_index(&self) -> LineLayoutIndex { + let frame = self.current_frame.read(); + LineLayoutIndex { + lines_index: frame.used_lines.len(), + wrapped_lines_index: frame.used_wrapped_lines.len(), + } + } + pub fn reuse_layouts(&self, range: Range) { + let mut previous_frame = &mut *self.previous_frame.lock(); + let mut current_frame = &mut *self.current_frame.write(); + + for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] { + if let Some((key, line)) = previous_frame.lines.remove_entry(key) { + current_frame.lines.insert(key, line); + } + current_frame.used_lines.push(key.clone()); + } + + for key in &previous_frame.used_wrapped_lines + [range.start.wrapped_lines_index..range.end.wrapped_lines_index] + { + if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) { + current_frame.wrapped_lines.insert(key, line); + } + current_frame.used_wrapped_lines.push(key.clone()); + } + } + + pub fn finish_frame(&self) { let mut prev_frame = self.previous_frame.lock(); let mut curr_frame = self.current_frame.write(); - for (key, layout) in prev_frame.drain() { - if key - .parent_view_id - .map_or(false, |view_id| reused_views.contains(&view_id)) - { - curr_frame.insert(key, layout); - } - } std::mem::swap(&mut *prev_frame, &mut *curr_frame); - - let mut prev_frame_wrapped = self.previous_frame_wrapped.lock(); - let mut curr_frame_wrapped = self.current_frame_wrapped.write(); - for (key, layout) in prev_frame_wrapped.drain() { - if key - .parent_view_id - .map_or(false, |view_id| reused_views.contains(&view_id)) - { - curr_frame_wrapped.insert(key, layout); - } - } - std::mem::swap(&mut *prev_frame_wrapped, &mut *curr_frame_wrapped); - } - - pub fn with_view(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R { - self.view_stack.lock().push(view_id); - let result = f(); - self.view_stack.lock().pop(); - result - } - - fn parent_view_id(&self) -> Option { - self.view_stack.lock().last().copied() + curr_frame.lines.clear(); + curr_frame.wrapped_lines.clear(); + curr_frame.used_lines.clear(); + curr_frame.used_wrapped_lines.clear(); } pub fn layout_wrapped_line( @@ -348,19 +357,24 @@ impl LineLayoutCache { font_size, runs, wrap_width, - parent_view_id: self.parent_view_id(), } as &dyn AsCacheKeyRef; - let current_frame = self.current_frame_wrapped.upgradable_read(); - if let Some(layout) = current_frame.get(key) { + let current_frame = self.current_frame.upgradable_read(); + if let Some(layout) = current_frame.wrapped_lines.get(key) { return layout.clone(); } - let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); - if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) { - current_frame.insert(key, layout.clone()); + let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key); + if let Some((key, layout)) = previous_frame_entry { + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + current_frame + .wrapped_lines + .insert(key.clone(), layout.clone()); + current_frame.used_wrapped_lines.push(key); layout } else { + drop(current_frame); + let unwrapped_layout = self.layout_line(text, font_size, runs); let wrap_boundaries = if let Some(wrap_width) = wrap_width { unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width) @@ -372,14 +386,19 @@ impl LineLayoutCache { wrap_boundaries, wrap_width, }); - let key = CacheKey { + let key = Arc::new(CacheKey { text: text.into(), font_size, runs: SmallVec::from(runs), wrap_width, - parent_view_id: self.parent_view_id(), - }; - current_frame.insert(key, layout.clone()); + }); + + let mut current_frame = self.current_frame.write(); + current_frame + .wrapped_lines + .insert(key.clone(), layout.clone()); + current_frame.used_wrapped_lines.push(key); + layout } } @@ -390,28 +409,28 @@ impl LineLayoutCache { font_size, runs, wrap_width: None, - parent_view_id: self.parent_view_id(), } as &dyn AsCacheKeyRef; let current_frame = self.current_frame.upgradable_read(); - if let Some(layout) = current_frame.get(key) { + if let Some(layout) = current_frame.lines.get(key) { return layout.clone(); } let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); - if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) { - current_frame.insert(key, layout.clone()); + if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) { + current_frame.lines.insert(key.clone(), layout.clone()); + current_frame.used_lines.push(key); layout } else { let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs)); - let key = CacheKey { + let key = Arc::new(CacheKey { text: text.into(), font_size, runs: SmallVec::from(runs), wrap_width: None, - parent_view_id: self.parent_view_id(), - }; - current_frame.insert(key, layout.clone()); + }); + current_frame.lines.insert(key.clone(), layout.clone()); + current_frame.used_lines.push(key); layout } } @@ -428,13 +447,12 @@ trait AsCacheKeyRef { fn as_cache_key_ref(&self) -> CacheKeyRef; } -#[derive(Debug, Eq)] +#[derive(Clone, Debug, Eq)] struct CacheKey { text: String, font_size: Pixels, runs: SmallVec<[FontRun; 1]>, wrap_width: Option, - parent_view_id: Option, } #[derive(Copy, Clone, PartialEq, Eq, Hash)] @@ -443,7 +461,6 @@ struct CacheKeyRef<'a> { font_size: Pixels, runs: &'a [FontRun], wrap_width: Option, - parent_view_id: Option, } impl<'a> PartialEq for (dyn AsCacheKeyRef + 'a) { @@ -467,7 +484,6 @@ impl AsCacheKeyRef for CacheKey { font_size: self.font_size, runs: self.runs.as_slice(), wrap_width: self.wrap_width, - parent_view_id: self.parent_view_id, } } } @@ -484,9 +500,9 @@ impl Hash for CacheKey { } } -impl<'a> Borrow for CacheKey { +impl<'a> Borrow for Arc { fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) { - self as &dyn AsCacheKeyRef + self.as_ref() as &dyn AsCacheKeyRef } } diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index e66ffbb00d..2475f379f1 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -1,14 +1,16 @@ use crate::{ - seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, Bounds, + seal::Sealed, AfterLayoutIndex, AnyElement, AnyModel, AnyWeakModel, AppContext, Bounds, ContentMask, Element, ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle, - FocusableView, IntoElement, LayoutId, Model, Pixels, Point, Render, Size, StackingOrder, Style, - TextStyle, ViewContext, VisualContext, WeakModel, + FocusableView, IntoElement, LayoutId, Model, PaintIndex, Pixels, Render, Style, + StyleRefinement, TextStyle, ViewContext, VisualContext, WeakModel, }; use anyhow::{Context, Result}; +use refineable::Refineable; use std::{ any::{type_name, TypeId}, fmt, hash::{Hash, Hasher}, + ops::Range, }; /// A view is a piece of state that can be presented on screen by implementing the [Render] trait. @@ -20,17 +22,15 @@ pub struct View { impl Sealed for View {} -#[doc(hidden)] -pub struct AnyViewState { - root_style: Style, - next_stacking_order_id: u16, - cache_key: Option, - element: Option, +struct AnyViewState { + after_layout_range: Range, + paint_range: Range, + cache_key: ViewCacheKey, } +#[derive(Default)] struct ViewCacheKey { bounds: Bounds, - stacking_order: StackingOrder, content_mask: ContentMask, text_style: TextStyle, } @@ -90,22 +90,39 @@ impl View { } impl Element for View { - type State = Option; + type BeforeLayout = AnyElement; + type AfterLayout = (); - fn request_layout( - &mut self, - _state: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { - cx.with_view_id(self.entity_id(), |cx| { + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element()); - let layout_id = element.request_layout(cx); - (layout_id, Some(element)) + let layout_id = element.before_layout(cx); + (layout_id, element) }) } - fn paint(&mut self, _: Bounds, element: &mut Self::State, cx: &mut ElementContext) { - cx.paint_view(self.entity_id(), |cx| element.take().unwrap().paint(cx)); + fn after_layout( + &mut self, + _: Bounds, + element: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) { + cx.set_view_id(self.entity_id()); + cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { + element.after_layout(cx) + }) + } + + fn paint( + &mut self, + _: Bounds, + element: &mut Self::BeforeLayout, + _: &mut Self::AfterLayout, + cx: &mut ElementContext, + ) { + cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { + element.paint(cx) + }) } } @@ -203,16 +220,16 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - pub(crate) request_layout: fn(&AnyView, &mut ElementContext) -> (LayoutId, AnyElement), - cache: bool, + render: fn(&AnyView, &mut ElementContext) -> AnyElement, + cached_style: Option, } impl AnyView { /// Indicate that this view should be cached when using it as an element. /// When using this method, the view's previous layout and paint will be recycled from the previous frame if [ViewContext::notify] has not been called since it was rendered. /// The one exception is when [WindowContext::refresh] is called, in which case caching is ignored. - pub fn cached(mut self) -> Self { - self.cache = true; + pub fn cached(mut self, style: StyleRefinement) -> Self { + self.cached_style = Some(style); self } @@ -220,7 +237,7 @@ impl AnyView { pub fn downgrade(&self) -> AnyWeakView { AnyWeakView { model: self.model.downgrade(), - layout: self.request_layout, + render: self.render, } } @@ -231,8 +248,8 @@ impl AnyView { Ok(model) => Ok(View { model }), Err(model) => Err(Self { model, - request_layout: self.request_layout, - cache: self.cache, + render: self.render, + cached_style: self.cached_style, }), } } @@ -246,113 +263,134 @@ impl AnyView { pub fn entity_id(&self) -> EntityId { self.model.entity_id() } - - pub(crate) fn draw( - &self, - origin: Point, - available_space: Size, - cx: &mut ElementContext, - ) { - cx.paint_view(self.entity_id(), |cx| { - cx.with_absolute_element_offset(origin, |cx| { - let (layout_id, mut rendered_element) = (self.request_layout)(self, cx); - cx.compute_layout(layout_id, available_space); - rendered_element.paint(cx) - }); - }) - } } impl From> for AnyView { fn from(value: View) -> Self { AnyView { model: value.model.into_any(), - request_layout: any_view::request_layout::, - cache: false, + render: any_view::render::, + cached_style: None, } } } impl Element for AnyView { - type State = AnyViewState; + type BeforeLayout = Option; + type AfterLayout = Option; - fn request_layout( - &mut self, - state: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { - cx.with_view_id(self.entity_id(), |cx| { - if self.cache - && !cx.window.dirty_views.contains(&self.entity_id()) - && !cx.window.refreshing - { - if let Some(state) = state { - let layout_id = cx.request_layout(&state.root_style, None); - return (layout_id, state); - } - } - - let (layout_id, element) = (self.request_layout)(self, cx); - let root_style = cx.layout_style(layout_id).unwrap().clone(); - let state = AnyViewState { - root_style, - next_stacking_order_id: 0, - cache_key: None, - element: Some(element), - }; - (layout_id, state) - }) + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + if let Some(style) = self.cached_style.as_ref() { + let mut root_style = Style::default(); + root_style.refine(style); + let layout_id = cx.request_layout(&root_style, None); + (layout_id, None) + } else { + cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { + let mut element = (self.render)(self, cx); + let layout_id = element.before_layout(cx); + (layout_id, Some(element)) + }) + } } - fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut ElementContext) { - cx.paint_view(self.entity_id(), |cx| { - if !self.cache { - state.element.take().unwrap().paint(cx); - return; - } + fn after_layout( + &mut self, + bounds: Bounds, + element: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) -> Option { + cx.set_view_id(self.entity_id()); + if self.cached_style.is_some() { + cx.with_element_state::( + Some(ElementId::View(self.entity_id())), + |element_state, cx| { + let mut element_state = element_state.unwrap(); - if let Some(cache_key) = state.cache_key.as_mut() { - if cache_key.bounds == bounds - && cache_key.content_mask == cx.content_mask() - && cache_key.stacking_order == *cx.stacking_order() - && cache_key.text_style == cx.text_style() - { - cx.reuse_view(state.next_stacking_order_id); - return; - } - } + let content_mask = cx.content_mask(); + let text_style = cx.text_style(); - if let Some(mut element) = state.element.take() { - element.paint(cx); - } else { - let mut element = (self.request_layout)(self, cx).1; - element.draw(bounds.origin, bounds.size.into(), cx); - } + if let Some(mut element_state) = element_state { + if element_state.cache_key.bounds == bounds + && element_state.cache_key.content_mask == content_mask + && element_state.cache_key.text_style == text_style + && !cx.window.dirty_views.contains(&self.entity_id()) + && !cx.window.refreshing + { + let after_layout_start = cx.after_layout_index(); + cx.reuse_after_layout(element_state.after_layout_range.clone()); + let after_layout_end = cx.after_layout_index(); + element_state.after_layout_range = after_layout_start..after_layout_end; + return (None, Some(element_state)); + } + } - state.next_stacking_order_id = cx - .window - .next_frame - .next_stacking_order_ids - .last() - .copied() - .unwrap(); - state.cache_key = Some(ViewCacheKey { - bounds, - stacking_order: cx.stacking_order().clone(), - content_mask: cx.content_mask(), - text_style: cx.text_style(), - }); - }) + let after_layout_start = cx.after_layout_index(); + let mut element = (self.render)(self, cx); + element.layout(bounds.origin, bounds.size.into(), cx); + let after_layout_end = cx.after_layout_index(); + + ( + Some(element), + Some(AnyViewState { + after_layout_range: after_layout_start..after_layout_end, + paint_range: PaintIndex::default()..PaintIndex::default(), + cache_key: ViewCacheKey { + bounds, + content_mask, + text_style, + }, + }), + ) + }, + ) + } else { + cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { + let mut element = element.take().unwrap(); + element.after_layout(cx); + Some(element) + }) + } + } + + fn paint( + &mut self, + _bounds: Bounds, + _: &mut Self::BeforeLayout, + element: &mut Self::AfterLayout, + cx: &mut ElementContext, + ) { + if self.cached_style.is_some() { + cx.with_element_state::( + Some(ElementId::View(self.entity_id())), + |element_state, cx| { + let mut element_state = element_state.unwrap().unwrap(); + + let paint_start = cx.paint_index(); + + if let Some(element) = element { + element.paint(cx); + } else { + cx.reuse_paint(element_state.paint_range.clone()); + } + + let paint_end = cx.paint_index(); + element_state.paint_range = paint_start..paint_end; + + ((), Some(element_state)) + }, + ) + } else { + cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { + element.as_mut().unwrap().paint(cx); + }) + } } } impl IntoElement for View { type Element = View; - fn element_id(&self) -> Option { - Some(ElementId::from_entity_id(self.model.entity_id)) - } - fn into_element(self) -> Self::Element { self } @@ -361,10 +399,6 @@ impl IntoElement for View { impl IntoElement for AnyView { type Element = Self; - fn element_id(&self) -> Option { - Some(ElementId::from_entity_id(self.model.entity_id)) - } - fn into_element(self) -> Self::Element { self } @@ -373,7 +407,7 @@ impl IntoElement for AnyView { /// A weak, dynamically-typed view handle that does not prevent the view from being released. pub struct AnyWeakView { model: AnyWeakModel, - layout: fn(&AnyView, &mut ElementContext) -> (LayoutId, AnyElement), + render: fn(&AnyView, &mut ElementContext) -> AnyElement, } impl AnyWeakView { @@ -382,8 +416,8 @@ impl AnyWeakView { let model = self.model.upgrade()?; Some(AnyView { model, - request_layout: self.layout, - cache: false, + render: self.render, + cached_style: None, }) } } @@ -392,7 +426,7 @@ impl From> for AnyWeakView { fn from(view: WeakView) -> Self { Self { model: view.model.into(), - layout: any_view::request_layout::, + render: any_view::render::, } } } @@ -412,15 +446,13 @@ impl std::fmt::Debug for AnyWeakView { } mod any_view { - use crate::{AnyElement, AnyView, ElementContext, IntoElement, LayoutId, Render}; + use crate::{AnyElement, AnyView, ElementContext, IntoElement, Render}; - pub(crate) fn request_layout( + pub(crate) fn render( view: &AnyView, cx: &mut ElementContext, - ) -> (LayoutId, AnyElement) { + ) -> AnyElement { let view = view.clone().downcast::().unwrap(); - let mut element = view.update(cx, |view, cx| view.render(cx).into_any_element()); - let layout_id = element.request_layout(cx); - (layout_id, element) + view.update(cx, |view, cx| view.render(cx).into_any_element()) } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index cc01e5223f..8a2a6f8f0d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1,12 +1,12 @@ use crate::{ - px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext, - AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId, - DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, - Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult, - Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent, - MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, - PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription, - TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowAppearance, WindowBounds, + px, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext, Bounds, + Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, + Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, Global, GlobalElementId, + Hsla, KeyBinding, KeyDownEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent, Model, + ModelContext, Modifiers, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, + PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Render, ScaledPixels, + SharedString, Size, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, + TextStyleRefinement, View, VisualContext, WeakView, WindowAppearance, WindowBounds, WindowOptions, WindowTextSystem, }; use anyhow::{anyhow, Context as _, Result}; @@ -14,6 +14,7 @@ use collections::FxHashSet; use derive_more::{Deref, DerefMut}; use futures::channel::oneshot; use parking_lot::RwLock; +use refineable::Refineable; use slotmap::SlotMap; use smallvec::SmallVec; use std::{ @@ -40,26 +41,6 @@ mod prompts; pub use element_cx::*; pub use prompts::*; -const ACTIVE_DRAG_Z_INDEX: u16 = 1; - -/// A global stacking order, which is created by stacking successive z-index values. -/// Each z-index will always be interpreted in the context of its parent z-index. -#[derive(Debug, Deref, DerefMut, Clone, Ord, PartialOrd, PartialEq, Eq, Default)] -pub struct StackingOrder(SmallVec<[StackingContext; 64]>); - -/// A single entry in a primitive's z-index stacking order -#[derive(Clone, Ord, PartialOrd, PartialEq, Eq, Default)] -pub struct StackingContext { - pub(crate) z_index: u16, - pub(crate) id: u16, -} - -impl std::fmt::Debug for StackingContext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{{{}.{}}} ", self.z_index, self.id) - } -} - /// Represents the two different phases when dispatching events. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] pub enum DispatchPhase { @@ -258,8 +239,10 @@ pub struct Window { layout_engine: Option, pub(crate) root_view: Option, pub(crate) element_id_stack: GlobalElementId, + pub(crate) text_style_stack: Vec, pub(crate) rendered_frame: Frame, pub(crate) next_frame: Frame, + pub(crate) next_hitbox_id: HitboxId, next_frame_callbacks: Rc>>, pub(crate) dirty_views: FxHashSet, pub(crate) focus_handles: Arc>>, @@ -267,6 +250,7 @@ pub struct Window { focus_lost_listeners: SubscriberSet<(), AnyObserver>, default_prevented: bool, mouse_position: Point, + mouse_hit_test: HitTest, modifiers: Modifiers, scale_factor: f32, bounds: WindowBounds, @@ -278,7 +262,7 @@ pub struct Window { pub(crate) needs_present: Rc>, pub(crate) last_input_timestamp: Rc>, pub(crate) refreshing: bool, - pub(crate) drawing: bool, + pub(crate) draw_phase: DrawPhase, activation_observers: SubscriberSet<(), AnyObserver>, pub(crate) focus: Option, focus_enabled: bool, @@ -286,6 +270,14 @@ pub struct Window { prompt: Option, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DrawPhase { + None, + Layout, + Paint, + Focus, +} + #[derive(Default, Debug)] struct PendingInput { keystrokes: SmallVec<[Keystroke; 1]>, @@ -319,7 +311,6 @@ impl PendingInput { pub(crate) struct ElementStateBox { pub(crate) inner: Box, - pub(crate) parent_view_id: EntityId, #[cfg(debug_assertions)] pub(crate) type_name: &'static str, } @@ -452,15 +443,18 @@ impl Window { layout_engine: Some(TaffyLayoutEngine::new()), root_view: None, element_id_stack: GlobalElementId::default(), + text_style_stack: Vec::new(), rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), next_frame_callbacks, + next_hitbox_id: HitboxId::default(), dirty_views: FxHashSet::default(), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_listeners: SubscriberSet::new(), focus_lost_listeners: SubscriberSet::new(), default_prevented: true, mouse_position, + mouse_hit_test: HitTest::default(), modifiers, scale_factor, bounds, @@ -472,7 +466,7 @@ impl Window { needs_present, last_input_timestamp, refreshing: false, - drawing: false, + draw_phase: DrawPhase::None, activation_observers: SubscriberSet::new(), focus: None, focus_enabled: true, @@ -533,7 +527,7 @@ impl<'a> WindowContext<'a> { /// Mark the window as dirty, scheduling it to be redrawn on the next frame. pub fn refresh(&mut self) { - if !self.window.drawing { + if self.window.draw_phase == DrawPhase::None { self.window.refreshing = true; self.window.dirty.set(true); } @@ -592,22 +586,39 @@ impl<'a> WindowContext<'a> { &self.window.text_system } + /// The current text style. Which is composed of all the style refinements provided to `with_text_style`. + pub fn text_style(&self) -> TextStyle { + let mut style = TextStyle::default(); + for refinement in &self.window.text_style_stack { + style.refine(refinement); + } + style + } + /// Dispatch the given action on the currently focused element. pub fn dispatch_action(&mut self, action: Box) { let focus_handle = self.focused(); - self.defer(move |cx| { - let node_id = focus_handle - .and_then(|handle| { - cx.window - .rendered_frame - .dispatch_tree - .focusable_node_id(handle.id) - }) - .unwrap_or_else(|| cx.window.rendered_frame.dispatch_tree.root_node_id()); - + let window = self.window.handle; + self.app.defer(move |cx| { cx.propagate_event = true; - cx.dispatch_action_on_node(node_id, action); + window + .update(cx, |_, cx| { + let node_id = focus_handle + .and_then(|handle| { + cx.window + .rendered_frame + .dispatch_tree + .focusable_node_id(handle.id) + }) + .unwrap_or_else(|| cx.window.rendered_frame.dispatch_tree.root_node_id()); + + cx.dispatch_action_on_node(node_id, action.as_ref()); + }) + .log_err(); + if cx.propagate_event { + cx.dispatch_global_action(action.as_ref()); + } }) } @@ -862,171 +873,21 @@ impl<'a> WindowContext<'a> { self.window.modifiers } - /// Returns true if there is no opaque layer containing the given point - /// on top of the given level. Layers who are extensions of the queried layer - /// are not considered to be on top of queried layer. - pub fn was_top_layer(&self, point: &Point, layer: &StackingOrder) -> bool { - // Precondition: the depth map is ordered from topmost to bottomost. - - for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() { - if layer >= opaque_layer { - // The queried layer is either above or is the same as the this opaque layer. - // Anything after this point is guaranteed to be below the queried layer. - return true; - } - - if !bounds.contains(point) { - // This opaque layer is above the queried layer but it doesn't contain - // the given position, so we can ignore it even if it's above. - continue; - } - - // At this point, we've established that this opaque layer is on top of the queried layer - // and contains the position: - // If neither the opaque layer or the queried layer is an extension of the other then - // we know they are on different stacking orders, and return false. - let is_on_same_layer = opaque_layer - .iter() - .zip(layer.iter()) - .all(|(a, b)| a.z_index == b.z_index); - - if !is_on_same_layer { - return false; - } - } - - true - } - - pub(crate) fn was_top_layer_under_active_drag( - &self, - point: &Point, - layer: &StackingOrder, - ) -> bool { - // Precondition: the depth map is ordered from topmost to bottomost. - - for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() { - if layer >= opaque_layer { - // The queried layer is either above or is the same as the this opaque layer. - // Anything after this point is guaranteed to be below the queried layer. - return true; - } - - if !bounds.contains(point) { - // This opaque layer is above the queried layer but it doesn't contain - // the given position, so we can ignore it even if it's above. - continue; - } - - // All normal content is rendered with a base z-index of 0, we know that if the root of this opaque layer - // equals `ACTIVE_DRAG_Z_INDEX` then it must be the drag layer and we can ignore it as we are - // looking to see if the queried layer was the topmost underneath the drag layer. - if opaque_layer - .first() - .map(|c| c.z_index == ACTIVE_DRAG_Z_INDEX) - .unwrap_or(false) - { - continue; - } - - // At this point, we've established that this opaque layer is on top of the queried layer - // and contains the position: - // If neither the opaque layer or the queried layer is an extension of the other then - // we know they are on different stacking orders, and return false. - let is_on_same_layer = opaque_layer - .iter() - .zip(layer.iter()) - .all(|(a, b)| a.z_index == b.z_index); - - if !is_on_same_layer { - return false; - } - } - - true - } - - /// Called during painting to get the current stacking order. - pub fn stacking_order(&self) -> &StackingOrder { - &self.window.next_frame.z_index_stack - } - /// Produces a new frame and assigns it to `rendered_frame`. To actually show /// the contents of the new [Scene], use [present]. #[profiling::function] pub fn draw(&mut self) { self.window.dirty.set(false); - self.window.drawing = true; - if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut() - { - let input_handler = self.window.platform_window.take_input_handler(); - requested_handler.handler = input_handler; + // Restore the previously-used input handler. + if let Some(input_handler) = self.window.platform_window.take_input_handler() { + self.window + .rendered_frame + .input_handlers + .push(Some(input_handler)); } - let root_view = self.window.root_view.take().unwrap(); - let mut prompt = self.window.prompt.take(); - self.with_element_context(|cx| { - cx.with_z_index(0, |cx| { - cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| { - // We need to use cx.cx here so we can utilize borrow splitting - for (action_type, action_listeners) in &cx.cx.app.global_action_listeners { - for action_listener in action_listeners.iter().cloned() { - cx.cx.window.next_frame.dispatch_tree.on_action( - *action_type, - Rc::new( - move |action: &dyn Any, phase, cx: &mut WindowContext<'_>| { - action_listener(action, phase, cx) - }, - ), - ) - } - } - - let available_space = cx.window.viewport_size.map(Into::into); - - let origin = Point::default(); - cx.paint_view(root_view.entity_id(), |cx| { - cx.with_absolute_element_offset(origin, |cx| { - let (layout_id, mut rendered_element) = - (root_view.request_layout)(&root_view, cx); - cx.compute_layout(layout_id, available_space); - rendered_element.paint(cx); - - if let Some(prompt) = &mut prompt { - prompt.paint(cx).draw(origin, available_space, cx) - } - }); - }); - }) - }) - }); - self.window.prompt = prompt; - - if let Some(active_drag) = self.app.active_drag.take() { - self.with_element_context(|cx| { - cx.with_z_index(ACTIVE_DRAG_Z_INDEX, |cx| { - let offset = cx.mouse_position() - active_drag.cursor_offset; - let available_space = - size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_drag.view.draw(offset, available_space, cx); - }) - }); - self.active_drag = Some(active_drag); - } else if let Some(tooltip_request) = self.window.next_frame.tooltip_request.take() { - self.with_element_context(|cx| { - cx.with_z_index(1, |cx| { - let available_space = - size(AvailableSpace::MinContent, AvailableSpace::MinContent); - tooltip_request.tooltip.view.draw( - tooltip_request.tooltip.cursor_offset, - available_space, - cx, - ); - }) - }); - self.window.next_frame.tooltip_request = Some(tooltip_request); - } + self.with_element_context(|cx| cx.draw_roots()); self.window.dirty_views.clear(); self.window @@ -1038,26 +899,22 @@ impl<'a> WindowContext<'a> { ); self.window.next_frame.focus = self.window.focus; self.window.next_frame.window_active = self.window.active.get(); - self.window.root_view = Some(root_view); // Set the cursor only if we're the active window. - let cursor_style_request = self.window.next_frame.requested_cursor_style.take(); if self.is_window_active() { - let cursor_style = - cursor_style_request.map_or(CursorStyle::Arrow, |request| request.style); + let cursor_style = self.compute_cursor_style().unwrap_or(CursorStyle::Arrow); self.platform.set_cursor_style(cursor_style); } // Register requested input handler with the platform window. - if let Some(requested_input) = self.window.next_frame.requested_input_handler.as_mut() { - if let Some(handler) = requested_input.handler.take() { - self.window.platform_window.set_input_handler(handler); - } + if let Some(input_handler) = self.window.next_frame.input_handlers.pop() { + self.window + .platform_window + .set_input_handler(input_handler.unwrap()); } self.window.layout_engine.as_mut().unwrap().clear(); - self.text_system() - .finish_frame(&self.window.next_frame.reused_views); + self.text_system().finish_frame(); self.window .next_frame .finish(&mut self.window.rendered_frame); @@ -1069,6 +926,7 @@ impl<'a> WindowContext<'a> { element_arena.clear(); }); + self.window.draw_phase = DrawPhase::Focus; let previous_focus_path = self.window.rendered_frame.focus_path(); let previous_window_active = self.window.rendered_frame.window_active; mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame); @@ -1104,7 +962,7 @@ impl<'a> WindowContext<'a> { .retain(&(), |listener| listener(&event, self)); } self.window.refreshing = false; - self.window.drawing = false; + self.window.draw_phase = DrawPhase::None; self.window.needs_present.set(true); } @@ -1117,6 +975,18 @@ impl<'a> WindowContext<'a> { profiling::finish_frame!(); } + fn compute_cursor_style(&mut self) -> Option { + // TODO: maybe we should have a HashMap keyed by HitboxId. + let request = self + .window + .next_frame + .cursor_styles + .iter() + .rev() + .find(|request| request.hitbox_id.is_hovered(self))?; + Some(request.style) + } + /// Dispatch a given keystroke as though the user had typed it. /// You can create a keystroke with Keystroke::parse(""). pub fn dispatch_keystroke(&mut self, keystroke: Keystroke) -> bool { @@ -1251,43 +1121,32 @@ impl<'a> WindowContext<'a> { } fn dispatch_mouse_event(&mut self, event: &dyn Any) { - if let Some(mut handlers) = self - .window - .rendered_frame - .mouse_listeners - .remove(&event.type_id()) - { - // Because handlers may add other handlers, we sort every time. - handlers.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); + self.window.mouse_hit_test = self.window.rendered_frame.hit_test(self.mouse_position()); + let mut mouse_listeners = mem::take(&mut self.window.rendered_frame.mouse_listeners); + self.with_element_context(|cx| { // Capture phase, events bubble from back to front. Handlers for this phase are used for // special purposes, such as detecting events outside of a given Bounds. - for (_, _, handler) in &mut handlers { - self.with_element_context(|cx| { - handler(event, DispatchPhase::Capture, cx); - }); - if !self.app.propagate_event { + for listener in &mut mouse_listeners { + let listener = listener.as_mut().unwrap(); + listener(event, DispatchPhase::Capture, cx); + if !cx.app.propagate_event { break; } } // Bubble phase, where most normal handlers do their work. - if self.app.propagate_event { - for (_, _, handler) in handlers.iter_mut().rev() { - self.with_element_context(|cx| { - handler(event, DispatchPhase::Bubble, cx); - }); - if !self.app.propagate_event { + if cx.app.propagate_event { + for listener in mouse_listeners.iter_mut().rev() { + let listener = listener.as_mut().unwrap(); + listener(event, DispatchPhase::Bubble, cx); + if !cx.app.propagate_event { break; } } } - - self.window - .rendered_frame - .mouse_listeners - .insert(event.type_id(), handlers); - } + }); + self.window.rendered_frame.mouse_listeners = mouse_listeners; if self.app.propagate_event && self.has_active_drag() { if event.is::() { @@ -1357,6 +1216,7 @@ impl<'a> WindowContext<'a> { }) .log_err(); })); + self.window.pending_input = Some(currently_pending); self.propagate_event = false; @@ -1376,7 +1236,7 @@ impl<'a> WindowContext<'a> { self.propagate_event = true; for binding in bindings { - self.dispatch_action_on_node(node_id, binding.action.boxed_clone()); + self.dispatch_action_on_node(node_id, binding.action.as_ref()); if !self.propagate_event { self.dispatch_keystroke_observers(event, Some(binding.action)); return; @@ -1454,7 +1314,7 @@ impl<'a> WindowContext<'a> { self.propagate_event = true; for binding in currently_pending.bindings { - self.dispatch_action_on_node(node_id, binding.action.boxed_clone()); + self.dispatch_action_on_node(node_id, binding.action.as_ref()); if !self.propagate_event { return; } @@ -1486,7 +1346,7 @@ impl<'a> WindowContext<'a> { } } - fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box) { + fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: &dyn Action) { let dispatch_path = self .window .rendered_frame @@ -1628,10 +1488,20 @@ impl<'a> WindowContext<'a> { }) .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id()); - self.window + let mut actions = self + .window .rendered_frame .dispatch_tree - .available_actions(node_id) + .available_actions(node_id); + for action_type in self.global_action_listeners.keys() { + if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) { + let action = self.actions.build_action_type(action_type).ok(); + if let Some(action) = action { + actions.insert(ix, action); + } + } + } + actions } /// Returns key bindings that invoke the given action on the currently focused element. @@ -1697,15 +1567,6 @@ impl<'a> WindowContext<'a> { .on_should_close(Box::new(move || this.update(|cx| f(cx)).unwrap_or(true))) } - pub(crate) fn parent_view_id(&self) -> EntityId { - *self - .window - .next_frame - .view_stack - .last() - .expect("a view should always be on the stack while drawing") - } - /// Register an action listener on the window for the next frame. The type of action /// is determined by the first parameter of the given listener. When the next frame is rendered /// the listener will be cleared. @@ -2141,7 +2002,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } } - if !self.window.drawing { + if self.window.draw_phase == DrawPhase::None { self.window_cx.window.dirty.set(true); self.window_cx.app.push_effect(Effect::Notify { emitter: self.view.model.entity_id, @@ -2734,12 +2595,6 @@ impl Display for ElementId { } } -impl ElementId { - pub(crate) fn from_entity_id(entity_id: EntityId) -> Self { - ElementId::View(entity_id) - } -} - impl TryInto for ElementId { type Error = anyhow::Error; diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs index 46b5a21cf3..ec58d25c30 100644 --- a/crates/gpui/src/window/element_cx.rs +++ b/crates/gpui/src/window/element_cx.rs @@ -16,92 +16,139 @@ use std::{ any::{Any, TypeId}, borrow::{Borrow, BorrowMut, Cow}, mem, + ops::Range, rc::Rc, sync::Arc, }; use anyhow::Result; -use collections::{FxHashMap, FxHashSet}; +use collections::FxHashMap; use derive_more::{Deref, DerefMut}; #[cfg(target_os = "macos")] use media::core_video::CVImageBuffer; use smallvec::SmallVec; -use util::post_inc; use crate::{ - prelude::*, size, AnyTooltip, AppContext, AvailableSpace, Bounds, BoxShadow, ContentMask, - Corners, CursorStyle, DevicePixels, DispatchPhase, DispatchTree, ElementId, ElementStateBox, - EntityId, FocusHandle, FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData, - InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad, - Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams, - RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext, - StackingOrder, StrikethroughStyle, Style, TextStyleRefinement, Underline, UnderlineStyle, - Window, WindowContext, SUBPIXEL_VARIANTS, + prelude::*, size, AnyElement, AnyTooltip, AppContext, AvailableSpace, Bounds, BoxShadow, + ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase, DispatchTree, + DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId, GlobalElementId, + GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, + LineLayoutIndex, MonochromeSprite, MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler, + Point, PolychromeSprite, Quad, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, + Shadow, SharedString, Size, StrikethroughStyle, Style, TextStyleRefinement, Underline, + UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS, }; -type AnyMouseListener = Box; - -pub(crate) struct RequestedInputHandler { - pub(crate) view_id: EntityId, - pub(crate) handler: Option, -} - -pub(crate) struct TooltipRequest { - pub(crate) view_id: EntityId, - pub(crate) tooltip: AnyTooltip, -} +pub(crate) type AnyMouseListener = + Box; #[derive(Clone)] pub(crate) struct CursorStyleRequest { + pub(crate) hitbox_id: HitboxId, pub(crate) style: CursorStyle, - stacking_order: StackingOrder, +} + +/// An identifier for a [Hitbox]. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct HitboxId(usize); + +impl HitboxId { + /// Checks if the hitbox with this id is currently hovered. + pub fn is_hovered(&self, cx: &WindowContext) -> bool { + cx.window.mouse_hit_test.0.contains(self) + } +} + +/// A rectangular region that potentially blocks hitboxes inserted prior. +/// See [ElementContext::insert_hitbox] for more details. +#[derive(Clone, Debug, Eq, PartialEq, Deref)] +pub struct Hitbox { + /// A unique identifier for the hitbox + pub id: HitboxId, + /// The bounds of the hitbox + #[deref] + pub bounds: Bounds, + /// Whether the hitbox occludes other hitboxes inserted prior. + pub opaque: bool, +} + +impl Hitbox { + /// Checks if the hitbox is currently hovered. + pub fn is_hovered(&self, cx: &WindowContext) -> bool { + self.id.is_hovered(cx) + } +} + +#[derive(Default)] +pub(crate) struct HitTest(SmallVec<[HitboxId; 8]>); + +pub(crate) struct DeferredDraw { + priority: usize, + parent_node: DispatchNodeId, + element_id_stack: GlobalElementId, + text_style_stack: Vec, + element: Option, + absolute_offset: Point, + layout_range: Range, + paint_range: Range, } pub(crate) struct Frame { pub(crate) focus: Option, pub(crate) window_active: bool, - pub(crate) element_states: FxHashMap, - pub(crate) mouse_listeners: FxHashMap>, + pub(crate) element_states: FxHashMap<(GlobalElementId, TypeId), ElementStateBox>, + accessed_element_states: Vec<(GlobalElementId, TypeId)>, + pub(crate) mouse_listeners: Vec>, pub(crate) dispatch_tree: DispatchTree, pub(crate) scene: Scene, - pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds)>, - pub(crate) z_index_stack: StackingOrder, - pub(crate) next_stacking_order_ids: Vec, - pub(crate) next_root_z_index: u16, + pub(crate) hitboxes: Vec, + pub(crate) deferred_draws: Vec, pub(crate) content_mask_stack: Vec>, pub(crate) element_offset_stack: Vec>, - pub(crate) requested_input_handler: Option, - pub(crate) tooltip_request: Option, - pub(crate) cursor_styles: FxHashMap, - pub(crate) requested_cursor_style: Option, - pub(crate) view_stack: Vec, - pub(crate) reused_views: FxHashSet, - + pub(crate) input_handlers: Vec>, + pub(crate) tooltip_requests: Vec>, + pub(crate) cursor_styles: Vec, #[cfg(any(test, feature = "test-support"))] pub(crate) debug_bounds: FxHashMap>, } +#[derive(Clone, Default)] +pub(crate) struct AfterLayoutIndex { + hitboxes_index: usize, + tooltips_index: usize, + deferred_draws_index: usize, + dispatch_tree_index: usize, + accessed_element_states_index: usize, + line_layout_index: LineLayoutIndex, +} + +#[derive(Clone, Default)] +pub(crate) struct PaintIndex { + scene_index: usize, + mouse_listeners_index: usize, + input_handlers_index: usize, + cursor_styles_index: usize, + accessed_element_states_index: usize, + line_layout_index: LineLayoutIndex, +} + impl Frame { pub(crate) fn new(dispatch_tree: DispatchTree) -> Self { Frame { focus: None, window_active: false, element_states: FxHashMap::default(), - mouse_listeners: FxHashMap::default(), + accessed_element_states: Vec::new(), + mouse_listeners: Vec::new(), dispatch_tree, scene: Scene::default(), - depth_map: Vec::new(), - z_index_stack: StackingOrder::default(), - next_stacking_order_ids: vec![0], - next_root_z_index: 0, + hitboxes: Vec::new(), + deferred_draws: Vec::new(), content_mask_stack: Vec::new(), element_offset_stack: Vec::new(), - requested_input_handler: None, - tooltip_request: None, - cursor_styles: FxHashMap::default(), - requested_cursor_style: None, - view_stack: Vec::new(), - reused_views: FxHashSet::default(), + input_handlers: Vec::new(), + tooltip_requests: Vec::new(), + cursor_styles: Vec::new(), #[cfg(any(test, feature = "test-support"))] debug_bounds: FxHashMap::default(), @@ -110,18 +157,28 @@ impl Frame { pub(crate) fn clear(&mut self) { self.element_states.clear(); - self.mouse_listeners.values_mut().for_each(Vec::clear); + self.accessed_element_states.clear(); + self.mouse_listeners.clear(); self.dispatch_tree.clear(); - self.depth_map.clear(); - self.next_stacking_order_ids = vec![0]; - self.next_root_z_index = 0; - self.reused_views.clear(); self.scene.clear(); - self.requested_input_handler.take(); - self.tooltip_request.take(); + self.input_handlers.clear(); + self.tooltip_requests.clear(); self.cursor_styles.clear(); - self.requested_cursor_style.take(); - debug_assert_eq!(self.view_stack.len(), 0); + self.hitboxes.clear(); + self.deferred_draws.clear(); + } + + pub(crate) fn hit_test(&self, position: Point) -> HitTest { + let mut hit_test = HitTest::default(); + for hitbox in self.hitboxes.iter().rev() { + if hitbox.bounds.contains(&position) { + hit_test.0.push(hitbox.id); + if hitbox.opaque { + break; + } + } + } + hit_test } pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> { @@ -131,38 +188,13 @@ impl Frame { } pub(crate) fn finish(&mut self, prev_frame: &mut Self) { - // Reuse mouse listeners that didn't change since the last frame. - for (type_id, listeners) in &mut prev_frame.mouse_listeners { - let next_listeners = self.mouse_listeners.entry(*type_id).or_default(); - for (order, view_id, listener) in listeners.drain(..) { - if self.reused_views.contains(&view_id) { - next_listeners.push((order, view_id, listener)); - } + for element_state_key in &self.accessed_element_states { + if let Some(element_state) = prev_frame.element_states.remove(element_state_key) { + self.element_states + .insert(element_state_key.clone(), element_state); } } - // Reuse entries in the depth map that didn't change since the last frame. - for (order, view_id, bounds) in prev_frame.depth_map.drain(..) { - if self.reused_views.contains(&view_id) { - match self - .depth_map - .binary_search_by(|(level, _, _)| order.cmp(level)) - { - Ok(i) | Err(i) => self.depth_map.insert(i, (order, view_id, bounds)), - } - } - } - - // Retain element states for views that didn't change since the last frame. - for (element_id, state) in prev_frame.element_states.drain() { - if self.reused_views.contains(&state.parent_view_id) { - self.element_states.entry(element_id).or_insert(state); - } - } - - // Reuse geometry that didn't change since the last frame. - self.scene - .reuse_views(&self.reused_views, &mut prev_frame.scene); self.scene.finish(); } } @@ -316,68 +348,218 @@ impl<'a> VisualContext for ElementContext<'a> { } impl<'a> ElementContext<'a> { - pub(crate) fn reuse_view(&mut self, next_stacking_order_id: u16) { - let view_id = self.parent_view_id(); - let grafted_view_ids = self - .cx - .window - .next_frame - .dispatch_tree - .reuse_view(view_id, &mut self.cx.window.rendered_frame.dispatch_tree); - for view_id in grafted_view_ids { - assert!(self.window.next_frame.reused_views.insert(view_id)); + pub(crate) fn draw_roots(&mut self) { + self.window.draw_phase = DrawPhase::Layout; - // Reuse the previous input handler requested during painting of the reused view. - if self - .window - .rendered_frame - .requested_input_handler - .as_ref() - .map_or(false, |requested| requested.view_id == view_id) - { - self.window.next_frame.requested_input_handler = - self.window.rendered_frame.requested_input_handler.take(); - } + // Layout all root elements. + let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any(); + root_element.layout(Point::default(), self.window.viewport_size.into(), self); - // Reuse the tooltip previously requested during painting of the reused view. - if self - .window - .rendered_frame - .tooltip_request - .as_ref() - .map_or(false, |requested| requested.view_id == view_id) - { - self.window.next_frame.tooltip_request = - self.window.rendered_frame.tooltip_request.take(); - } - - // Reuse the cursor styles previously requested during painting of the reused view. - if let Some(cursor_style_request) = - self.window.rendered_frame.cursor_styles.remove(&view_id) - { - self.set_cursor_style( - cursor_style_request.style, - cursor_style_request.stacking_order, - ); - } + let mut prompt_element = None; + let mut active_drag_element = None; + let mut tooltip_element = None; + if let Some(prompt) = self.window.prompt.take() { + let mut element = prompt.view.any_view().into_any(); + element.layout(Point::default(), self.window.viewport_size.into(), self); + prompt_element = Some(element); + self.window.prompt = Some(prompt); + } else if let Some(active_drag) = self.app.active_drag.take() { + let mut element = active_drag.view.clone().into_any(); + let offset = self.mouse_position() - active_drag.cursor_offset; + element.layout(offset, AvailableSpace::min_size(), self); + active_drag_element = Some(element); + self.app.active_drag = Some(active_drag); + } else if let Some(tooltip_request) = + self.window.next_frame.tooltip_requests.last().cloned() + { + let tooltip_request = tooltip_request.unwrap(); + let mut element = tooltip_request.view.clone().into_any(); + let offset = tooltip_request.cursor_offset; + element.layout(offset, AvailableSpace::min_size(), self); + tooltip_element = Some(element); } - debug_assert!( - next_stacking_order_id - >= self - .window - .next_frame - .next_stacking_order_ids - .last() - .copied() - .unwrap() + let mut sorted_deferred_draws = + (0..self.window.next_frame.deferred_draws.len()).collect::>(); + sorted_deferred_draws + .sort_unstable_by_key(|ix| self.window.next_frame.deferred_draws[*ix].priority); + self.layout_deferred_draws(&sorted_deferred_draws); + + self.window.mouse_hit_test = self.window.next_frame.hit_test(self.window.mouse_position); + + // Now actually paint the elements. + self.window.draw_phase = DrawPhase::Paint; + root_element.paint(self); + + if let Some(mut prompt_element) = prompt_element { + prompt_element.paint(self) + } else if let Some(mut drag_element) = active_drag_element { + drag_element.paint(self); + } else if let Some(mut tooltip_element) = tooltip_element { + tooltip_element.paint(self); + } + + self.paint_deferred_draws(&sorted_deferred_draws); + } + + fn layout_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { + assert_eq!(self.window.element_id_stack.len(), 0); + + let mut deferred_draws = mem::take(&mut self.window.next_frame.deferred_draws); + for deferred_draw_ix in deferred_draw_indices { + let deferred_draw = &mut deferred_draws[*deferred_draw_ix]; + self.window.element_id_stack = deferred_draw.element_id_stack.clone(); + self.window.text_style_stack = deferred_draw.text_style_stack.clone(); + self.window + .next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let layout_start = self.after_layout_index(); + if let Some(element) = deferred_draw.element.as_mut() { + self.with_absolute_element_offset(deferred_draw.absolute_offset, |cx| { + element.after_layout(cx) + }); + } else { + self.reuse_after_layout(deferred_draw.layout_range.clone()); + } + let layout_end = self.after_layout_index(); + deferred_draw.layout_range = layout_start..layout_end; + } + assert_eq!( + self.window.next_frame.deferred_draws.len(), + 0, + "cannot call defer_draw during deferred drawing" + ); + self.window.next_frame.deferred_draws = deferred_draws; + self.window.element_id_stack.clear(); + } + + fn paint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { + assert_eq!(self.window.element_id_stack.len(), 0); + + let mut deferred_draws = mem::take(&mut self.window.next_frame.deferred_draws); + for deferred_draw_ix in deferred_draw_indices { + let mut deferred_draw = &mut deferred_draws[*deferred_draw_ix]; + self.window.element_id_stack = deferred_draw.element_id_stack.clone(); + self.window + .next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let paint_start = self.paint_index(); + if let Some(element) = deferred_draw.element.as_mut() { + element.paint(self); + } else { + self.reuse_paint(deferred_draw.paint_range.clone()); + } + let paint_end = self.paint_index(); + deferred_draw.paint_range = paint_start..paint_end; + } + self.window.next_frame.deferred_draws = deferred_draws; + self.window.element_id_stack.clear(); + } + + pub(crate) fn after_layout_index(&self) -> AfterLayoutIndex { + AfterLayoutIndex { + hitboxes_index: self.window.next_frame.hitboxes.len(), + tooltips_index: self.window.next_frame.tooltip_requests.len(), + deferred_draws_index: self.window.next_frame.deferred_draws.len(), + dispatch_tree_index: self.window.next_frame.dispatch_tree.len(), + accessed_element_states_index: self.window.next_frame.accessed_element_states.len(), + line_layout_index: self.window.text_system.layout_index(), + } + } + + pub(crate) fn reuse_after_layout(&mut self, range: Range) { + let window = &mut self.window; + window.next_frame.hitboxes.extend( + window.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index] + .iter() + .cloned(), + ); + window.next_frame.tooltip_requests.extend( + window.rendered_frame.tooltip_requests + [range.start.tooltips_index..range.end.tooltips_index] + .iter_mut() + .map(|request| request.take()), + ); + window.next_frame.accessed_element_states.extend( + window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index + ..range.end.accessed_element_states_index] + .iter() + .cloned(), + ); + window + .text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + + let reused_subtree = window.next_frame.dispatch_tree.reuse_subtree( + range.start.dispatch_tree_index..range.end.dispatch_tree_index, + &mut window.rendered_frame.dispatch_tree, + ); + window.next_frame.deferred_draws.extend( + window.rendered_frame.deferred_draws + [range.start.deferred_draws_index..range.end.deferred_draws_index] + .iter() + .map(|deferred_draw| DeferredDraw { + parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), + element_id_stack: deferred_draw.element_id_stack.clone(), + text_style_stack: deferred_draw.text_style_stack.clone(), + priority: deferred_draw.priority, + element: None, + absolute_offset: deferred_draw.absolute_offset, + layout_range: deferred_draw.layout_range.clone(), + paint_range: deferred_draw.paint_range.clone(), + }), + ); + } + + pub(crate) fn paint_index(&self) -> PaintIndex { + PaintIndex { + scene_index: self.window.next_frame.scene.len(), + mouse_listeners_index: self.window.next_frame.mouse_listeners.len(), + input_handlers_index: self.window.next_frame.input_handlers.len(), + cursor_styles_index: self.window.next_frame.cursor_styles.len(), + accessed_element_states_index: self.window.next_frame.accessed_element_states.len(), + line_layout_index: self.window.text_system.layout_index(), + } + } + + pub(crate) fn reuse_paint(&mut self, range: Range) { + let window = &mut self.cx.window; + + window.next_frame.cursor_styles.extend( + window.rendered_frame.cursor_styles + [range.start.cursor_styles_index..range.end.cursor_styles_index] + .iter() + .cloned(), + ); + window.next_frame.input_handlers.extend( + window.rendered_frame.input_handlers + [range.start.input_handlers_index..range.end.input_handlers_index] + .iter_mut() + .map(|handler| handler.take()), + ); + window.next_frame.mouse_listeners.extend( + window.rendered_frame.mouse_listeners + [range.start.mouse_listeners_index..range.end.mouse_listeners_index] + .iter_mut() + .map(|listener| listener.take()), + ); + window.next_frame.accessed_element_states.extend( + window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index + ..range.end.accessed_element_states_index] + .iter() + .cloned(), + ); + window + .text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + window.next_frame.scene.replay( + range.start.scene_index..range.end.scene_index, + &window.rendered_frame.scene, ); - *self - .window - .next_frame - .next_stacking_order_ids - .last_mut() - .unwrap() = next_stacking_order_id; } /// Push a text style onto the stack, and call a function with that style active. @@ -387,9 +569,9 @@ impl<'a> ElementContext<'a> { F: FnOnce(&mut Self) -> R, { if let Some(style) = style { - self.push_text_style(style); + self.window.text_style_stack.push(style); let result = f(self); - self.pop_text_style(); + self.window.text_style_stack.pop(); result } else { f(self) @@ -397,33 +579,19 @@ impl<'a> ElementContext<'a> { } /// Updates the cursor style at the platform level. - pub fn set_cursor_style(&mut self, style: CursorStyle, stacking_order: StackingOrder) { - let view_id = self.parent_view_id(); - let style_request = CursorStyleRequest { - style, - stacking_order, - }; - if self - .window - .next_frame - .requested_cursor_style - .as_ref() - .map_or(true, |prev_style_request| { - style_request.stacking_order >= prev_style_request.stacking_order - }) - { - self.window.next_frame.requested_cursor_style = Some(style_request.clone()); - } + pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { self.window .next_frame .cursor_styles - .insert(view_id, style_request); + .push(CursorStyleRequest { + hitbox_id: hitbox.id, + style, + }); } /// Sets a tooltip to be rendered for the upcoming frame pub fn set_tooltip(&mut self, tooltip: AnyTooltip) { - let view_id = self.parent_view_id(); - self.window.next_frame.tooltip_request = Some(TooltipRequest { view_id, tooltip }); + self.window.next_frame.tooltip_requests.push(Some(tooltip)); } /// Pushes the given element id onto the global stack and invokes the given closure @@ -465,65 +633,6 @@ impl<'a> ElementContext<'a> { } } - /// Invoke the given function with the content mask reset to that - /// of the window. - pub fn break_content_mask(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { - let mask = ContentMask { - bounds: Bounds { - origin: Point::default(), - size: self.window().viewport_size, - }, - }; - - let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index); - let new_stacking_order_id = post_inc( - self.window_mut() - .next_frame - .next_stacking_order_ids - .last_mut() - .unwrap(), - ); - let new_context = StackingContext { - z_index: new_root_z_index, - id: new_stacking_order_id, - }; - - let old_stacking_order = mem::take(&mut self.window_mut().next_frame.z_index_stack); - - self.window_mut().next_frame.z_index_stack.push(new_context); - self.window_mut().next_frame.content_mask_stack.push(mask); - let result = f(self); - self.window_mut().next_frame.content_mask_stack.pop(); - self.window_mut().next_frame.z_index_stack = old_stacking_order; - - result - } - - /// Called during painting to invoke the given closure in a new stacking context. The given - /// z-index is interpreted relative to the previous call to `stack`. - pub fn with_z_index(&mut self, z_index: u16, f: impl FnOnce(&mut Self) -> R) -> R { - let new_stacking_order_id = post_inc( - self.window_mut() - .next_frame - .next_stacking_order_ids - .last_mut() - .unwrap(), - ); - self.window_mut().next_frame.next_stacking_order_ids.push(0); - let new_context = StackingContext { - z_index, - id: new_stacking_order_id, - }; - - self.window_mut().next_frame.z_index_stack.push(new_context); - let result = f(self); - self.window_mut().next_frame.z_index_stack.pop(); - - self.window_mut().next_frame.next_stacking_order_ids.pop(); - - result - } - /// Updates the global element offset relative to the current offset. This is used to implement /// scrolling. pub fn with_element_offset( @@ -592,30 +701,37 @@ impl<'a> ElementContext<'a> { /// when drawing the next frame. pub fn with_element_state( &mut self, - id: ElementId, - f: impl FnOnce(Option, &mut Self) -> (R, S), + element_id: Option, + f: impl FnOnce(Option>, &mut Self) -> (R, Option), ) -> R where S: 'static, { - self.with_element_id(Some(id), |cx| { + let id_is_none = element_id.is_none(); + self.with_element_id(element_id, |cx| { + if id_is_none { + let (result, state) = f(None, cx); + debug_assert!(state.is_none(), "you must not return an element state when passing None for the element id"); + result + } else { let global_id = cx.window().element_id_stack.clone(); + let key = (global_id, TypeId::of::()); + cx.window.next_frame.accessed_element_states.push(key.clone()); if let Some(any) = cx .window_mut() .next_frame .element_states - .remove(&global_id) + .remove(&key) .or_else(|| { cx.window_mut() .rendered_frame .element_states - .remove(&global_id) + .remove(&key) }) { let ElementStateBox { inner, - parent_view_id, #[cfg(debug_assertions)] type_name } = any; @@ -646,29 +762,26 @@ impl<'a> ElementContext<'a> { // Requested: () <- AnyElement let state = state_box .take() - .expect("element state is already on the stack"); - let (result, state) = f(Some(state), cx); - state_box.replace(state); + .expect("reentrant call to with_element_state for the same state type and element id"); + let (result, state) = f(Some(Some(state)), cx); + state_box.replace(state.expect("you must return ")); cx.window_mut() .next_frame .element_states - .insert(global_id, ElementStateBox { + .insert(key, ElementStateBox { inner: state_box, - parent_view_id, #[cfg(debug_assertions)] type_name }); result } else { - let (result, state) = f(None, cx); - let parent_view_id = cx.parent_view_id(); + let (result, state) = f(Some(None), cx); cx.window_mut() .next_frame .element_states - .insert(global_id, + .insert(key, ElementStateBox { - inner: Box::new(Some(state)), - parent_view_id, + inner: Box::new(Some(state.expect("you must return Some when you pass some element id"))), #[cfg(debug_assertions)] type_name: std::any::type_name::() } @@ -676,8 +789,61 @@ impl<'a> ElementContext<'a> { ); result } - }) + } + }) } + + /// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree + /// at a later time. The `priority` parameter determines the drawing order relative to other deferred elements, + /// with higher values being drawn on top. + pub fn defer_draw( + &mut self, + element: AnyElement, + absolute_offset: Point, + priority: usize, + ) { + let window = &mut self.cx.window; + assert_eq!( + window.draw_phase, + DrawPhase::Layout, + "defer_draw can only be called during before_layout or after_layout" + ); + let parent_node = window.next_frame.dispatch_tree.active_node_id().unwrap(); + window.next_frame.deferred_draws.push(DeferredDraw { + parent_node, + element_id_stack: window.element_id_stack.clone(), + text_style_stack: window.text_style_stack.clone(), + priority, + element: Some(element), + absolute_offset, + layout_range: AfterLayoutIndex::default()..AfterLayoutIndex::default(), + paint_range: PaintIndex::default()..PaintIndex::default(), + }); + } + + /// Creates a new painting layer for the specified bounds. A "layer" is a batch + /// of geometry that are non-overlapping and have the same draw order. This is typically used + /// for performance reasons. + pub fn paint_layer(&mut self, bounds: Bounds, f: impl FnOnce(&mut Self) -> R) -> R { + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let clipped_bounds = bounds.intersect(&content_mask.bounds); + if !clipped_bounds.is_empty() { + self.window + .next_frame + .scene + .push_layer(clipped_bounds.scale(scale_factor)); + } + + let result = f(self); + + if !clipped_bounds.is_empty() { + self.window.next_frame.scene.pop_layer(); + } + + result + } + /// Paint one or more drop shadows into the scene for the next frame at the current z-index. pub fn paint_shadows( &mut self, @@ -687,26 +853,19 @@ impl<'a> ElementContext<'a> { ) { let scale_factor = self.scale_factor(); let content_mask = self.content_mask(); - let view_id = self.parent_view_id(); - let window = &mut *self.window; for shadow in shadows { let mut shadow_bounds = bounds; shadow_bounds.origin += shadow.offset; shadow_bounds.dilate(shadow.spread_radius); - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - Shadow { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds: shadow_bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - corner_radii: corner_radii.scale(scale_factor), - color: shadow.color, - blur_radius: shadow.blur_radius.scale(scale_factor), - pad: 0, - }, - ); + self.window.next_frame.scene.insert_primitive(Shadow { + order: 0, + bounds: shadow_bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + corner_radii: corner_radii.scale(scale_factor), + color: shadow.color, + blur_radius: shadow.blur_radius.scale(scale_factor), + pad: 0, + }); } } @@ -716,39 +875,27 @@ impl<'a> ElementContext<'a> { pub fn paint_quad(&mut self, quad: PaintQuad) { let scale_factor = self.scale_factor(); let content_mask = self.content_mask(); - let view_id = self.parent_view_id(); - - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - Quad { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds: quad.bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - background: quad.background, - border_color: quad.border_color, - corner_radii: quad.corner_radii.scale(scale_factor), - border_widths: quad.border_widths.scale(scale_factor), - }, - ); + self.window.next_frame.scene.insert_primitive(Quad { + order: 0, + bounds: quad.bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + background: quad.background, + border_color: quad.border_color, + corner_radii: quad.corner_radii.scale(scale_factor), + border_widths: quad.border_widths.scale(scale_factor), + }); } /// Paint the given `Path` into the scene for the next frame at the current z-index. pub fn paint_path(&mut self, mut path: Path, color: impl Into) { let scale_factor = self.scale_factor(); let content_mask = self.content_mask(); - let view_id = self.parent_view_id(); - path.content_mask = content_mask; path.color = color.into(); - path.view_id = view_id.into(); - let window = &mut *self.window; - window + self.window .next_frame .scene - .insert(&window.next_frame.z_index_stack, path.scale(scale_factor)); + .insert_primitive(path.scale(scale_factor)); } /// Paint an underline into the scene for the next frame at the current z-index. @@ -769,22 +916,15 @@ impl<'a> ElementContext<'a> { size: size(width, height), }; let content_mask = self.content_mask(); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - Underline { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds: bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - color: style.color.unwrap_or_default(), - thickness: style.thickness.scale(scale_factor), - wavy: style.wavy, - }, - ); + self.window.next_frame.scene.insert_primitive(Underline { + order: 0, + bounds: bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + color: style.color.unwrap_or_default(), + thickness: style.thickness.scale(scale_factor), + wavy: style.wavy, + }); } /// Paint a strikethrough into the scene for the next frame at the current z-index. @@ -801,22 +941,15 @@ impl<'a> ElementContext<'a> { size: size(width, height), }; let content_mask = self.content_mask(); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - Underline { - view_id: view_id.into(), - layer_id: 0, - order: 0, - bounds: bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - thickness: style.thickness.scale(scale_factor), - color: style.color.unwrap_or_default(), - wavy: false, - }, - ); + self.window.next_frame.scene.insert_primitive(Underline { + order: 0, + bounds: bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + thickness: style.thickness.scale(scale_factor), + color: style.color.unwrap_or_default(), + wavy: false, + }); } /// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. @@ -862,20 +995,16 @@ impl<'a> ElementContext<'a> { size: tile.bounds.size.map(Into::into), }; let content_mask = self.content_mask().scale(scale_factor); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - MonochromeSprite { - view_id: view_id.into(), - layer_id: 0, + self.window + .next_frame + .scene + .insert_primitive(MonochromeSprite { order: 0, bounds, content_mask, color, tile, - }, - ); + }); } Ok(()) } @@ -919,14 +1048,11 @@ impl<'a> ElementContext<'a> { size: tile.bounds.size.map(Into::into), }; let content_mask = self.content_mask().scale(scale_factor); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - PolychromeSprite { - view_id: view_id.into(), - layer_id: 0, + self.window + .next_frame + .scene + .insert_primitive(PolychromeSprite { order: 0, bounds, corner_radii: Default::default(), @@ -934,8 +1060,7 @@ impl<'a> ElementContext<'a> { tile, grayscale: false, pad: 0, - }, - ); + }); } Ok(()) } @@ -965,21 +1090,17 @@ impl<'a> ElementContext<'a> { Ok((params.size, Cow::Owned(bytes))) })?; let content_mask = self.content_mask().scale(scale_factor); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - MonochromeSprite { - view_id: view_id.into(), - layer_id: 0, + self.window + .next_frame + .scene + .insert_primitive(MonochromeSprite { order: 0, bounds, content_mask, color, tile, - }, - ); + }); Ok(()) } @@ -1004,14 +1125,11 @@ impl<'a> ElementContext<'a> { })?; let content_mask = self.content_mask().scale(scale_factor); let corner_radii = corner_radii.scale(scale_factor); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - PolychromeSprite { - view_id: view_id.into(), - layer_id: 0, + self.window + .next_frame + .scene + .insert_primitive(PolychromeSprite { order: 0, bounds, content_mask, @@ -1019,8 +1137,7 @@ impl<'a> ElementContext<'a> { tile, grayscale, pad: 0, - }, - ); + }); Ok(()) } @@ -1030,19 +1147,15 @@ impl<'a> ElementContext<'a> { let scale_factor = self.scale_factor(); let bounds = bounds.scale(scale_factor); let content_mask = self.content_mask().scale(scale_factor); - let view_id = self.parent_view_id(); - let window = &mut *self.window; - window.next_frame.scene.insert( - &window.next_frame.z_index_stack, - crate::Surface { - view_id: view_id.into(), - layer_id: 0, + self.window + .next_frame + .scene + .insert_primitive(crate::Surface { order: 0, bounds, content_mask, image_buffer, - }, - ); + }); } #[must_use] @@ -1063,7 +1176,7 @@ impl<'a> ElementContext<'a> { .layout_engine .as_mut() .unwrap() - .request_layout(style, rem_size, &self.cx.app.layout_id_buffer) + .before_layout(style, rem_size, &self.cx.app.layout_id_buffer) } /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, @@ -1111,81 +1224,44 @@ impl<'a> ElementContext<'a> { bounds } - pub(crate) fn layout_style(&self, layout_id: LayoutId) -> Option<&Style> { - self.window - .layout_engine - .as_ref() - .unwrap() - .requested_style(layout_id) - } - - /// Called during painting to track which z-index is on top at each pixel position - pub fn add_opaque_layer(&mut self, bounds: Bounds) { - let stacking_order = self.window.next_frame.z_index_stack.clone(); - let view_id = self.parent_view_id(); - let depth_map = &mut self.window.next_frame.depth_map; - match depth_map.binary_search_by(|(level, _, _)| stacking_order.cmp(level)) { - Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, view_id, bounds)), - } - } - - /// Invoke the given function with the given focus handle present on the key dispatch stack. - /// If you want an element to participate in key dispatch, use this method to push its key context and focus handle into the stack during paint. - pub fn with_key_dispatch( - &mut self, - context: Option, - focus_handle: Option, - f: impl FnOnce(Option, &mut Self) -> R, - ) -> R { + /// This method should be called during `after_layout`. You can use + /// the returned [Hitbox] during `paint` or in an event handler + /// to determine whether the inserted hitbox was the topmost. + pub fn insert_hitbox(&mut self, bounds: Bounds, opaque: bool) -> Hitbox { + let content_mask = self.content_mask(); let window = &mut self.window; - let focus_id = focus_handle.as_ref().map(|handle| handle.id); - window + let id = window.next_hitbox_id; + window.next_hitbox_id.0 += 1; + let hitbox = Hitbox { + id, + bounds: bounds.intersect(&content_mask.bounds), + opaque, + }; + window.next_frame.hitboxes.push(hitbox.clone()); + hitbox + } + + /// Sets the key context for the current element. This context will be used to translate + /// keybindings into actions. + pub fn set_key_context(&mut self, context: KeyContext) { + self.window .next_frame .dispatch_tree - .push_node(context.clone(), focus_id, None); - - let result = f(focus_handle, self); - - self.window.next_frame.dispatch_tree.pop_node(); - - result + .set_key_context(context); } - /// Invoke the given function with the given view id present on the view stack. - /// This is a fairly low-level method used to layout views. - pub fn with_view_id(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R { - let text_system = self.text_system().clone(); - text_system.with_view(view_id, || { - if self.window.next_frame.view_stack.last() == Some(&view_id) { - f(self) - } else { - self.window.next_frame.view_stack.push(view_id); - let result = f(self); - self.window.next_frame.view_stack.pop(); - result - } - }) + /// Sets the focus handle for the current element. This handle will be used to manage focus state + /// and keyboard event dispatch for the element. + pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle) { + self.window + .next_frame + .dispatch_tree + .set_focus_id(focus_handle.id); } - /// Invoke the given function with the given view id present on the view stack. - /// This is a fairly low-level method used to paint views. - pub fn paint_view(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R { - let text_system = self.text_system().clone(); - text_system.with_view(view_id, || { - if self.window.next_frame.view_stack.last() == Some(&view_id) { - f(self) - } else { - self.window.next_frame.view_stack.push(view_id); - self.window - .next_frame - .dispatch_tree - .push_node(None, None, Some(view_id)); - let result = f(self); - self.window.next_frame.dispatch_tree.pop_node(); - self.window.next_frame.view_stack.pop(); - result - } - }) + /// Sets the view id for the current element, which will be used to manage view caching. + pub fn set_view_id(&mut self, view_id: EntityId) { + self.window.next_frame.dispatch_tree.set_view_id(view_id); } /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the @@ -1196,14 +1272,11 @@ impl<'a> ElementContext<'a> { /// [element_input_handler]: crate::ElementInputHandler pub fn handle_input(&mut self, focus_handle: &FocusHandle, input_handler: impl InputHandler) { if focus_handle.is_focused(self) { - let view_id = self.parent_view_id(); - self.window.next_frame.requested_input_handler = Some(RequestedInputHandler { - view_id, - handler: Some(PlatformInputHandler::new( - self.to_async(), - Box::new(input_handler), - )), - }) + let cx = self.to_async(); + self.window + .next_frame + .input_handlers + .push(Some(PlatformInputHandler::new(cx, Box::new(input_handler)))); } } @@ -1214,22 +1287,13 @@ impl<'a> ElementContext<'a> { &mut self, mut handler: impl FnMut(&Event, DispatchPhase, &mut ElementContext) + 'static, ) { - let view_id = self.parent_view_id(); - let order = self.window.next_frame.z_index_stack.clone(); - self.window - .next_frame - .mouse_listeners - .entry(TypeId::of::()) - .or_default() - .push(( - order, - view_id, - Box::new( - move |event: &dyn Any, phase: DispatchPhase, cx: &mut ElementContext<'_>| { - handler(event.downcast_ref().unwrap(), phase, cx) - }, - ), - )) + self.window.next_frame.mouse_listeners.push(Some(Box::new( + move |event: &dyn Any, phase: DispatchPhase, cx: &mut ElementContext<'_>| { + if let Some(event) = event.downcast_ref() { + handler(event, phase, cx) + } + }, + ))); } /// Register a key event listener on the window for the next frame. The type of event diff --git a/crates/gpui/src/window/prompts.rs b/crates/gpui/src/window/prompts.rs index 9c75f223db..69e4f88ea0 100644 --- a/crates/gpui/src/window/prompts.rs +++ b/crates/gpui/src/window/prompts.rs @@ -3,9 +3,9 @@ use std::ops::Deref; use futures::channel::oneshot; use crate::{ - div, opaque_grey, white, AnyElement, AnyView, ElementContext, EventEmitter, FocusHandle, - FocusableView, InteractiveElement, IntoElement, ParentElement, PromptLevel, Render, - StatefulInteractiveElement, Styled, View, ViewContext, VisualContext, WindowContext, + div, opaque_grey, white, AnyView, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + IntoElement, ParentElement, PromptLevel, Render, StatefulInteractiveElement, Styled, View, + ViewContext, VisualContext, WindowContext, }; /// The event emitted when a prompt's option is selected. @@ -57,13 +57,7 @@ impl PromptHandle { /// A prompt handle capable of being rendered in a window. pub struct RenderablePromptHandle { - view: Box, -} - -impl RenderablePromptHandle { - pub(crate) fn paint(&mut self, _: &mut ElementContext) -> AnyElement { - self.view.any_view().into_any_element() - } + pub(crate) view: Box, } /// Use this function in conjunction with [AppContext::set_prompt_renderer] to force @@ -146,7 +140,6 @@ impl Render for FallbackPromptRenderer { div() .size_full() - .z_index(u16::MAX) .child( div() .size_full() @@ -184,7 +177,7 @@ impl FocusableView for FallbackPromptRenderer { } } -trait PromptViewHandle { +pub(crate) trait PromptViewHandle { fn any_view(&self) -> AnyView; } diff --git a/crates/gpui_macros/src/derive_into_element.rs b/crates/gpui_macros/src/derive_into_element.rs index c3015d8c09..e430c1dfaf 100644 --- a/crates/gpui_macros/src/derive_into_element.rs +++ b/crates/gpui_macros/src/derive_into_element.rs @@ -13,10 +13,6 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream { { type Element = gpui::Component; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { gpui::Component::new(self) } diff --git a/crates/install_cli/src/install_cli.rs b/crates/install_cli/src/install_cli.rs index b9956ebbb9..506de309ef 100644 --- a/crates/install_cli/src/install_cli.rs +++ b/crates/install_cli/src/install_cli.rs @@ -18,7 +18,7 @@ pub async fn install_cli(cx: &AsyncAppContext) -> Result { // If the symlink is not there or is outdated, first try replacing it // without escalating. smol::fs::remove_file(link_path).await.log_err(); - // todo(windows) + // todo("windows") #[cfg(not(windows))] { if smol::fs::unix::symlink(&cli_path, link_path) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index f1590b6cb8..6de1df9580 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -807,7 +807,7 @@ impl Render for LspLogToolbarItemView { .justify_between() .child(Label::new(RPC_MESSAGES)) .child( - div().z_index(120).child( + div().child( Checkbox::new( ix, if row.rpc_trace_enabled { diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 0340180442..6b5b325ee7 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -1,9 +1,9 @@ use editor::{scroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ - actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div, - EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, - MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Render, Styled, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, + 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}; @@ -281,7 +281,7 @@ impl Render for SyntaxTreeView { .and_then(|buffer| buffer.active_layer.as_ref()) { let layer = layer.clone(); - let list = uniform_list( + let mut list = uniform_list( cx.view().clone(), "SyntaxTreeView", layer.node().descendant_count(), @@ -360,16 +360,16 @@ impl Render for SyntaxTreeView { ) .size_full() .track_scroll(self.list_scroll_handle.clone()) - .text_bg(cx.theme().colors().background); + .text_bg(cx.theme().colors().background).into_any_element(); rendered = rendered.child( - canvas(move |bounds, cx| { - list.into_any_element().draw( - bounds.origin, - bounds.size.map(AvailableSpace::Definite), - cx, - ) - }) + canvas( + move |bounds, cx| { + list.layout(bounds.origin, bounds.size.into(), cx); + list + }, + |_, mut list, cx| list.paint(cx), + ) .size_full(), ); } diff --git a/crates/languages/src/csharp.rs b/crates/languages/src/csharp.rs index 0678f89926..297e397cdd 100644 --- a/crates/languages/src/csharp.rs +++ b/crates/languages/src/csharp.rs @@ -77,7 +77,7 @@ impl super::LspAdapter for OmniSharpAdapter { archive.unpack(container_dir).await?; } - // todo(windows) + // todo("windows") #[cfg(not(windows))] { fs::set_permissions( diff --git a/crates/languages/src/elixir.rs b/crates/languages/src/elixir.rs index 012401412b..471f466c84 100644 --- a/crates/languages/src/elixir.rs +++ b/crates/languages/src/elixir.rs @@ -350,7 +350,7 @@ impl LspAdapter for NextLspAdapter { } futures::io::copy(response.body_mut(), &mut file).await?; - // todo(windows) + // todo("windows") #[cfg(not(windows))] { fs::set_permissions( diff --git a/crates/languages/src/lua.rs b/crates/languages/src/lua.rs index 96bc26964f..cad9004480 100644 --- a/crates/languages/src/lua.rs +++ b/crates/languages/src/lua.rs @@ -79,7 +79,7 @@ impl super::LspAdapter for LuaLspAdapter { archive.unpack(container_dir).await?; } - // todo(windows) + // todo("windows") #[cfg(not(windows))] { fs::set_permissions( diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 652b1b5102..2d3925e7d6 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -70,7 +70,7 @@ impl LspAdapter for RustLspAdapter { let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); let mut file = File::create(&destination_path).await?; futures::io::copy(decompressed_bytes, &mut file).await?; - // todo(windows) + // todo("windows") #[cfg(not(windows))] { fs::set_permissions( diff --git a/crates/languages/src/toml.rs b/crates/languages/src/toml.rs index a2caea6dbf..1ca6bb8d1d 100644 --- a/crates/languages/src/toml.rs +++ b/crates/languages/src/toml.rs @@ -68,7 +68,7 @@ impl LspAdapter for TaploLspAdapter { futures::io::copy(decompressed_bytes, &mut file).await?; - // todo(windows) + // todo("windows") #[cfg(not(windows))] { fs::set_permissions( diff --git a/crates/languages/src/zig.rs b/crates/languages/src/zig.rs index a8ba262905..3e6f3b8824 100644 --- a/crates/languages/src/zig.rs +++ b/crates/languages/src/zig.rs @@ -73,7 +73,7 @@ impl LspAdapter for ZlsAdapter { archive.unpack(container_dir).await?; } - // todo(windows) + // todo("windows") #[cfg(not(windows))] { fs::set_permissions( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 0dd6c6ddb2..3b2bd7a9f9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1378,7 +1378,7 @@ impl ProjectPanel { let is_selected = self .selection .map_or(false, |selection| selection.entry_id == entry_id); - let width = self.width.unwrap_or(px(0.)); + let width = self.size(cx); let filename_text_color = details .git_status diff --git a/crates/storybook/src/stories.rs b/crates/storybook/src/stories.rs index 8a49c4372a..7777af2aa3 100644 --- a/crates/storybook/src/stories.rs +++ b/crates/storybook/src/stories.rs @@ -7,7 +7,6 @@ mod picker; mod scroll; mod text; mod viewport_units; -mod z_index; pub use auto_height_editor::*; pub use cursor::*; @@ -18,4 +17,3 @@ pub use picker::*; pub use scroll::*; pub use text::*; pub use viewport_units::*; -pub use z_index::*; diff --git a/crates/storybook/src/stories/z_index.rs b/crates/storybook/src/stories/z_index.rs deleted file mode 100644 index e32e39a2d1..0000000000 --- a/crates/storybook/src/stories/z_index.rs +++ /dev/null @@ -1,172 +0,0 @@ -use gpui::{px, rgb, Div, IntoElement, Render, RenderOnce}; -use story::Story; -use ui::prelude::*; - -/// A reimplementation of the MDN `z-index` example, found here: -/// [https://developer.mozilla.org/en-US/docs/Web/CSS/z-index](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index). -pub struct ZIndexStory; - -impl Render for ZIndexStory { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - Story::container().child(Story::title("z-index")).child( - div() - .flex() - .child( - div() - .w(px(250.)) - .child(Story::label("z-index: auto")) - .child(ZIndexExample::new(0)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label("z-index: 1")) - .child(ZIndexExample::new(1)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label("z-index: 3")) - .child(ZIndexExample::new(3)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label("z-index: 5")) - .child(ZIndexExample::new(5)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label("z-index: 7")) - .child(ZIndexExample::new(7)), - ), - ) - } -} - -trait Styles: Styled + Sized { - // Trailing `_` is so we don't collide with `block` style `StyleHelpers`. - fn block_(self) -> Self { - self.absolute() - .w(px(150.)) - .h(px(50.)) - .text_color(rgb(0x000000)) - } - - fn blue(self) -> Self { - self.bg(rgb(0xe5e8fc)) - .border_5() - .border_color(rgb(0x112382)) - .line_height(px(55.)) - // HACK: Simulate `text-align: center`. - .pl(px(24.)) - } - - fn red(self) -> Self { - self.bg(rgb(0xfce5e7)) - .border_5() - .border_color(rgb(0xe3a1a7)) - // HACK: Simulate `text-align: center`. - .pl(px(8.)) - } -} - -impl Styles for Div {} - -#[derive(IntoElement)] -struct ZIndexExample { - z_index: u16, -} - -impl RenderOnce for ZIndexExample { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - div() - .relative() - .size_full() - // Example element. - .child( - div() - .absolute() - .top(px(15.)) - .left(px(15.)) - .w(px(180.)) - .h(px(230.)) - .bg(rgb(0xfcfbe5)) - .text_color(rgb(0x000000)) - .border_5() - .border_color(rgb(0xe3e0a1)) - .line_height(px(215.)) - // HACK: Simulate `text-align: center`. - .pl(px(24.)) - .z_index(self.z_index) - .child(SharedString::from(format!( - "z-index: {}", - if self.z_index == 0 { - "auto".to_string() - } else { - self.z_index.to_string() - } - ))), - ) - // Blue blocks. - .child( - div() - .blue() - .block_() - .top(px(0.)) - .left(px(0.)) - .z_index(6) - .child("z-index: 6"), - ) - .child( - div() - .blue() - .block_() - .top(px(30.)) - .left(px(30.)) - .z_index(4) - .child("z-index: 4"), - ) - .child( - div() - .blue() - .block_() - .top(px(60.)) - .left(px(60.)) - .z_index(2) - .child("z-index: 2"), - ) - // Red blocks. - .child( - div() - .red() - .block_() - .top(px(150.)) - .left(px(0.)) - .child("z-index: auto"), - ) - .child( - div() - .red() - .block_() - .top(px(180.)) - .left(px(30.)) - .child("z-index: auto"), - ) - .child( - div() - .red() - .block_() - .top(px(210.)) - .left(px(60.)) - .child("z-index: auto"), - ) - } -} - -impl ZIndexExample { - pub fn new(z_index: u16) -> Self { - Self { z_index } - } -} diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index 120e60d34a..c8cda13018 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -35,7 +35,6 @@ pub enum ComponentStory { ToggleButton, Text, ViewportUnits, - ZIndex, Picker, } @@ -67,7 +66,6 @@ impl ComponentStory { Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(), Self::ToggleButton => cx.new_view(|_| ui::ToggleButtonStory).into(), Self::ViewportUnits => cx.new_view(|_| crate::stories::ViewportUnitsStory).into(), - Self::ZIndex => cx.new_view(|_| ZIndexStory).into(), Self::Picker => PickerStory::new(cx).into(), } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index ddcf8f9428..c2c9f92eee 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -404,7 +404,7 @@ impl TerminalBuilder { #[cfg(unix)] let (fd, shell_pid) = (pty.file().as_raw_fd(), pty.child().id()); - // todo(windows) + // todo("windows") #[cfg(windows)] let (fd, shell_pid) = { let child = pty.child_watcher(); diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index c70cd87df1..e419fb6793 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,11 +1,11 @@ -use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; +use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine}; use gpui::{ - div, fill, point, px, relative, AnyElement, AvailableSpace, Bounds, DispatchPhase, Element, - ElementContext, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hsla, - InputHandler, InteractiveBounds, InteractiveElement, InteractiveElementState, Interactivity, - IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, MouseMoveEvent, - Pixels, Point, ShapedLine, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, - TextStyle, UnderlineStyle, WeakView, WhiteSpace, WindowContext, WindowTextSystem, + div, fill, point, px, relative, AnyElement, Bounds, DispatchPhase, Element, ElementContext, + FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hitbox, Hsla, InputHandler, + InteractiveElement, Interactivity, IntoElement, LayoutId, Model, ModelContext, + ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, Point, ShapedLine, + StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UnderlineStyle, + WeakView, WhiteSpace, WindowContext, WindowTextSystem, }; use itertools::Itertools; use language::CursorShape; @@ -15,10 +15,13 @@ use terminal::{ grid::Dimensions, index::Point as AlacPoint, term::{cell::Flags, TermMode}, - vte::ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, + vte::ansi::{ + Color::{self as AnsiColor, Named}, + CursorShape as AlacCursorShape, NamedColor, + }, }, terminal_settings::TerminalSettings, - IndexedCell, Terminal, TerminalContent, TerminalSize, + HoveredWord, IndexedCell, Terminal, TerminalContent, TerminalSize, }; use theme::{ActiveTheme, Theme, ThemeSettings}; use ui::Tooltip; @@ -29,16 +32,18 @@ use std::{fmt::Debug, ops::RangeInclusive}; /// The information generated during layout that is necessary for painting. pub struct LayoutState { + hitbox: Hitbox, cells: Vec, rects: Vec, relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, - cursor: Option, + cursor: Option, background_color: Hsla, dimensions: TerminalSize, mode: TermMode, display_offset: usize, hyperlink_tooltip: Option, gutter: Pixels, + last_hovered_word: Option, } /// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points. @@ -392,216 +397,6 @@ impl TerminalElement { result } - fn compute_layout(&self, bounds: Bounds, cx: &mut ElementContext) -> LayoutState { - let settings = ThemeSettings::get_global(cx).clone(); - - let buffer_font_size = settings.buffer_font_size(cx); - - let terminal_settings = TerminalSettings::get_global(cx); - let font_family = terminal_settings - .font_family - .as_ref() - .map(|string| string.clone().into()) - .unwrap_or(settings.buffer_font.family); - - let font_features = terminal_settings - .font_features - .unwrap_or(settings.buffer_font.features); - - let line_height = terminal_settings.line_height.value(); - let font_size = terminal_settings.font_size; - - let font_size = - font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)); - - let theme = cx.theme().clone(); - - let link_style = HighlightStyle { - color: Some(theme.colors().link_text_hover), - font_weight: None, - font_style: None, - background_color: None, - underline: Some(UnderlineStyle { - thickness: px(1.0), - color: Some(theme.colors().link_text_hover), - wavy: false, - }), - strikethrough: None, - fade_out: None, - }; - - let text_style = TextStyle { - font_family, - font_features, - font_size: font_size.into(), - font_style: FontStyle::Normal, - line_height: line_height.into(), - background_color: None, - white_space: WhiteSpace::Normal, - // These are going to be overridden per-cell - underline: None, - strikethrough: None, - color: theme.colors().text, - font_weight: FontWeight::NORMAL, - }; - - let text_system = cx.text_system(); - let player_color = theme.players().local(); - let match_color = theme.colors().search_match_background; - let gutter; - let dimensions = { - let rem_size = cx.rem_size(); - let font_pixels = text_style.font_size.to_pixels(rem_size); - let line_height = font_pixels * line_height.to_pixels(rem_size); - let font_id = cx.text_system().resolve_font(&text_style.font()); - - let cell_width = text_system - .advance(font_id, font_pixels, 'm') - .unwrap() - .width; - gutter = cell_width; - - let mut size = bounds.size; - size.width -= gutter; - - // https://github.com/zed-industries/zed/issues/2750 - // if the terminal is one column wide, rendering 🦀 - // causes alacritty to misbehave. - if size.width < cell_width * 2.0 { - size.width = cell_width * 2.0; - } - - TerminalSize::new(line_height, cell_width, size) - }; - - let search_matches = self.terminal.read(cx).matches.clone(); - - let background_color = theme.colors().terminal_background; - - let last_hovered_word = self.terminal.update(cx, |terminal, cx| { - terminal.set_size(dimensions); - terminal.sync(cx); - if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { - terminal.last_content.last_hovered_word.clone() - } else { - None - } - }); - - if bounds.contains(&cx.mouse_position()) { - let stacking_order = cx.stacking_order().clone(); - if self.can_navigate_to_selected_word && last_hovered_word.is_some() { - cx.set_cursor_style(gpui::CursorStyle::PointingHand, stacking_order); - } else { - cx.set_cursor_style(gpui::CursorStyle::IBeam, stacking_order); - } - } - - let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { - div() - .size_full() - .id("terminal-element") - .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx)) - .into_any_element() - }); - - let TerminalContent { - cells, - mode, - display_offset, - cursor_char, - selection, - cursor, - .. - } = &self.terminal.read(cx).last_content; - - // searches, highlights to a single range representations - let mut relative_highlighted_ranges = Vec::new(); - for search_match in search_matches { - relative_highlighted_ranges.push((search_match, match_color)) - } - if let Some(selection) = selection { - relative_highlighted_ranges - .push((selection.start..=selection.end, player_color.selection)); - } - - // then have that representation be converted to the appropriate highlight data structure - - let (cells, rects) = TerminalElement::layout_grid( - cells, - &text_style, - &cx.text_system(), - last_hovered_word - .as_ref() - .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), - cx, - ); - - // Layout cursor. Rectangle is used for IME, so we should lay it out even - // if we don't end up showing it. - let cursor = if let AlacCursorShape::Hidden = cursor.shape { - None - } else { - let cursor_point = DisplayCursor::from(cursor.point, *display_offset); - let cursor_text = { - let str_trxt = cursor_char.to_string(); - let len = str_trxt.len(); - cx.text_system() - .shape_line( - str_trxt.into(), - text_style.font_size.to_pixels(cx.rem_size()), - &[TextRun { - len, - font: text_style.font(), - color: theme.colors().terminal_background, - background_color: None, - underline: Default::default(), - strikethrough: None, - }], - ) - .unwrap() - }; - - let focused = self.focused; - TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( - move |(cursor_position, block_width)| { - let (shape, text) = match cursor.shape { - AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), - AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), - AlacCursorShape::Underline => (CursorShape::Underscore, None), - AlacCursorShape::Beam => (CursorShape::Bar, None), - AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), - //This case is handled in the if wrapping the whole cursor layout - AlacCursorShape::Hidden => unreachable!(), - }; - - Cursor::new( - cursor_position, - block_width, - dimensions.line_height, - theme.players().local().cursor, - shape, - text, - None, - ) - }, - ) - }; - - LayoutState { - cells, - cursor, - background_color, - dimensions, - rects, - relative_highlighted_ranges, - mode: *mode, - display_offset: *display_offset, - hyperlink_tooltip, - gutter, - } - } - fn generic_button_handler( connection: Model, origin: Point, @@ -622,15 +417,11 @@ impl TerminalElement { &mut self, origin: Point, mode: TermMode, - bounds: Bounds, + hitbox: &Hitbox, cx: &mut ElementContext, ) { let focus = self.focus.clone(); let terminal = self.terminal.clone(); - let interactive_bounds = InteractiveBounds { - bounds: bounds.intersect(&cx.content_mask().bounds), - stacking_order: cx.stacking_order().clone(), - }; self.interactivity.on_mouse_down(MouseButton::Left, { let terminal = terminal.clone(); @@ -647,27 +438,28 @@ impl TerminalElement { cx.on_mouse_event({ let focus = self.focus.clone(); let terminal = self.terminal.clone(); + let hitbox = hitbox.clone(); move |e: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble || !focus.is_focused(cx) { return; } if e.pressed_button.is_some() && !cx.has_active_drag() { - let visibly_contains = interactive_bounds.visibly_contains(&e.position, cx); + let hovered = hitbox.is_hovered(cx); terminal.update(cx, |terminal, cx| { if !terminal.selection_started() { - if visibly_contains { - terminal.mouse_drag(e, origin, bounds); + if hovered { + terminal.mouse_drag(e, origin, hitbox.bounds); cx.notify(); } } else { - terminal.mouse_drag(e, origin, bounds); + terminal.mouse_drag(e, origin, hitbox.bounds); cx.notify(); } }) } - if interactive_bounds.visibly_contains(&e.position, cx) { + if hitbox.is_hovered(cx) { terminal.update(cx, |terminal, cx| { terminal.mouse_move(&e, origin); cx.notify(); @@ -749,34 +541,244 @@ impl TerminalElement { } impl Element for TerminalElement { - type State = InteractiveElementState; + type BeforeLayout = (); + type AfterLayout = LayoutState; - fn request_layout( + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + self.interactivity.occlude_mouse(); + let layout_id = self.interactivity.before_layout(cx, |mut style, cx| { + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + let layout_id = cx.request_layout(&style, None); + + layout_id + }); + (layout_id, ()) + } + + fn after_layout( &mut self, - element_state: Option, - cx: &mut ElementContext<'_>, - ) -> (LayoutId, Self::State) { - let (layout_id, interactive_state) = - self.interactivity - .layout(element_state, cx, |mut style, cx| { - style.size.width = relative(1.).into(); - style.size.height = relative(1.).into(); - let layout_id = cx.request_layout(&style, None); + bounds: Bounds, + _: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) -> Self::AfterLayout { + self.interactivity + .after_layout(bounds, bounds.size, cx, |_, _, hitbox, cx| { + let hitbox = hitbox.unwrap(); + let settings = ThemeSettings::get_global(cx).clone(); - layout_id + let buffer_font_size = settings.buffer_font_size(cx); + + let terminal_settings = TerminalSettings::get_global(cx); + let font_family = terminal_settings + .font_family + .as_ref() + .map(|string| string.clone().into()) + .unwrap_or(settings.buffer_font.family); + + let font_features = terminal_settings + .font_features + .unwrap_or(settings.buffer_font.features); + + let line_height = terminal_settings.line_height.value(); + let font_size = terminal_settings.font_size; + + let font_size = + font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)); + + let theme = cx.theme().clone(); + + let link_style = HighlightStyle { + color: Some(theme.colors().link_text_hover), + font_weight: None, + font_style: None, + background_color: None, + underline: Some(UnderlineStyle { + thickness: px(1.0), + color: Some(theme.colors().link_text_hover), + wavy: false, + }), + strikethrough: None, + fade_out: None, + }; + + let text_style = TextStyle { + font_family, + font_features, + font_size: font_size.into(), + font_style: FontStyle::Normal, + line_height: line_height.into(), + background_color: None, + white_space: WhiteSpace::Normal, + // These are going to be overridden per-cell + underline: None, + strikethrough: None, + color: theme.colors().text, + font_weight: FontWeight::NORMAL, + }; + + let text_system = cx.text_system(); + let player_color = theme.players().local(); + let match_color = theme.colors().search_match_background; + let gutter; + let dimensions = { + let rem_size = cx.rem_size(); + let font_pixels = text_style.font_size.to_pixels(rem_size); + let line_height = font_pixels * line_height.to_pixels(rem_size); + let font_id = cx.text_system().resolve_font(&text_style.font()); + + let cell_width = text_system + .advance(font_id, font_pixels, 'm') + .unwrap() + .width; + gutter = cell_width; + + let mut size = bounds.size; + size.width -= gutter; + + // https://github.com/zed-industries/zed/issues/2750 + // if the terminal is one column wide, rendering 🦀 + // causes alacritty to misbehave. + if size.width < cell_width * 2.0 { + size.width = cell_width * 2.0; + } + + TerminalSize::new(line_height, cell_width, size) + }; + + let search_matches = self.terminal.read(cx).matches.clone(); + + let background_color = theme.colors().terminal_background; + + let last_hovered_word = self.terminal.update(cx, |terminal, cx| { + terminal.set_size(dimensions); + terminal.sync(cx); + if self.can_navigate_to_selected_word + && terminal.can_navigate_to_selected_word() + { + terminal.last_content.last_hovered_word.clone() + } else { + None + } }); - (layout_id, interactive_state) + let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { + let offset = bounds.origin + Point::new(gutter, px(0.)); + let mut element = div() + .size_full() + .id("terminal-element") + .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx)) + .into_any_element(); + element.layout(offset, bounds.size.into(), cx); + element + }); + + let TerminalContent { + cells, + mode, + display_offset, + cursor_char, + selection, + cursor, + .. + } = &self.terminal.read(cx).last_content; + + // searches, highlights to a single range representations + let mut relative_highlighted_ranges = Vec::new(); + for search_match in search_matches { + relative_highlighted_ranges.push((search_match, match_color)) + } + if let Some(selection) = selection { + relative_highlighted_ranges + .push((selection.start..=selection.end, player_color.selection)); + } + + // then have that representation be converted to the appropriate highlight data structure + + let (cells, rects) = TerminalElement::layout_grid( + cells, + &text_style, + &cx.text_system(), + last_hovered_word + .as_ref() + .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), + cx, + ); + + // Layout cursor. Rectangle is used for IME, so we should lay it out even + // if we don't end up showing it. + let cursor = if let AlacCursorShape::Hidden = cursor.shape { + None + } else { + let cursor_point = DisplayCursor::from(cursor.point, *display_offset); + let cursor_text = { + let str_trxt = cursor_char.to_string(); + let len = str_trxt.len(); + cx.text_system() + .shape_line( + str_trxt.into(), + text_style.font_size.to_pixels(cx.rem_size()), + &[TextRun { + len, + font: text_style.font(), + color: theme.colors().terminal_background, + background_color: None, + underline: Default::default(), + strikethrough: None, + }], + ) + .unwrap() + }; + + let focused = self.focused; + TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( + move |(cursor_position, block_width)| { + let (shape, text) = match cursor.shape { + AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), + AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), + AlacCursorShape::Underline => (CursorShape::Underscore, None), + AlacCursorShape::Beam => (CursorShape::Bar, None), + AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), + //This case is handled in the if wrapping the whole cursor layout + AlacCursorShape::Hidden => unreachable!(), + }; + + CursorLayout::new( + cursor_position, + block_width, + dimensions.line_height, + theme.players().local().cursor, + shape, + text, + ) + }, + ) + }; + + LayoutState { + hitbox, + cells, + cursor, + background_color, + dimensions, + rects, + relative_highlighted_ranges, + mode: *mode, + display_offset: *display_offset, + hyperlink_tooltip, + gutter, + last_hovered_word, + } + }) } fn paint( &mut self, bounds: Bounds, - state: &mut Self::State, + _: &mut Self::BeforeLayout, + layout: &mut Self::AfterLayout, cx: &mut ElementContext<'_>, ) { - let mut layout = self.compute_layout(bounds, cx); - cx.paint_quad(fill(bounds, layout.background_color)); let origin = bounds.origin + Point::new(layout.gutter, px(0.)); @@ -789,10 +791,17 @@ impl Element for TerminalElement { workspace: self.workspace.clone(), }; - self.register_mouse_listeners(origin, layout.mode, bounds, cx); + self.register_mouse_listeners(origin, layout.mode, &layout.hitbox, cx); + if self.can_navigate_to_selected_word && layout.last_hovered_word.is_some() { + cx.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox); + } else { + cx.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox); + } + let cursor = layout.cursor.take(); + let hyperlink_tooltip = layout.hyperlink_tooltip.take(); self.interactivity - .paint(bounds, bounds.size, state, cx, |_, _, cx| { + .paint(bounds, Some(&layout.hitbox), cx, |_, cx| { cx.handle_input(&self.focus, terminal_input_handler); cx.on_key_event({ @@ -815,42 +824,35 @@ impl Element for TerminalElement { rect.paint(origin, &layout, cx); } - cx.with_z_index(1, |cx| { - for (relative_highlighted_range, color) in - layout.relative_highlighted_ranges.iter() + for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter() + { + if let Some((start_y, highlighted_range_lines)) = + to_highlighted_range_lines(relative_highlighted_range, &layout, origin) { - if let Some((start_y, highlighted_range_lines)) = - to_highlighted_range_lines(relative_highlighted_range, &layout, origin) - { - let hr = HighlightedRange { - start_y, //Need to change this - line_height: layout.dimensions.line_height, - lines: highlighted_range_lines, - color: *color, - //Copied from editor. TODO: move to theme or something - corner_radius: 0.15 * layout.dimensions.line_height, - }; - hr.paint(bounds, cx); - } + let hr = HighlightedRange { + start_y, //Need to change this + line_height: layout.dimensions.line_height, + lines: highlighted_range_lines, + color: *color, + //Copied from editor. TODO: move to theme or something + corner_radius: 0.15 * layout.dimensions.line_height, + }; + hr.paint(bounds, cx); } - }); - - cx.with_z_index(2, |cx| { - for cell in &layout.cells { - cell.paint(origin, &layout, bounds, cx); - } - }); - - if self.cursor_visible { - cx.with_z_index(3, |cx| { - if let Some(cursor) = &layout.cursor { - cursor.paint(origin, cx); - } - }); } - if let Some(mut element) = layout.hyperlink_tooltip.take() { - element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx) + for cell in &layout.cells { + cell.paint(origin, &layout, bounds, cx); + } + + if self.cursor_visible { + if let Some(mut cursor) = cursor { + cursor.paint(origin, cx); + } + } + + if let Some(mut element) = hyperlink_tooltip { + element.paint(cx); } }); } @@ -859,10 +861,6 @@ impl Element for TerminalElement { impl IntoElement for TerminalElement { type Element = Self; - fn element_id(&self) -> Option { - Some("terminal".into()) - } - fn into_element(self) -> Self::Element { self } diff --git a/crates/theme/src/styles/stories/players.rs b/crates/theme/src/styles/stories/players.rs index 21af258641..6d3ec5913e 100644 --- a/crates/theme/src/styles/stories/players.rs +++ b/crates/theme/src/styles/stories/players.rs @@ -77,7 +77,6 @@ impl Render for PlayerStory { .relative() .neg_mx_1() .rounded_full() - .z_index(3) .border_2() .border_color(player.background) .size(px(28.)) @@ -93,7 +92,6 @@ impl Render for PlayerStory { .relative() .neg_mx_1() .rounded_full() - .z_index(2) .border_2() .border_color(player.background) .size(px(28.)) @@ -109,7 +107,6 @@ impl Render for PlayerStory { .relative() .neg_mx_1() .rounded_full() - .z_index(1) .border_2() .border_color(player.background) .size(px(28.)) diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index d93b280e4b..1f8913bbd2 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -122,9 +122,6 @@ impl RenderOnce for Avatar { .size(image_size) .bg(cx.theme().colors().ghost_element_background), ) - .children( - self.indicator - .map(|indicator| div().z_index(1).child(indicator)), - ) + .children(self.indicator.map(|indicator| div().child(indicator))) } } diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 23a6a5e168..8ee86278b4 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -243,7 +243,7 @@ impl ContextMenuItem { impl Render for ContextMenu { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div().elevation_2(cx).flex().flex_row().child( + div().occlude().elevation_2(cx).flex().flex_row().child( v_flex() .min_w(px(200.)) .track_focus(&self.focus_handle) diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index de4de73f42..41d4e9af9d 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -2,9 +2,9 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ overlay, point, prelude::FluentBuilder, px, rems, AnchorCorner, AnyElement, Bounds, - DismissEvent, DispatchPhase, Element, ElementContext, ElementId, InteractiveBounds, - IntoElement, LayoutId, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View, - VisualContext, WindowContext, + DismissEvent, DispatchPhase, Element, ElementContext, ElementId, HitboxId, IntoElement, + LayoutId, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext, + WindowContext, }; use crate::{Clickable, Selectable}; @@ -109,6 +109,21 @@ impl PopoverMenu { } }) } + + fn with_element_state( + &mut self, + cx: &mut ElementContext, + f: impl FnOnce(&mut Self, &mut PopoverMenuElementState, &mut ElementContext) -> R, + ) -> R { + cx.with_element_state::, _>( + Some(self.id.clone()), + |element_state, cx| { + let mut element_state = element_state.unwrap().unwrap_or_default(); + let result = f(self, &mut element_state, cx); + (result, Some(element_state)) + }, + ) + } } /// Creates a [`PopoverMenu`] @@ -123,114 +138,136 @@ pub fn popover_menu(id: impl Into) -> PopoverMenu } } -pub struct PopoverMenuState { +pub struct PopoverMenuElementState { + menu: Rc>>>, + child_bounds: Option>, +} + +impl Clone for PopoverMenuElementState { + fn clone(&self) -> Self { + Self { + menu: Rc::clone(&self.menu), + child_bounds: self.child_bounds, + } + } +} + +impl Default for PopoverMenuElementState { + fn default() -> Self { + Self { + menu: Rc::default(), + child_bounds: None, + } + } +} + +pub struct PopoverMenuFrameState { child_layout_id: Option, child_element: Option, - child_bounds: Option>, menu_element: Option, - menu: Rc>>>, } impl Element for PopoverMenu { - type State = PopoverMenuState; + type BeforeLayout = PopoverMenuFrameState; + type AfterLayout = Option; - fn request_layout( + fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, Self::BeforeLayout) { + self.with_element_state(cx, |this, element_state, cx| { + let mut menu_layout_id = None; + + let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| { + let mut overlay = overlay().snap_to_window().anchor(this.anchor); + + if let Some(child_bounds) = element_state.child_bounds { + overlay = overlay.position( + this.resolved_attach().corner(child_bounds) + this.resolved_offset(cx), + ); + } + + let mut element = overlay.child(menu.clone()).into_any(); + menu_layout_id = Some(element.before_layout(cx)); + element + }); + + let mut child_element = this.child_builder.take().map(|child_builder| { + (child_builder)(element_state.menu.clone(), this.menu_builder.clone()) + }); + + let child_layout_id = child_element + .as_mut() + .map(|child_element| child_element.before_layout(cx)); + + let layout_id = cx.request_layout( + &gpui::Style::default(), + menu_layout_id.into_iter().chain(child_layout_id), + ); + + ( + layout_id, + PopoverMenuFrameState { + child_element, + child_layout_id, + menu_element, + }, + ) + }) + } + + fn after_layout( &mut self, - element_state: Option, + _bounds: Bounds, + before_layout: &mut Self::BeforeLayout, cx: &mut ElementContext, - ) -> (gpui::LayoutId, Self::State) { - let mut menu_layout_id = None; - - let (menu, child_bounds) = if let Some(element_state) = element_state { - (element_state.menu, element_state.child_bounds) - } else { - (Rc::default(), None) - }; - - let menu_element = menu.borrow_mut().as_mut().map(|menu| { - let mut overlay = overlay().snap_to_window().anchor(self.anchor); - - if let Some(child_bounds) = child_bounds { - overlay = overlay.position( - self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx), - ); + ) -> Option { + self.with_element_state(cx, |_this, element_state, cx| { + if let Some(child) = before_layout.child_element.as_mut() { + child.after_layout(cx); } - let mut element = overlay.child(menu.clone()).into_any(); - menu_layout_id = Some(element.request_layout(cx)); - element - }); + if let Some(menu) = before_layout.menu_element.as_mut() { + menu.after_layout(cx); + } - let mut child_element = self - .child_builder - .take() - .map(|child_builder| (child_builder)(menu.clone(), self.menu_builder.clone())); - - let child_layout_id = child_element - .as_mut() - .map(|child_element| child_element.request_layout(cx)); - - let layout_id = cx.request_layout( - &gpui::Style::default(), - menu_layout_id.into_iter().chain(child_layout_id), - ); - - ( - layout_id, - PopoverMenuState { - menu, - child_element, - child_layout_id, - menu_element, - child_bounds, - }, - ) + before_layout.child_layout_id.map(|layout_id| { + let bounds = cx.layout_bounds(layout_id); + element_state.child_bounds = Some(bounds); + cx.insert_hitbox(bounds, false).id + }) + }) } fn paint( &mut self, _: Bounds, - element_state: &mut Self::State, + before_layout: &mut Self::BeforeLayout, + child_hitbox: &mut Option, cx: &mut ElementContext, ) { - if let Some(mut child) = element_state.child_element.take() { - child.paint(cx); - } - - if let Some(child_layout_id) = element_state.child_layout_id.take() { - element_state.child_bounds = Some(cx.layout_bounds(child_layout_id)); - } - - if let Some(mut menu) = element_state.menu_element.take() { - menu.paint(cx); - - if let Some(child_bounds) = element_state.child_bounds { - let interactive_bounds = InteractiveBounds { - bounds: child_bounds, - stacking_order: cx.stacking_order().clone(), - }; - - // Mouse-downing outside the menu dismisses it, so we don't - // want a click on the toggle to re-open it. - cx.on_mouse_event(move |e: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && interactive_bounds.visibly_contains(&e.position, cx) - { - cx.stop_propagation() - } - }) + self.with_element_state(cx, |_this, _element_state, cx| { + if let Some(mut child) = before_layout.child_element.take() { + child.paint(cx); } - } + + if let Some(mut menu) = before_layout.menu_element.take() { + menu.paint(cx); + + if let Some(child_hitbox) = *child_hitbox { + // Mouse-downing outside the menu dismisses it, so we don't + // want a click on the toggle to re-open it. + cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(cx) { + cx.stop_propagation() + } + }) + } + } + }) } } impl IntoElement for PopoverMenu { type Element = Self; - fn element_id(&self) -> Option { - Some(self.id.clone()) - } - fn into_element(self) -> Self::Element { self } diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 4f7f062ec4..bd73ed8103 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -2,7 +2,7 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, - ElementContext, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseButton, + ElementContext, ElementId, Hitbox, IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext, WindowContext, }; @@ -37,6 +37,21 @@ impl RightClickMenu { self.attach = Some(attach); self } + + fn with_element_state( + &mut self, + cx: &mut ElementContext, + f: impl FnOnce(&mut Self, &mut MenuHandleElementState, &mut ElementContext) -> R, + ) -> R { + cx.with_element_state::, _>( + Some(self.id.clone()), + |element_state, cx| { + let mut element_state = element_state.unwrap().unwrap_or_default(); + let result = f(self, &mut element_state, cx); + (result, Some(element_state)) + }, + ) + } } /// Creates a [`RightClickMenu`] @@ -50,140 +65,172 @@ pub fn right_click_menu(id: impl Into) -> RightClickM } } -pub struct MenuHandleState { +pub struct MenuHandleElementState { menu: Rc>>>, position: Rc>>, +} + +impl Clone for MenuHandleElementState { + fn clone(&self) -> Self { + Self { + menu: Rc::clone(&self.menu), + position: Rc::clone(&self.position), + } + } +} + +impl Default for MenuHandleElementState { + fn default() -> Self { + Self { + menu: Rc::default(), + position: Rc::default(), + } + } +} + +pub struct MenuHandleFrameState { child_layout_id: Option, child_element: Option, menu_element: Option, } impl Element for RightClickMenu { - type State = MenuHandleState; + type BeforeLayout = MenuHandleFrameState; + type AfterLayout = Hitbox; - fn request_layout( + fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, Self::BeforeLayout) { + self.with_element_state(cx, |this, element_state, cx| { + let mut menu_layout_id = None; + + let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| { + let mut overlay = overlay().snap_to_window(); + if let Some(anchor) = this.anchor { + overlay = overlay.anchor(anchor); + } + overlay = overlay.position(*element_state.position.borrow()); + + let mut element = overlay.child(menu.clone()).into_any(); + menu_layout_id = Some(element.before_layout(cx)); + element + }); + + let mut child_element = this + .child_builder + .take() + .map(|child_builder| (child_builder)(element_state.menu.borrow().is_some())); + + let child_layout_id = child_element + .as_mut() + .map(|child_element| child_element.before_layout(cx)); + + let layout_id = cx.request_layout( + &gpui::Style::default(), + menu_layout_id.into_iter().chain(child_layout_id), + ); + + ( + layout_id, + MenuHandleFrameState { + child_element, + child_layout_id, + menu_element, + }, + ) + }) + } + + fn after_layout( &mut self, - element_state: Option, + bounds: Bounds, + before_layout: &mut Self::BeforeLayout, cx: &mut ElementContext, - ) -> (gpui::LayoutId, Self::State) { - let (menu, position) = if let Some(element_state) = element_state { - (element_state.menu, element_state.position) - } else { - (Rc::default(), Rc::default()) - }; + ) -> Hitbox { + cx.with_element_id(Some(self.id.clone()), |cx| { + let hitbox = cx.insert_hitbox(bounds, false); - let mut menu_layout_id = None; - - let menu_element = menu.borrow_mut().as_mut().map(|menu| { - let mut overlay = overlay().snap_to_window(); - if let Some(anchor) = self.anchor { - overlay = overlay.anchor(anchor); + if let Some(child) = before_layout.child_element.as_mut() { + child.after_layout(cx); } - overlay = overlay.position(*position.borrow()); - let mut element = overlay.child(menu.clone()).into_any(); - menu_layout_id = Some(element.request_layout(cx)); - element - }); + if let Some(menu) = before_layout.menu_element.as_mut() { + menu.after_layout(cx); + } - let mut child_element = self - .child_builder - .take() - .map(|child_builder| (child_builder)(menu.borrow().is_some())); - - let child_layout_id = child_element - .as_mut() - .map(|child_element| child_element.request_layout(cx)); - - let layout_id = cx.request_layout( - &gpui::Style::default(), - menu_layout_id.into_iter().chain(child_layout_id), - ); - - ( - layout_id, - MenuHandleState { - menu, - position, - child_element, - child_layout_id, - menu_element, - }, - ) + hitbox + }) } fn paint( &mut self, - bounds: Bounds, - element_state: &mut Self::State, + _bounds: Bounds, + before_layout: &mut Self::BeforeLayout, + hitbox: &mut Self::AfterLayout, cx: &mut ElementContext, ) { - if let Some(mut child) = element_state.child_element.take() { - child.paint(cx); - } + self.with_element_state(cx, |this, element_state, cx| { + if let Some(mut child) = before_layout.child_element.take() { + child.paint(cx); + } - if let Some(mut menu) = element_state.menu_element.take() { - menu.paint(cx); - return; - } + if let Some(mut menu) = before_layout.menu_element.take() { + menu.paint(cx); + return; + } - let Some(builder) = self.menu_builder.take() else { - return; - }; - let menu = element_state.menu.clone(); - let position = element_state.position.clone(); - let attach = self.attach; - let child_layout_id = element_state.child_layout_id; - let child_bounds = cx.layout_bounds(child_layout_id.unwrap()); + let Some(builder) = this.menu_builder.take() else { + return; + }; - let interactive_bounds = InteractiveBounds { - bounds: bounds.intersect(&cx.content_mask().bounds), - stacking_order: cx.stacking_order().clone(), - }; - cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == MouseButton::Right - && interactive_bounds.visibly_contains(&event.position, cx) - { - cx.stop_propagation(); - cx.prevent_default(); + let attach = this.attach; + let menu = element_state.menu.clone(); + let position = element_state.position.clone(); + let child_layout_id = before_layout.child_layout_id; + let child_bounds = cx.layout_bounds(child_layout_id.unwrap()); - let new_menu = (builder)(cx); - let menu2 = menu.clone(); - let previous_focus_handle = cx.focused(); + let hitbox_id = hitbox.id; + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == MouseButton::Right + && hitbox_id.is_hovered(cx) + { + cx.stop_propagation(); + cx.prevent_default(); - cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| { - if modal.focus_handle(cx).contains_focused(cx) { - if let Some(previous_focus_handle) = previous_focus_handle.as_ref() { - cx.focus(previous_focus_handle); + let new_menu = (builder)(cx); + let menu2 = menu.clone(); + let previous_focus_handle = cx.focused(); + + cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| { + if modal.focus_handle(cx).contains_focused(cx) { + if let Some(previous_focus_handle) = previous_focus_handle.as_ref() { + cx.focus(previous_focus_handle); + } + } + *menu2.borrow_mut() = None; + cx.refresh(); + }) + .detach(); + cx.focus_view(&new_menu); + *menu.borrow_mut() = Some(new_menu); + *position.borrow_mut() = if child_layout_id.is_some() { + if let Some(attach) = attach { + attach.corner(child_bounds) + } else { + cx.mouse_position() } - } - *menu2.borrow_mut() = None; - cx.refresh(); - }) - .detach(); - cx.focus_view(&new_menu); - *menu.borrow_mut() = Some(new_menu); - - *position.borrow_mut() = - if let Some(attach) = attach.filter(|_| child_layout_id.is_some()) { - attach.corner(child_bounds) } else { cx.mouse_position() }; - cx.refresh(); - } - }); + cx.refresh(); + } + }); + }) } } impl IntoElement for RightClickMenu { type Element = Self; - fn element_id(&self) -> Option { - Some(self.id.clone()) - } - fn into_element(self) -> Self::Element { self } diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index abfd7284f2..0d3208d29c 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -123,7 +123,6 @@ impl RenderOnce for TabBar { .absolute() .top_0() .left_0() - .z_index(1) .size_full() .border_b() .border_color(cx.theme().colors().border), @@ -131,7 +130,6 @@ impl RenderOnce for TabBar { .child( h_flex() .id("tabs") - .z_index(2) .flex_grow() .overflow_x_scroll() .when_some(self.scroll_handle, |cx, scroll_handle| { diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index 2b4cc2b395..70c14d1051 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -7,7 +7,6 @@ use crate::{ElevationIndex, UiTextSize}; fn elevated(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) - .z_index(index.z_index()) .rounded(px(8.)) .border() .border_color(cx.theme().colors().border_variant) diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index c2605fd152..3f568d8223 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -20,17 +20,6 @@ pub enum ElevationIndex { } impl ElevationIndex { - pub fn z_index(self) -> u16 { - match self { - ElevationIndex::Background => 0, - ElevationIndex::Surface => 42, - ElevationIndex::ElevatedSurface => 84, - ElevationIndex::Wash => 126, - ElevationIndex::ModalSurface => 168, - ElevationIndex::DraggedElement => 210, - } - } - pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> { match self { ElevationIndex::Surface => smallvec![], @@ -75,16 +64,6 @@ pub enum LayerIndex { ElevatedElement, } -impl LayerIndex { - pub fn usize(&self) -> usize { - match *self { - LayerIndex::BehindElement => 0, - LayerIndex::Element => 100, - LayerIndex::ElevatedElement => 200, - } - } -} - /// An appropriate z-index for the given layer based on its intended usage. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ElementIndex { @@ -95,16 +74,3 @@ pub enum ElementIndex { Content, Overlay, } - -impl ElementIndex { - pub fn usize(&self) -> usize { - match *self { - ElementIndex::Effect => 0, - ElementIndex::Background => 100, - ElementIndex::Tint => 200, - ElementIndex::Highlight => 300, - ElementIndex::Content => 400, - ElementIndex::Overlay => 500, - } - } -} diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 278ccded11..2947d26ddf 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -4,8 +4,8 @@ use crate::{status_bar::StatusItemView, Workspace}; use gpui::{ div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton, ParentElement, - Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, - WindowContext, + Render, SharedString, StyleRefinement, Styled, Subscription, View, ViewContext, VisualContext, + WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -563,8 +563,7 @@ impl Render for Dock { cx.stop_propagation(); } })) - .z_index(1) - .block_mouse(); + .occlude(); match self.position() { DockPosition::Left => { @@ -618,7 +617,12 @@ impl Render for Dock { Axis::Horizontal => this.min_w(size).h_full(), Axis::Vertical => this.min_h(size).w_full(), }) - .child(entry.panel.to_any().cached()), + .child( + entry + .panel + .to_any() + .cached(StyleRefinement::default().v_flex().size_full()), + ), ) .child(handle) } else { diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index f67b78f7d1..db67e1618a 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -139,21 +139,15 @@ impl Render for ModalLayer { return div(); }; - div() - .absolute() - .size_full() - .top_0() - .left_0() - .z_index(169) - .child( - v_flex() - .h(px(0.0)) - .top_20() - .flex() - .flex_col() - .items_center() - .track_focus(&active_modal.focus_handle) - .child(h_flex().child(active_modal.modal.view())), - ) + div().absolute().size_full().top_0().left_0().child( + v_flex() + .h(px(0.0)) + .top_20() + .flex() + .flex_col() + .items_center() + .track_focus(&active_modal.focus_handle) + .child(h_flex().child(active_modal.modal.view())), + ) } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 493a99dbe7..5422a47f49 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1561,7 +1561,6 @@ impl Pane { fn render_menu_overlay(menu: &View) -> Div { div() .absolute() - .z_index(1) .bottom_0() .right_0() .size_0() @@ -1886,7 +1885,6 @@ impl Render for Pane { .child( // drag target div() - .z_index(1) .invisible() .absolute() .bg(theme::color_alpha( diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs deleted file mode 100644 index 3e1f6393a6..0000000000 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ /dev/null @@ -1,239 +0,0 @@ -use super::DraggedItem; -use crate::{Pane, SplitDirection, Workspace}; -use gpui::{ - color::Color, - elements::{Canvas, MouseEventHandler, ParentComponent, Stack}, - geometry::{rect::RectF, vector::Vector2F}, - platform::MouseButton, - scene::MouseUp, - AppContext, Element, EventContext, MouseState, Quad, ViewContext, WeakViewHandle, -}; -use project2::ProjectEntryId; - -pub fn dragged_item_receiver( - pane: &Pane, - region_id: usize, - drop_index: usize, - allow_same_pane: bool, - split_margin: Option, - cx: &mut ViewContext, - render_child: F, -) -> MouseEventHandler -where - Tag: 'static, - D: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> D, -{ - let drag_and_drop = cx.global::>(); - let drag_position = if (pane.can_drop)(drag_and_drop, cx) { - drag_and_drop - .currently_dragged::(cx.window()) - .map(|(drag_position, _)| drag_position) - .or_else(|| { - drag_and_drop - .currently_dragged::(cx.window()) - .map(|(drag_position, _)| drag_position) - }) - } else { - None - }; - - let mut handler = MouseEventHandler::above::(region_id, cx, |state, cx| { - // Observing hovered will cause a render when the mouse enters regardless - // of if mouse position was accessed before - let drag_position = if state.dragging() { - drag_position - } else { - None - }; - Stack::new() - .with_child(render_child(state, cx)) - .with_children(drag_position.map(|drag_position| { - Canvas::new(move |bounds, _, _, cx| { - if bounds.contains_point(drag_position) { - let overlay_region = split_margin - .and_then(|split_margin| { - drop_split_direction(drag_position, bounds, split_margin) - .map(|dir| (dir, split_margin)) - }) - .map(|(dir, margin)| dir.along_edge(bounds, margin)) - .unwrap_or(bounds); - - cx.scene().push_stacking_context(None, None); - let background = overlay_color(cx); - cx.scene().push_quad(Quad { - bounds: overlay_region, - background: Some(background), - border: Default::default(), - corner_radii: Default::default(), - }); - cx.scene().pop_stacking_context(); - } - }) - })) - }); - - if drag_position.is_some() { - handler = handler - .on_up(MouseButton::Left, { - move |event, pane, cx| { - let workspace = pane.workspace.clone(); - let pane = cx.weak_handle(); - handle_dropped_item( - event, - workspace, - &pane, - drop_index, - allow_same_pane, - split_margin, - cx, - ); - cx.notify(); - } - }) - .on_move(|_, _, cx| { - let drag_and_drop = cx.global::>(); - - if drag_and_drop - .currently_dragged::(cx.window()) - .is_some() - || drag_and_drop - .currently_dragged::(cx.window()) - .is_some() - { - cx.notify(); - } else { - cx.propagate_event(); - } - }) - } - - handler -} - -pub fn handle_dropped_item( - event: MouseUp, - workspace: WeakViewHandle, - pane: &WeakViewHandle, - index: usize, - allow_same_pane: bool, - split_margin: Option, - cx: &mut EventContext, -) { - enum Action { - Move(WeakViewHandle, usize), - Open(ProjectEntryId), - } - let drag_and_drop = cx.global::>(); - let action = if let Some((_, dragged_item)) = - drag_and_drop.currently_dragged::(cx.window()) - { - Action::Move(dragged_item.pane.clone(), dragged_item.handle.id()) - } else if let Some((_, project_entry)) = - drag_and_drop.currently_dragged::(cx.window()) - { - Action::Open(*project_entry) - } else { - cx.propagate_event(); - return; - }; - - if let Some(split_direction) = - split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin)) - { - let pane_to_split = pane.clone(); - match action { - Action::Move(from, item_id_to_move) => { - cx.window_context().defer(move |cx| { - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.split_pane_with_item( - pane_to_split, - split_direction, - from, - item_id_to_move, - cx, - ); - }) - } - }); - } - Action::Open(project_entry) => { - cx.window_context().defer(move |cx| { - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - if let Some(task) = workspace.split_pane_with_project_entry( - pane_to_split, - split_direction, - project_entry, - cx, - ) { - task.detach_and_log_err(cx); - } - }) - } - }); - } - }; - } else { - match action { - Action::Move(from, item_id) => { - if pane != &from || allow_same_pane { - let pane = pane.clone(); - cx.window_context().defer(move |cx| { - if let Some(((workspace, from), to)) = workspace - .upgrade(cx) - .zip(from.upgrade(cx)) - .zip(pane.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| { - workspace.move_item(from, to, item_id, index, cx); - }) - } - }); - } else { - cx.propagate_event(); - } - } - Action::Open(project_entry) => { - let pane = pane.clone(); - cx.window_context().defer(move |cx| { - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - if let Some(path) = - workspace.project.read(cx).path_for_entry(project_entry, cx) - { - workspace - .open_path(path, Some(pane), true, cx) - .detach_and_log_err(cx); - } - }); - } - }); - } - } - } -} - -fn drop_split_direction( - position: Vector2F, - region: RectF, - split_margin: f32, -) -> Option { - let mut min_direction = None; - let mut min_distance = split_margin; - for direction in SplitDirection::all() { - let edge_distance = (direction.edge(region) - direction.axis().component(position)).abs(); - - if edge_distance < min_distance { - min_direction = Some(direction); - min_distance = edge_distance; - } - } - - min_direction -} - -fn overlay_color(cx: &AppContext) -> Color { - theme2::current(cx).workspace.drop_target_overlay_color -} diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 0c3f9b9dc0..63e9e7d3a4 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -4,7 +4,7 @@ use call::{ActiveCall, ParticipantLocation}; use collections::HashMap; use gpui::{ point, size, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton, Pixels, - Point, View, ViewContext, + Point, StyleRefinement, View, ViewContext, }; use parking_lot::Mutex; use project::Project; @@ -239,7 +239,10 @@ impl Member { .relative() .flex_1() .size_full() - .child(AnyView::from(pane.clone()).cached()) + .child( + AnyView::from(pane.clone()) + .cached(StyleRefinement::default().v_flex().size_full()), + ) .when_some(leader_border, |this, color| { this.child( div() @@ -260,7 +263,6 @@ impl Member { .right_3() .elevation_2(cx) .p_1() - .z_index(1) .child(status_box) .when_some( leader_join_data, @@ -588,13 +590,15 @@ impl SplitDirection { mod element { + use std::mem; use std::{cell::RefCell, iter, rc::Rc, sync::Arc}; use gpui::{ - px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, IntoElement, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style, - WeakView, WindowContext, + px, relative, Along, AnyElement, Axis, Bounds, Element, IntoElement, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style, WeakView, + WindowContext, }; + use gpui::{CursorStyle, Hitbox}; use parking_lot::Mutex; use settings::Settings; use smallvec::SmallVec; @@ -637,6 +641,22 @@ mod element { workspace: WeakView, } + pub struct PaneAxisLayout { + dragged_handle: Rc>>, + children: Vec, + } + + struct PaneAxisChildLayout { + bounds: Bounds, + element: AnyElement, + handle: Option, + } + + struct PaneAxisHandleLayout { + hitbox: Hitbox, + divider_bounds: Bounds, + } + impl PaneAxisElement { pub fn with_active_pane(mut self, active_pane_ix: Option) -> Self { self.active_pane_ix = active_pane_ix; @@ -733,16 +753,11 @@ mod element { } #[allow(clippy::too_many_arguments)] - fn push_handle( - flexes: Arc>>, - dragged_handle: Rc>>, + fn layout_handle( axis: Axis, - ix: usize, pane_bounds: Bounds, - axis_bounds: Bounds, - workspace: WeakView, cx: &mut ElementContext, - ) { + ) -> PaneAxisHandleLayout { let handle_bounds = Bounds { origin: pane_bounds.origin.apply_along(axis, |origin| { origin + pane_bounds.size.along(axis) - px(HANDLE_HITBOX_SIZE / 2.) @@ -758,99 +773,53 @@ mod element { size: pane_bounds.size.apply_along(axis, |_| px(DIVIDER_SIZE)), }; - cx.with_z_index(3, |cx| { - if handle_bounds.contains(&cx.mouse_position()) { - let stacking_order = cx.stacking_order().clone(); - let cursor_style = match axis { - Axis::Vertical => CursorStyle::ResizeUpDown, - Axis::Horizontal => CursorStyle::ResizeLeftRight, - }; - cx.set_cursor_style(cursor_style, stacking_order); - } - - cx.add_opaque_layer(handle_bounds); - cx.paint_quad(gpui::fill(divider_bounds, cx.theme().colors().border)); - - cx.on_mouse_event({ - let dragged_handle = dragged_handle.clone(); - let flexes = flexes.clone(); - let workspace = workspace.clone(); - move |e: &MouseDownEvent, phase, cx| { - if phase.bubble() && handle_bounds.contains(&e.position) { - dragged_handle.replace(Some(ix)); - if e.click_count >= 2 { - let mut borrow = flexes.lock(); - *borrow = vec![1.; borrow.len()]; - workspace - .update(cx, |this, cx| this.schedule_serialize(cx)) - .log_err(); - - cx.refresh(); - } - cx.stop_propagation(); - } - } - }); - cx.on_mouse_event({ - let workspace = workspace.clone(); - move |e: &MouseMoveEvent, phase, cx| { - let dragged_handle = dragged_handle.borrow(); - - if phase.bubble() && *dragged_handle == Some(ix) { - Self::compute_resize( - &flexes, - e, - ix, - axis, - pane_bounds.origin, - axis_bounds.size, - workspace.clone(), - cx, - ) - } - } - }); - }); + PaneAxisHandleLayout { + hitbox: cx.insert_hitbox(handle_bounds, true), + divider_bounds, + } } } impl IntoElement for PaneAxisElement { type Element = Self; - fn element_id(&self) -> Option { - Some(self.basis.into()) - } - fn into_element(self) -> Self::Element { self } } impl Element for PaneAxisElement { - type State = Rc>>; + type BeforeLayout = (); + type AfterLayout = PaneAxisLayout; - fn request_layout( + fn before_layout( &mut self, - state: Option, cx: &mut ui::prelude::ElementContext, - ) -> (gpui::LayoutId, Self::State) { + ) -> (gpui::LayoutId, Self::BeforeLayout) { let mut style = Style::default(); style.flex_grow = 1.; style.flex_shrink = 1.; style.flex_basis = relative(0.).into(); style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); - let layout_id = cx.request_layout(&style, None); - let dragged_pane = state.unwrap_or_else(|| Rc::new(RefCell::new(None))); - (layout_id, dragged_pane) + (cx.request_layout(&style, None), ()) } - fn paint( + fn after_layout( &mut self, - bounds: gpui::Bounds, - state: &mut Self::State, - cx: &mut ui::prelude::ElementContext, - ) { + bounds: Bounds, + _state: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) -> PaneAxisLayout { + let dragged_handle = cx.with_element_state::>>, _>( + Some(self.basis.into()), + |state, _cx| { + let state = state + .unwrap() + .unwrap_or_else(|| Rc::new(RefCell::new(None))); + (state.clone(), Some(state)) + }, + ); let flexes = self.flexes.lock().clone(); let len = self.children.len(); debug_assert!(flexes.len() == len); @@ -875,7 +844,11 @@ mod element { let mut bounding_boxes = self.bounding_boxes.lock(); bounding_boxes.clear(); - for (ix, child) in self.children.iter_mut().enumerate() { + let mut layout = PaneAxisLayout { + dragged_handle: dragged_handle.clone(), + children: Vec::new(), + }; + for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() { let child_flex = active_pane_magnification .map(|magnification| { if self.active_pane_ix == Some(ix) { @@ -896,40 +869,105 @@ mod element { size: child_size, }; bounding_boxes.push(Some(child_bounds)); - cx.with_z_index(0, |cx| { - child.draw(origin, child_size.into(), cx); - }); + child.layout(origin, child_size.into(), cx); + origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis)); + layout.children.push(PaneAxisChildLayout { + bounds: child_bounds, + element: child, + handle: None, + }) + } + + for (ix, child_layout) in layout.children.iter_mut().enumerate() { if active_pane_magnification.is_none() { - cx.with_z_index(1, |cx| { - if ix < len - 1 { - Self::push_handle( - self.flexes.clone(), - state.clone(), - self.axis, - ix, - child_bounds, - bounds, - self.workspace.clone(), - cx, - ); + if ix < len - 1 { + child_layout.handle = + Some(Self::layout_handle(self.axis, child_layout.bounds, cx)); + } + } + } + + layout + } + + fn paint( + &mut self, + bounds: gpui::Bounds, + _: &mut Self::BeforeLayout, + layout: &mut Self::AfterLayout, + cx: &mut ui::prelude::ElementContext, + ) { + for child in &mut layout.children { + child.element.paint(cx); + } + + for (ix, child) in &mut layout.children.iter_mut().enumerate() { + if let Some(handle) = child.handle.as_mut() { + let cursor_style = match self.axis { + Axis::Vertical => CursorStyle::ResizeUpDown, + Axis::Horizontal => CursorStyle::ResizeLeftRight, + }; + cx.set_cursor_style(cursor_style, &handle.hitbox); + cx.paint_quad(gpui::fill( + handle.divider_bounds, + cx.theme().colors().border, + )); + + cx.on_mouse_event({ + let dragged_handle = layout.dragged_handle.clone(); + let flexes = self.flexes.clone(); + let workspace = self.workspace.clone(); + let handle_hitbox = handle.hitbox.clone(); + move |e: &MouseDownEvent, phase, cx| { + if phase.bubble() && handle_hitbox.is_hovered(cx) { + dragged_handle.replace(Some(ix)); + if e.click_count >= 2 { + let mut borrow = flexes.lock(); + *borrow = vec![1.; borrow.len()]; + workspace + .update(cx, |this, cx| this.schedule_serialize(cx)) + .log_err(); + + cx.refresh(); + } + cx.stop_propagation(); + } + } + }); + cx.on_mouse_event({ + let workspace = self.workspace.clone(); + let dragged_handle = layout.dragged_handle.clone(); + let flexes = self.flexes.clone(); + let child_bounds = child.bounds; + let axis = self.axis; + move |e: &MouseMoveEvent, phase, cx| { + let dragged_handle = dragged_handle.borrow(); + if phase.bubble() && *dragged_handle == Some(ix) { + Self::compute_resize( + &flexes, + e, + ix, + axis, + child_bounds.origin, + bounds.size, + workspace.clone(), + cx, + ) + } } }); } - - origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis)); } - cx.with_z_index(1, |cx| { - cx.on_mouse_event({ - let state = state.clone(); - move |_: &MouseUpEvent, phase, _cx| { - if phase.bubble() { - state.replace(None); - } + cx.on_mouse_event({ + let dragged_handle = layout.dragged_handle.clone(); + move |_: &MouseUpEvent, phase, _cx| { + if phase.bubble() { + dragged_handle.replace(None); } - }); - }) + } + }); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 00b167e07b..e402baad5e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2756,7 +2756,6 @@ impl Workspace { Some( div() .absolute() - .z_index(100) .right_3() .bottom_3() .w_112() @@ -3832,18 +3831,15 @@ impl Render for Workspace { .border_t() .border_b() .border_color(colors.border) - .child( - canvas({ - let this = cx.view().clone(); - move |bounds, cx| { - this.update(cx, |this, _cx| { - this.bounds = *bounds; - }) - } - }) + .child({ + let this = cx.view().clone(); + canvas( + move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds), + |_, _, _| {}, + ) .absolute() - .size_full(), - ) + .size_full() + }) .on_drag_move( cx.listener(|workspace, e: &DragMoveEvent, cx| { match e.drag(cx).0 { @@ -3868,7 +3864,6 @@ impl Render for Workspace { } }), ) - .child(self.modal_layer.clone()) .child( div() .flex() @@ -3917,11 +3912,11 @@ impl Render for Workspace { }, )), ) - .children(self.render_notifications(cx)) + .child(self.modal_layer.clone()) .children(self.zoomed.as_ref().and_then(|view| { let zoomed_view = view.upgrade()?; let div = div() - .z_index(1) + .occlude() .absolute() .overflow_hidden() .border_color(colors.border) @@ -3936,7 +3931,8 @@ impl Render for Workspace { Some(DockPosition::Bottom) => div.top_2().border_t(), None => div.top_2().bottom_2().left_2().right_2().border(), }) - })), + })) + .children(self.render_notifications(cx)), ) .child(self.status_bar.clone()) .children(if self.project.read(cx).is_disconnected() { @@ -4662,13 +4658,10 @@ pub fn titlebar_height(cx: &mut WindowContext) -> Pixels { struct DisconnectedOverlay; impl Element for DisconnectedOverlay { - type State = AnyElement; + type BeforeLayout = AnyElement; + type AfterLayout = (); - fn request_layout( - &mut self, - _: Option, - cx: &mut ElementContext, - ) -> (LayoutId, Self::State) { + fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { let mut background = cx.theme().colors().elevated_surface_background; background.fade_out(0.2); let mut overlay = div() @@ -4686,29 +4679,33 @@ impl Element for DisconnectedOverlay { "Your connection to the remote project has been lost.", )) .into_any(); - (overlay.request_layout(cx), overlay) + (overlay.before_layout(cx), overlay) + } + + fn after_layout( + &mut self, + bounds: Bounds, + overlay: &mut Self::BeforeLayout, + cx: &mut ElementContext, + ) { + cx.insert_hitbox(bounds, true); + overlay.after_layout(cx); } fn paint( &mut self, - bounds: Bounds, - overlay: &mut Self::State, + _: Bounds, + overlay: &mut Self::BeforeLayout, + _: &mut Self::AfterLayout, cx: &mut ElementContext, ) { - cx.with_z_index(u16::MAX, |cx| { - cx.add_opaque_layer(bounds); - overlay.paint(cx); - }) + overlay.paint(cx) } } impl IntoElement for DisconnectedOverlay { type Element = Self; - fn element_id(&self) -> Option { - None - } - fn into_element(self) -> Self::Element { self }