From c397fd9a715c6443fd8b7dcc1e0b05bbbfe6e685 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Feb 2023 15:49:34 -0800 Subject: [PATCH] Added click regions and cursor styles --- crates/editor/src/display_map/fold_map.rs | 2 +- crates/editor/src/editor.rs | 162 ++++++++++++++++++---- crates/editor/src/element.rs | 148 ++++++++++++++++++-- 3 files changed, 274 insertions(+), 38 deletions(-) diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index a2f7005e3dfa675278bd1d4080394fc3427e37ec..0b8721b1a4fb9fd4eb7ced9d525a50e996ce5365 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -370,7 +370,7 @@ impl FoldMap { } if fold.end > fold.start { - let output_text = "…"; + let output_text = "⋯"; new_transforms.push( Transform { summary: TransformSummary { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 73de63bed29fa7a3835a1559822220e67934c4f7..92338d7b3723cf58d82565fa0cabbade77ea6bf8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -39,10 +39,11 @@ use gpui::{ impl_actions, impl_internal_actions, keymap_matcher::KeymapContext, platform::CursorStyle, + scene::MouseClick, serde_json::json, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, - ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + EventContext, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HideHover, HoverState}; @@ -457,6 +458,8 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; +type TextClickedCallback = + fn(&MouseClick, &Range, &EditorSnapshot, &mut EventContext); pub struct Editor { handle: WeakViewHandle, @@ -485,6 +488,7 @@ pub struct Editor { highlighted_rows: Option>, #[allow(clippy::type_complexity)] background_highlights: BTreeMap Color, Vec>)>, + clickable_text: BTreeMap>)>, nav_history: Option, context_menu: Option, mouse_context_menu: ViewHandle, @@ -1155,6 +1159,7 @@ impl Editor { placeholder_text: None, highlighted_rows: None, background_highlights: Default::default(), + clickable_text: Default::default(), nav_history: None, context_menu: None, mouse_context_menu: cx.add_view(context_menu::ContextMenu::new), @@ -5776,15 +5781,11 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); if let Some(fold_range) = display_map.foldable_range(display_row) { - let autoscroll = { - let selections = self.selections.all::(cx); - - selections.iter().any(|selection| { - let display_range = selection.display_range(&display_map); - - fold_range.overlaps(&display_range) - }) - }; + let autoscroll = self + .selections + .all::(cx) + .iter() + .any(|selection| fold_range.overlaps(&selection.display_range(&display_map))); let fold_range = fold_range.start.to_point(&display_map)..fold_range.end.to_point(&display_map); @@ -5816,14 +5817,11 @@ impl Editor { let unfold_range = fold_at.display_row.to_points(&display_map); - let autoscroll = { - let selections = self.selections.all::(cx); - selections.iter().any(|selection| { - let display_range = selection.display_range(&display_map); - - unfold_range.overlaps(&display_range) - }) - }; + let autoscroll = self + .selections + .all::(cx) + .iter() + .any(|selection| unfold_range.overlaps(&selection.display_range(&display_map))); let unfold_range = unfold_range.start.to_point(&display_map)..unfold_range.end.to_point(&display_map); @@ -5836,7 +5834,7 @@ impl Editor { self.fold_ranges(ranges, true, cx); } - pub fn fold_ranges( + pub fn fold_ranges( &mut self, ranges: impl IntoIterator>, auto_scroll: bool, @@ -5844,15 +5842,33 @@ impl Editor { ) { let mut ranges = ranges.into_iter().peekable(); if ranges.peek().is_some() { - self.display_map.update(cx, |map, cx| map.fold(ranges, cx)); + let ranges = ranges.collect_vec(); + + self.display_map + .update(cx, |map, cx| map.fold(ranges.iter().cloned(), cx)); + if auto_scroll { self.request_autoscroll(Autoscroll::fit(), cx); } + + let snapshot = self.snapshot(cx); + let anchor_ranges = offset_to_anchors(ranges, &snapshot); + + self.change_click_ranges::(cx, |click_ranges| { + for range in anchor_ranges { + if let Err(idx) = click_ranges.binary_search_by(|click_range| { + click_range.cmp(&range, &snapshot.buffer_snapshot) + }) { + click_ranges.insert(idx, range) + } + } + }); + cx.notify(); } } - pub fn unfold_ranges( + pub fn unfold_ranges( &mut self, ranges: impl IntoIterator>, inclusive: bool, @@ -5861,11 +5877,35 @@ impl Editor { ) { let mut ranges = ranges.into_iter().peekable(); if ranges.peek().is_some() { - self.display_map - .update(cx, |map, cx| map.unfold(ranges, inclusive, cx)); + let ranges = ranges.collect_vec(); + + self.display_map.update(cx, |map, cx| { + map.unfold(ranges.iter().cloned(), inclusive, cx) + }); if auto_scroll { self.request_autoscroll(Autoscroll::fit(), cx); } + + let snapshot = self.snapshot(cx); + let anchor_ranges = offset_to_anchors(ranges, &snapshot); + + self.change_click_ranges::(cx, |click_ranges| { + for range in anchor_ranges { + let range_point = range.start.to_point(&snapshot.buffer_snapshot); + // Fold and unfold ranges start at different points in the row. + // But their rows do match, so we can use that to detect sameness. + if let Ok(idx) = click_ranges.binary_search_by(|click_range| { + click_range + .start + .to_point(&snapshot.buffer_snapshot) + .row + .cmp(&range_point.row) + }) { + click_ranges.remove(idx); + } + } + }); + cx.notify(); } } @@ -5991,6 +6031,61 @@ impl Editor { } } + pub fn change_click_ranges( + &mut self, + cx: &mut ViewContext, + change: impl FnOnce(&mut Vec>), + ) { + let mut ranges = self + .clickable_text + .remove(&TypeId::of::()) + .map(|click_range| click_range.1) + .unwrap_or_default(); + + change(&mut ranges); + + self.clickable_text + .insert(TypeId::of::(), (T::click_handler, ranges)); + + cx.notify(); + } + + pub fn click_ranges_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + ) -> Vec<(Range, TextClickedCallback)> { + let mut results = Vec::new(); + let buffer = &display_snapshot.buffer_snapshot; + for (callback, ranges) in self.clickable_text.values() { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&search_range.start, buffer); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&search_range.end, buffer).is_ge() { + break; + } + let start = range + .start + .to_point(buffer) + .to_display_point(display_snapshot); + let end = range + .end + .to_point(buffer) + .to_display_point(display_snapshot); + results.push((start..end, *callback)) + } + } + results + } + pub fn highlight_rows(&mut self, rows: Option>) { self.highlighted_rows = rows; } @@ -6369,6 +6464,25 @@ fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) } } +fn offset_to_anchors< + 'snapshot, + 'iter: 'snapshot, + T: ToOffset, + I: IntoIterator> + 'iter, +>( + ranges: I, + snapshot: &'snapshot EditorSnapshot, +) -> impl Iterator> + 'snapshot { + ranges.into_iter().map(|range| { + snapshot + .buffer_snapshot + .anchor_at(range.start.to_offset(&snapshot.buffer_snapshot), Bias::Left) + ..snapshot + .buffer_snapshot + .anchor_at(range.end.to_offset(&snapshot.buffer_snapshot), Bias::Right) + }) +} + impl EditorSnapshot { pub fn language_at(&self, position: T) -> Option<&Arc> { self.display_snapshot.buffer_snapshot.language_at(position) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5f2a3670fef2bb13e12701d9133f7882006af2d5..4cc1347fd73b89634efbb7b29b53aa2e4c30ab6e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ ToPoint, MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, + display_map::{BlockStyle, DisplayRow, DisplaySnapshot, FoldStatus, TransformBlock}, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, @@ -14,7 +14,7 @@ use crate::{ }, mouse_context_menu::DeployMouseContextMenu, scroll::actions::Scroll, - EditorStyle, GutterHover, + EditorStyle, GutterHover, TextClickedCallback, UnfoldAt, }; use clock::ReplicaId; use collections::{BTreeMap, HashMap}; @@ -30,6 +30,7 @@ use gpui::{ }, json::{self, ToJson}, platform::CursorStyle, + scene::MouseClick, text_layout::{self, Line, RunStyle, TextLayoutCache}, AppContext, Axis, Border, CursorRegion, Element, ElementBox, EventContext, LayoutContext, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MutableAppContext, @@ -115,6 +116,7 @@ impl EditorElement { fn attach_mouse_handlers( view: &WeakViewHandle, position_map: &Arc, + click_ranges: Arc, TextClickedCallback)>>, has_popovers: bool, visible_bounds: RectF, text_bounds: RectF, @@ -211,6 +213,20 @@ impl EditorElement { cx.propagate_event() } } + }) + .on_click(MouseButton::Left, { + let position_map = position_map.clone(); + move |e, cx| { + let point = + position_to_display_point(e.position, text_bounds, &position_map); + if let Some(point) = point { + for (range, callback) in click_ranges.iter() { + if range.contains(&point) { + callback(&e, range, &position_map.snapshot, cx) + } + } + } + } }), ); @@ -412,16 +428,7 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu - let point = if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } - } else { - None - }; + let point = position_to_display_point(position, text_bounds, position_map); cx.dispatch_action(UpdateGoToDefinitionLink { point, @@ -702,6 +709,7 @@ impl EditorElement { let max_glyph_width = layout.position_map.em_width; let scroll_left = scroll_position.x() * max_glyph_width; let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.); + let line_end_overshoot = 0.15 * layout.position_map.line_height; cx.scene.push_layer(Some(bounds)); @@ -714,12 +722,29 @@ impl EditorElement { }, }); + for (range, _) in layout.click_ranges.iter() { + for bound in range_to_bounds( + range, + content_origin, + scroll_left, + scroll_top, + &layout.visible_display_row_range, + line_end_overshoot, + &layout.position_map, + ) { + cx.scene.push_cursor_region(CursorRegion { + bounds: bound, + style: CursorStyle::PointingHand, + }); + } + } + for (range, color) in &layout.highlighted_ranges { self.paint_highlighted_range( range.clone(), *color, 0., - 0.15 * layout.position_map.line_height, + line_end_overshoot, layout, content_origin, scroll_top, @@ -1650,6 +1675,7 @@ impl Element for EditorElement { let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; let mut highlighted_ranges = Vec::new(); + let mut click_ranges = Vec::new(); let mut show_scrollbars = false; let mut include_root = false; let mut is_singleton = false; @@ -1662,6 +1688,7 @@ impl Element for EditorElement { let theme = cx.global::().theme.as_ref(); highlighted_ranges = view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme); + click_ranges = view.click_ranges_in_range(start_anchor..end_anchor, &display_map); let mut remote_selections = HashMap::default(); for (replica_id, line_mode, cursor_shape, selection) in display_map @@ -1917,6 +1944,7 @@ impl Element for EditorElement { active_rows, highlighted_rows, highlighted_ranges, + click_ranges: Arc::new(click_ranges), line_number_layouts, display_hunks, blocks, @@ -1948,6 +1976,7 @@ impl Element for EditorElement { Self::attach_mouse_handlers( &self.view, &layout.position_map, + layout.click_ranges.clone(), // No need to clone the vec layout.hover_popovers.is_some(), visible_bounds, text_bounds, @@ -2045,6 +2074,7 @@ pub struct LayoutState { display_hunks: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, + click_ranges: Arc, TextClickedCallback)>>, selections: Vec<(ReplicaId, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, @@ -2351,6 +2381,98 @@ impl HighlightedRange { } } +pub trait ClickRange: 'static { + fn click_handler( + click: &MouseClick, + range: &Range, + snapshot: &EditorSnapshot, + cx: &mut EventContext, + ); +} + +pub enum FoldMarker {} +impl ClickRange for FoldMarker { + fn click_handler( + _click: &MouseClick, + range: &Range, + _snapshot: &EditorSnapshot, + cx: &mut EventContext, + ) { + cx.dispatch_action(UnfoldAt { + display_row: DisplayRow(range.start.row()), + }) + } +} + +pub fn position_to_display_point( + position: Vector2F, + text_bounds: RectF, + position_map: &PositionMap, +) -> Option { + if text_bounds.contains_point(position) { + let (point, target_point) = position_map.point_for_position(text_bounds, position); + if point == target_point { + Some(point) + } else { + None + } + } else { + None + } +} + +pub fn range_to_bounds( + range: &Range, + content_origin: Vector2F, + scroll_left: f32, + scroll_top: f32, + visible_row_range: &Range, + line_end_overshoot: f32, + position_map: &PositionMap, +) -> impl Iterator { + let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new(); + + if range.start == range.end { + return bounds.into_iter(); + } + + let start_row = visible_row_range.start; + let end_row = visible_row_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 first_y = + content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top; + + for (idx, row) in row_range.enumerate() { + let line_layout = &position_map.line_layouts[(row - start_row) as usize]; + + let start_x = if row == range.start.row() { + content_origin.x() + line_layout.x_for_index(range.start.column() as usize) + - scroll_left + } else { + content_origin.x() - scroll_left + }; + + let end_x = if row == range.end.row() { + content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left + } else { + content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left + }; + + bounds.push(RectF::from_points( + vec2f(start_x, first_y + position_map.line_height * idx as f32), + vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32), + )) + } + + bounds.into_iter() +} + pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 { delta.powf(1.5) / 100.0 }