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 }