From ed3e647ed7653fadd9e5ac14a7eaf638a1de9c4d Mon Sep 17 00:00:00 2001 From: Carlos Kieliszewski <150813857+frqubit@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:24:59 -0500 Subject: [PATCH] editor: Add horizontal scrollbar (#19495) ![editor_scrollbars](https://github.com/user-attachments/assets/76c26776-8fe4-47f8-9c79-9add7d7d2151) Closes #4427 Release Notes: - Added a horizontal scrollbar to the editor panel - Added `axis` option to `scrollbar` in the Zed configuration, which can forcefully disable either the horizontal or vertical scrollbar - Added `horizontal_scroll_margin` equivalent to `vertical_scroll_margin` in the Zed configuration Rough Edges: This feature seems mostly stable from my testing. I've been using a development build for about a week with no issues. Any feedback would be appreciated. There are a few things to note as well: 1. Scrolling to the lower right occasionally causes scrollbar clipping on my end, but it isn't consistent and it isn't major. Some more testing would definitely be a good idea. [FIXED] 2. Documentation may need to be modified 3. I added an `AxisPair` type to the `editor` crate to manage values that have a horizontal and vertical variant. I'm not sure if that's the optimal way to do it, but I didn't see a good alternative. The `Point` type would technically work, but it may cause confusion. --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- assets/settings/default.json | 11 +- crates/editor/src/editor_settings.rs | 37 ++ crates/editor/src/element.rs | 715 ++++++++++++++++++++------- crates/editor/src/scroll.rs | 79 ++- crates/search/src/project_search.rs | 16 +- docs/src/configuring-zed.md | 41 +- 6 files changed, 696 insertions(+), 203 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 1acae2f9e7..59156fe9cc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -254,7 +254,14 @@ // Whether to show selected symbol occurrences in the scrollbar. "selected_symbol": true, // Whether to show diagnostic indicators in the scrollbar. - "diagnostics": true + "diagnostics": true, + /// Forcefully enable or disable the scrollbar for each axis + "axes": { + /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. + "horizontal": true, + /// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. + "vertical": true + } }, // Enable middle-click paste on Linux. "middle_click_paste": true, @@ -304,6 +311,8 @@ "vertical_scroll_margin": 3, // Whether to scroll when clicking near the edge of the visible text area. "autoscroll_on_clicks": false, + // The number of characters to keep on either side when scrolling with the mouse + "horizontal_scroll_margin": 5, // Scroll sensitivity multiplier. This multiplier is applied // to both the horizontal and vertical delta values while scrolling. "scroll_sensitivity": 1.0, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index bc348543a8..0908c35262 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -18,6 +18,7 @@ pub struct EditorSettings { pub scroll_beyond_last_line: ScrollBeyondLastLine, pub vertical_scroll_margin: f32, pub autoscroll_on_clicks: bool, + pub horizontal_scroll_margin: f32, pub scroll_sensitivity: f32, pub relative_line_numbers: bool, pub seed_search_query_from_cursor: SeedQuerySetting, @@ -105,6 +106,7 @@ pub struct Scrollbar { pub search_results: bool, pub diagnostics: bool, pub cursors: bool, + pub axes: ScrollbarAxes, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -132,6 +134,21 @@ pub enum ShowScrollbar { Never, } +/// Forcefully enable or disable the scrollbar for each axis +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub struct ScrollbarAxes { + /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. + /// + /// Default: true + pub horizontal: bool, + + /// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. + /// + /// Default: true + pub vertical: bool, +} + /// The key to use for adding multiple cursors /// /// Default: alt @@ -219,6 +236,10 @@ pub struct EditorSettingsContent { /// /// Default: false pub autoscroll_on_clicks: Option, + /// The number of characters to keep on either side when scrolling with the mouse. + /// + /// Default: 5. + pub horizontal_scroll_margin: Option, /// Scroll sensitivity multiplier. This multiplier is applied /// to both the horizontal and vertical delta values while scrolling. /// @@ -328,6 +349,22 @@ pub struct ScrollbarContent { /// /// Default: true pub cursors: Option, + /// Forcefully enable or disable the scrollbar for each axis + pub axes: Option, +} + +/// Forcefully enable or disable the scrollbar for each axis +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct ScrollbarAxesContent { + /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. + /// + /// Default: true + horizontal: Option, + + /// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. + /// + /// Default: true + vertical: Option, } /// Gutter related settings diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ef6580089a..b75338c718 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -16,7 +16,7 @@ use crate::{ hunk_status, items::BufferSearchHighlights, mouse_context_menu::{self, MenuPosition, MouseContextMenu}, - scroll::scroll_amount::ScrollAmount, + scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair}, BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, @@ -31,7 +31,7 @@ use file_icons::FileIcons; use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; use gpui::{ anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg, - transparent_black, Action, AnyElement, AvailableSpace, Bounds, ClickEvent, ClipboardItem, + transparent_black, Action, AnyElement, AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, @@ -154,7 +154,7 @@ pub struct EditorElement { type DisplayRowDelta = u32; impl EditorElement { - pub(crate) const SCROLLBAR_WIDTH: Pixels = px(13.); + pub(crate) const SCROLLBAR_WIDTH: Pixels = px(15.); pub fn new(editor: &View, style: EditorStyle) -> Self { Self { @@ -714,9 +714,24 @@ impl EditorElement { scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); } - let horizontal_margin = position_map.line_height.min(text_bounds.size.width / 3.0); - let left = text_bounds.origin.x + horizontal_margin; - let right = text_bounds.top_right().x - horizontal_margin; + // We need horizontal width of text + let style = editor.style.clone().unwrap_or_default(); + let font_id = cx.text_system().resolve_font(&style.text.font()); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + + let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin; + + let scroll_space: Pixels = scroll_margin_x * em_width; + + let left = text_bounds.origin.x + scroll_space; + let right = text_bounds.top_right().x - scroll_space; + if event.position.x < left { scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); } @@ -1161,15 +1176,20 @@ impl EditorElement { cursor_layouts } - fn layout_scrollbar( + fn layout_scrollbars( &self, snapshot: &EditorSnapshot, - bounds: Bounds, + scrollbar_range_data: ScrollbarRangeData, scroll_position: gpui::Point, - rows_per_page: f32, non_visible_cursors: bool, cx: &mut WindowContext, - ) -> Option { + ) -> AxisPair> { + let letter_size = scrollbar_range_data.letter_size; + let text_units_per_page = axis_pair( + scrollbar_range_data.scrollbar_bounds.size.width / letter_size.width, + scrollbar_range_data.scrollbar_bounds.size.height / letter_size.height, + ); + let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; let show_scrollbars = match scrollbar_settings.show { ShowScrollbar::Auto => { @@ -1197,45 +1217,139 @@ impl EditorElement { ShowScrollbar::Always => true, ShowScrollbar::Never => false, }; + + let axes: AxisPair = scrollbar_settings.axes.into(); + if snapshot.mode != EditorMode::Full { - return None; + return axis_pair(None, None); } - let visible_row_range = scroll_position.y..scroll_position.y + rows_per_page; + let visible_range = axis_pair( + axes.horizontal + .then(|| scroll_position.x..scroll_position.x + text_units_per_page.horizontal), + axes.vertical + .then(|| scroll_position.y..scroll_position.y + text_units_per_page.vertical), + ); // If a drag took place after we started dragging the scrollbar, // cancel the scrollbar drag. if cx.has_active_drag() { self.editor.update(cx, |editor, cx| { - editor.scroll_manager.set_is_dragging_scrollbar(false, cx); + editor + .scroll_manager + .set_is_dragging_scrollbar(Axis::Horizontal, false, cx); + editor + .scroll_manager + .set_is_dragging_scrollbar(Axis::Vertical, false, cx); }); } - let track_bounds = Bounds::from_corners( - point(self.scrollbar_left(&bounds), bounds.origin.y), - point(bounds.bottom_right().x, bounds.bottom_left().y), + let text_bounds = scrollbar_range_data.scrollbar_bounds; + + let track_bounds = axis_pair( + axes.horizontal.then(|| { + Bounds::from_corners( + point( + text_bounds.bottom_left().x, + text_bounds.bottom_left().y - self.style.scrollbar_width, + ), + point( + text_bounds.bottom_right().x + - if axes.vertical { + self.style.scrollbar_width + } else { + px(0.) + }, + text_bounds.bottom_right().y, + ), + ) + }), + axes.vertical.then(|| { + Bounds::from_corners( + point(self.scrollbar_left(&text_bounds), text_bounds.origin.y), + text_bounds.bottom_right(), + ) + }), ); - let settings = EditorSettings::get_global(cx); - let scroll_beyond_last_line: f32 = match settings.scroll_beyond_last_line { - ScrollBeyondLastLine::OnePage => rows_per_page, - ScrollBeyondLastLine::Off => 1.0, - ScrollBeyondLastLine::VerticalScrollMargin => 1.0 + settings.vertical_scroll_margin, - }; - let total_rows = - (snapshot.max_point().row().as_f32() + scroll_beyond_last_line).max(rows_per_page); - let height = bounds.size.height; - let px_per_row = height / total_rows; - let thumb_height = (rows_per_page * px_per_row).max(ScrollbarLayout::MIN_THUMB_HEIGHT); - let row_height = (height - thumb_height) / (total_rows - rows_per_page).max(0.); + let scroll_range_size = scrollbar_range_data.scroll_range.size; + let total_text_units = axis_pair( + Some(scroll_range_size.width / letter_size.width), + Some(scroll_range_size.height / letter_size.height), + ); - Some(ScrollbarLayout { - hitbox: cx.insert_hitbox(track_bounds, false), - visible_row_range, - row_height, - visible: show_scrollbars, - thumb_height, - }) + let thumb_size = axis_pair( + total_text_units + .horizontal + .zip(track_bounds.horizontal) + .map(|(total_text_units_x, track_bounds_x)| { + let thumb_percent = + (text_units_per_page.horizontal / total_text_units_x).min(1.); + + track_bounds_x.size.width * thumb_percent + }), + total_text_units.vertical.zip(track_bounds.vertical).map( + |(total_text_units_y, track_bounds_y)| { + let thumb_percent = (text_units_per_page.vertical / total_text_units_y).min(1.); + + track_bounds_y.size.height * thumb_percent + }, + ), + ); + + // NOTE: Space not taken by track bounds divided by text units not on screen + let text_unit_size = axis_pair( + thumb_size + .horizontal + .zip(track_bounds.horizontal) + .zip(total_text_units.horizontal) + .map(|((thumb_size, track_bounds), total_text_units)| { + (track_bounds.size.width - thumb_size) + / (total_text_units - text_units_per_page.horizontal).max(0.) + }), + thumb_size + .vertical + .zip(track_bounds.vertical) + .zip(total_text_units.vertical) + .map(|((thumb_size, track_bounds), total_text_units)| { + (track_bounds.size.height - thumb_size) + / (total_text_units - text_units_per_page.vertical).max(0.) + }), + ); + + let horizontal_scrollbar = track_bounds + .horizontal + .zip(visible_range.horizontal) + .zip(text_unit_size.horizontal) + .zip(thumb_size.horizontal) + .map( + |(((track_bounds, visible_range), text_unit_size), thumb_size)| ScrollbarLayout { + hitbox: cx.insert_hitbox(track_bounds, false), + visible_range, + text_unit_size, + visible: show_scrollbars, + thumb_size, + axis: Axis::Horizontal, + }, + ); + + let vertical_scrollbar = track_bounds + .vertical + .zip(visible_range.vertical) + .zip(text_unit_size.vertical) + .zip(thumb_size.vertical) + .map( + |(((track_bounds, visible_range), text_unit_size), thumb_size)| ScrollbarLayout { + hitbox: cx.insert_hitbox(track_bounds, false), + visible_range, + text_unit_size, + visible: show_scrollbars, + thumb_size, + axis: Axis::Vertical, + }, + ); + + axis_pair(horizontal_scrollbar, vertical_scrollbar) } #[allow(clippy::too_many_arguments)] @@ -3419,10 +3533,13 @@ impl EditorElement { + layout.position_map.em_width / 2.) - scroll_left; - let show_scrollbars = layout - .scrollbar_layout - .as_ref() - .map_or(false, |scrollbar| scrollbar.visible); + let show_scrollbars = { + let (scrollbar_x, scrollbar_y) = &layout.scrollbars_layout.as_xy(); + + scrollbar_x.as_ref().map_or(false, |sx| sx.visible) + || scrollbar_y.as_ref().map_or(false, |sy| sy.visible) + }; + if x < layout.text_hitbox.origin.x || (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds)) { @@ -3903,137 +4020,306 @@ impl EditorElement { } } - fn paint_scrollbar(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { - let Some(scrollbar_layout) = layout.scrollbar_layout.as_ref() else { - return; - }; + fn paint_scrollbars(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { + let (scrollbar_x, scrollbar_y) = layout.scrollbars_layout.as_xy(); - 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: ScrollbarLayout::BORDER_WIDTH, - }, - cx.theme().colors().scrollbar_track_border, - )); - - let fast_markers = - self.collect_fast_scrollbar_markers(layout, scrollbar_layout, cx); - // Refresh slow scrollbar markers in the background. Below, we paint whatever markers have already been computed. - self.refresh_slow_scrollbar_markers(layout, scrollbar_layout, cx); - - let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone(); - for marker in markers.iter().chain(&fast_markers) { - let mut marker = marker.clone(); - marker.bounds.origin += scrollbar_layout.hitbox.origin; - cx.paint_quad(marker); - } - - cx.paint_quad(quad( - thumb_bounds, - Corners::default(), - cx.theme().colors().scrollbar_thumb_background, - Edges { - top: Pixels::ZERO, - right: Pixels::ZERO, - bottom: Pixels::ZERO, - left: ScrollbarLayout::BORDER_WIDTH, - }, - cx.theme().colors().scrollbar_thumb_border, - )); - }); - } - - cx.set_cursor_style(CursorStyle::Arrow, &scrollbar_layout.hitbox); - - let row_height = scrollbar_layout.row_height; - let row_range = scrollbar_layout.visible_row_range.clone(); - - cx.on_mouse_event({ - let editor = self.editor.clone(); + if let Some(scrollbar_layout) = scrollbar_x { let hitbox = scrollbar_layout.hitbox.clone(); - let mut mouse_position = cx.mouse_position(); - move |event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture { - return; - } + let text_unit_size = scrollbar_layout.text_unit_size; + let visible_range = scrollbar_layout.visible_range.clone(); + let thumb_bounds = scrollbar_layout.thumb_bounds(); - 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) / row_height; - if position.y < 0.0 { - position.y = 0.0; - } - editor.set_scroll_position(position, cx); - } + if scrollbar_layout.visible { + cx.paint_layer(hitbox.bounds, |cx| { + cx.paint_quad(quad( + hitbox.bounds, + Corners::default(), + cx.theme().colors().scrollbar_track_background, + Edges { + top: Pixels::ZERO, + right: Pixels::ZERO, + bottom: Pixels::ZERO, + left: Pixels::ZERO, + }, + cx.theme().colors().scrollbar_track_border, + )); - cx.stop_propagation(); - } else { - editor.scroll_manager.set_is_dragging_scrollbar(false, cx); - if hitbox.is_hovered(cx) { - editor.scroll_manager.show_scrollbar(cx); - } - } - mouse_position = event.position; + cx.paint_quad(quad( + thumb_bounds, + Corners::default(), + cx.theme().colors().scrollbar_thumb_background, + Edges { + top: Pixels::ZERO, + right: Pixels::ZERO, + bottom: Pixels::ZERO, + left: ScrollbarLayout::BORDER_WIDTH, + }, + cx.theme().colors().scrollbar_thumb_border, + )); }) } - }); - if self.editor.read(cx).scroll_manager.is_dragging_scrollbar() { + cx.set_cursor_style(CursorStyle::Arrow, &hitbox); + cx.on_mouse_event({ let editor = self.editor.clone(); - move |_: &MouseUpEvent, phase, cx| { + + // there may be a way to avoid this clone + let hitbox = hitbox.clone(); + + let mut mouse_position = cx.mouse_position(); + move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { return; } editor.update(cx, |editor, cx| { - editor.scroll_manager.set_is_dragging_scrollbar(false, cx); - cx.stop_propagation(); - }); + if event.pressed_button == Some(MouseButton::Left) + && editor + .scroll_manager + .is_dragging_scrollbar(Axis::Horizontal) + { + let x = mouse_position.x; + let new_x = event.position.x; + if (hitbox.left()..hitbox.right()).contains(&x) { + let mut position = editor.scroll_position(cx); + + position.x += (new_x - x) / text_unit_size; + if position.x < 0.0 { + position.x = 0.0; + } + editor.set_scroll_position(position, cx); + } + + cx.stop_propagation(); + } else { + editor.scroll_manager.set_is_dragging_scrollbar( + Axis::Horizontal, + false, + cx, + ); + + if hitbox.is_hovered(cx) { + editor.scroll_manager.show_scrollbar(cx); + } + } + mouse_position = event.position; + }) } }); - } else { + + if self + .editor + .read(cx) + .scroll_manager + .is_dragging_scrollbar(Axis::Horizontal) + { + 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( + Axis::Horizontal, + false, + cx, + ); + cx.stop_propagation(); + }); + } + }); + } else { + cx.on_mouse_event({ + let editor = self.editor.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( + Axis::Horizontal, + true, + cx, + ); + + let x = event.position.x; + + if x < thumb_bounds.left() || thumb_bounds.right() < x { + let center_row = + ((x - hitbox.left()) / text_unit_size).round() as u32; + let top_row = center_row.saturating_sub( + (visible_range.end - visible_range.start) as u32 / 2, + ); + + let mut position = editor.scroll_position(cx); + position.x = top_row as f32; + + editor.set_scroll_position(position, cx); + } else { + editor.scroll_manager.show_scrollbar(cx); + } + + cx.stop_propagation(); + }); + } + }); + } + } + + if let Some(scrollbar_layout) = scrollbar_y { + let hitbox = scrollbar_layout.hitbox.clone(); + let text_unit_size = scrollbar_layout.text_unit_size; + let visible_range = scrollbar_layout.visible_range.clone(); + let thumb_bounds = scrollbar_layout.thumb_bounds(); + + if scrollbar_layout.visible { + cx.paint_layer(hitbox.bounds, |cx| { + cx.paint_quad(quad( + hitbox.bounds, + Corners::default(), + cx.theme().colors().scrollbar_track_background, + Edges { + top: Pixels::ZERO, + right: Pixels::ZERO, + bottom: Pixels::ZERO, + left: ScrollbarLayout::BORDER_WIDTH, + }, + cx.theme().colors().scrollbar_track_border, + )); + + let fast_markers = + self.collect_fast_scrollbar_markers(layout, &scrollbar_layout, cx); + // Refresh slow scrollbar markers in the background. Below, we paint whatever markers have already been computed. + self.refresh_slow_scrollbar_markers(layout, &scrollbar_layout, cx); + + let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone(); + for marker in markers.iter().chain(&fast_markers) { + let mut marker = marker.clone(); + marker.bounds.origin += hitbox.origin; + cx.paint_quad(marker); + } + + cx.paint_quad(quad( + thumb_bounds, + Corners::default(), + cx.theme().colors().scrollbar_thumb_background, + Edges { + top: Pixels::ZERO, + right: Pixels::ZERO, + bottom: Pixels::ZERO, + left: ScrollbarLayout::BORDER_WIDTH, + }, + cx.theme().colors().scrollbar_thumb_border, + )); + }); + } + + cx.set_cursor_style(CursorStyle::Arrow, &hitbox); + 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) { + + let hitbox = hitbox.clone(); + + let mut mouse_position = cx.mouse_position(); + move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { 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()) / 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); + if event.pressed_button == Some(MouseButton::Left) + && editor.scroll_manager.is_dragging_scrollbar(Axis::Vertical) + { + 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) / text_unit_size; + if position.y < 0.0 { + position.y = 0.0; + } + editor.set_scroll_position(position, cx); + } } else { - editor.scroll_manager.show_scrollbar(cx); - } + editor.scroll_manager.set_is_dragging_scrollbar( + Axis::Vertical, + false, + cx, + ); - cx.stop_propagation(); - }); + if hitbox.is_hovered(cx) { + editor.scroll_manager.show_scrollbar(cx); + } + } + mouse_position = event.position; + }) } }); + + if self + .editor + .read(cx) + .scroll_manager + .is_dragging_scrollbar(Axis::Vertical) + { + 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( + Axis::Vertical, + false, + cx, + ); + cx.stop_propagation(); + }); + } + }); + } else { + cx.on_mouse_event({ + let editor = self.editor.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( + Axis::Vertical, + true, + cx, + ); + + let y = event.position.y; + if y < thumb_bounds.top() || thumb_bounds.bottom() < y { + let center_row = + ((y - hitbox.top()) / text_unit_size).round() as u32; + let top_row = center_row.saturating_sub( + (visible_range.end - visible_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(); + }); + } + }); + } } } @@ -5423,6 +5709,8 @@ impl Element for EditorElement { .unwrap() .width; + let letter_size = size(em_width, line_height); + let gutter_dimensions = snapshot.gutter_dimensions( font_id, font_size, @@ -5433,15 +5721,7 @@ impl Element for EditorElement { ); let text_width = bounds.size.width - gutter_dimensions.width; - let right_margin = if snapshot.mode == EditorMode::Full { - EditorElement::SCROLLBAR_WIDTH - } else { - px(0.) - }; - let overscroll = size(em_width + right_margin, px(0.)); - - let editor_width = - text_width - gutter_dimensions.margin - overscroll.width - em_width; + let editor_width = text_width - gutter_dimensions.margin - em_width; snapshot = self.editor.update(cx, |editor, cx| { editor.last_bounds = Some(bounds); @@ -5492,8 +5772,15 @@ impl Element for EditorElement { let content_origin = text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO); - let height_in_lines = bounds.size.height / line_height; + let scrollbar_bounds = + Bounds::from_corners(content_origin, bounds.bottom_right()); + + let height_in_lines = scrollbar_bounds.size.height / line_height; + + // NOTE: The max row number in the current file, minus one let max_row = snapshot.max_point().row().as_f32(); + + // NOTE: The max scroll position for the top of the window let max_scroll_top = if matches!(snapshot.mode, EditorMode::AutoHeight { .. }) { (max_row - height_in_lines + 1.).max(0.) } else { @@ -5508,6 +5795,7 @@ impl Element for EditorElement { } }; + // TODO: Autoscrolling for both axes let mut autoscroll_request = None; let mut autoscroll_containing_element = false; let mut autoscroll_horizontally = false; @@ -5515,6 +5803,7 @@ impl Element for EditorElement { autoscroll_request = editor.autoscroll_request(); autoscroll_containing_element = autoscroll_request.is_some() || editor.has_pending_selection(); + // TODO: Is this horizontal or vertical?! autoscroll_horizontally = editor.autoscroll_vertically(bounds, line_height, max_scroll_top, cx); snapshot = editor.snapshot(cx); @@ -5648,8 +5937,18 @@ impl Element for EditorElement { cx, ) .width; - let mut scroll_width = - longest_line_width.max(max_visible_line_width) + overscroll.width; + + let scrollbar_range_data = ScrollbarRangeData::new( + scrollbar_bounds, + letter_size, + &snapshot, + longest_line_width, + &style, + cx, + ); + + let scroll_range_bounds = scrollbar_range_data.scroll_range; + let mut scroll_width = scroll_range_bounds.size.width; let blocks = cx.with_element_namespace("blocks", |cx| { self.render_blocks( @@ -5685,7 +5984,7 @@ impl Element for EditorElement { MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); let scroll_max = point( - ((scroll_width - text_hitbox.size.width) / em_width).max(0.0), + ((scroll_width - scrollbar_bounds.size.width) / em_width).max(0.0), max_row.as_f32(), ); @@ -5770,7 +6069,7 @@ impl Element for EditorElement { ); let scroll_max = point( - ((scroll_width - text_hitbox.size.width) / em_width).max(0.0), + ((scroll_width - scrollbar_bounds.size.width) / em_width).max(0.0), max_scroll_top, ); @@ -5839,11 +6138,10 @@ impl Element for EditorElement { cx, ); - let scrollbar_layout = self.layout_scrollbar( + let scrollbars_layout = self.layout_scrollbars( &snapshot, - bounds, + scrollbar_range_data, scroll_position, - height_in_lines, non_visible_cursors, cx, ); @@ -6075,7 +6373,7 @@ impl Element for EditorElement { gutter_dimensions, display_hunks, content_origin, - scrollbar_layout, + scrollbars_layout, active_rows, highlighted_rows, highlighted_ranges, @@ -6178,7 +6476,7 @@ impl Element for EditorElement { }); } - self.paint_scrollbar(layout, cx); + self.paint_scrollbars(layout, cx); self.paint_inline_completion_popover(layout, cx); self.paint_mouse_context_menu(layout, cx); }); @@ -6197,6 +6495,52 @@ pub(super) fn gutter_bounds( } } +struct ScrollbarRangeData { + scrollbar_bounds: Bounds, + scroll_range: Bounds, + letter_size: Size, +} + +impl ScrollbarRangeData { + pub fn new( + scrollbar_bounds: Bounds, + letter_size: Size, + snapshot: &EditorSnapshot, + longest_line_width: Pixels, + style: &EditorStyle, + cx: &WindowContext, + ) -> ScrollbarRangeData { + // TODO: Simplify this function down, it requires a lot of parameters + let max_row = snapshot.max_point().row(); + let text_bounds_size = size(longest_line_width, max_row.0 as f32 * letter_size.height); + + let scrollbar_width = style.scrollbar_width; + + let settings = EditorSettings::get_global(cx); + let scroll_beyond_last_line: Pixels = match settings.scroll_beyond_last_line { + ScrollBeyondLastLine::OnePage => px(scrollbar_bounds.size.height / letter_size.height), + ScrollBeyondLastLine::Off => px(1.), + ScrollBeyondLastLine::VerticalScrollMargin => px(1.0 + settings.vertical_scroll_margin), + }; + + let overscroll = size( + scrollbar_width + (letter_size.width / 2.0), + letter_size.height * scroll_beyond_last_line, + ); + + let scroll_range = Bounds { + origin: scrollbar_bounds.origin, + size: text_bounds_size + overscroll, + }; + + ScrollbarRangeData { + scrollbar_bounds, + scroll_range, + letter_size, + } + } +} + impl IntoElement for EditorElement { type Element = Self; @@ -6212,7 +6556,7 @@ pub struct EditorLayout { gutter_hitbox: Hitbox, gutter_dimensions: GutterDimensions, content_origin: gpui::Point, - scrollbar_layout: Option, + scrollbars_layout: AxisPair>, mode: EditorMode, wrap_guides: SmallVec<[(Pixels, bool); 2]>, indent_guides: Option>, @@ -6256,29 +6600,43 @@ struct ColoredRange { #[derive(Clone)] struct ScrollbarLayout { hitbox: Hitbox, - visible_row_range: Range, + visible_range: Range, visible: bool, - row_height: Pixels, - thumb_height: Pixels, + text_unit_size: Pixels, + thumb_size: Pixels, + axis: Axis, } impl ScrollbarLayout { const BORDER_WIDTH: Pixels = px(1.0); const LINE_MARKER_HEIGHT: Pixels = px(2.0); const MIN_MARKER_HEIGHT: Pixels = px(5.0); - const MIN_THUMB_HEIGHT: Pixels = px(20.0); + // const MIN_THUMB_HEIGHT: Pixels = px(20.0); fn thumb_bounds(&self) -> Bounds { - let thumb_top = self.y_for_row(self.visible_row_range.start); - let thumb_bottom = thumb_top + self.thumb_height; - Bounds::from_corners( - point(self.hitbox.left(), thumb_top), - point(self.hitbox.right(), thumb_bottom), - ) + match self.axis { + Axis::Vertical => { + let thumb_top = self.y_for_row(self.visible_range.start); + let thumb_bottom = thumb_top + self.thumb_size; + Bounds::from_corners( + point(self.hitbox.left(), thumb_top), + point(self.hitbox.right(), thumb_bottom), + ) + } + Axis::Horizontal => { + let thumb_left = + self.hitbox.left() + self.visible_range.start * self.text_unit_size; + let thumb_right = thumb_left + self.thumb_size; + Bounds::from_corners( + point(thumb_left, self.hitbox.top()), + point(thumb_right, self.hitbox.bottom()), + ) + } + } } fn y_for_row(&self, row: f32) -> Pixels { - self.hitbox.top() + row * self.row_height + self.hitbox.top() + row * self.text_unit_size } fn marker_quads_for_ranges( @@ -6314,13 +6672,16 @@ impl ScrollbarLayout { ) }; - let row_to_y = |row: DisplayRow| row.as_f32() * self.row_height; + let row_to_y = |row: DisplayRow| row.as_f32() * self.text_unit_size; let mut pixel_ranges = row_ranges .into_iter() .map(|range| { let start_y = row_to_y(range.start); let end_y = row_to_y(range.end) - + self.row_height.max(height_limit.min).min(height_limit.max); + + self + .text_unit_size + .max(height_limit.min) + .min(height_limit.max); ColoredRange { start: start_y, end: end_y, diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 97e0db5b37..1fae8099d4 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -2,7 +2,7 @@ mod actions; pub(crate) mod autoscroll; pub(crate) mod scroll_amount; -use crate::editor_settings::ScrollBeyondLastLine; +use crate::editor_settings::{ScrollBeyondLastLine, ScrollbarAxes}; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, hover_popover::hide_hover, @@ -11,7 +11,10 @@ use crate::{ InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint, }; pub use autoscroll::{Autoscroll, AutoscrollStrategy}; -use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext, WindowContext}; +use core::fmt::Debug; +use gpui::{ + point, px, Along, AppContext, Axis, Entity, Global, Pixels, Task, ViewContext, WindowContext, +}; use language::{Bias, Point}; pub use scroll_amount::ScrollAmount; use settings::Settings; @@ -60,10 +63,53 @@ impl ScrollAnchor { } } -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum Axis { - Vertical, - Horizontal, +#[derive(Debug, Clone)] +pub struct AxisPair { + pub vertical: T, + pub horizontal: T, +} + +pub fn axis_pair(horizontal: T, vertical: T) -> AxisPair { + AxisPair { + vertical, + horizontal, + } +} + +impl AxisPair { + pub fn as_xy(&self) -> (&T, &T) { + (&self.horizontal, &self.vertical) + } +} + +impl Along for AxisPair { + type Unit = T; + + fn along(&self, axis: gpui::Axis) -> Self::Unit { + match axis { + gpui::Axis::Horizontal => self.horizontal.clone(), + gpui::Axis::Vertical => self.vertical.clone(), + } + } + + fn apply_along(&self, axis: gpui::Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self { + match axis { + gpui::Axis::Horizontal => Self { + horizontal: f(self.horizontal.clone()), + vertical: self.vertical.clone(), + }, + gpui::Axis::Vertical => Self { + horizontal: self.horizontal.clone(), + vertical: f(self.vertical.clone()), + }, + } + } +} + +impl From for AxisPair { + fn from(value: ScrollbarAxes) -> Self { + axis_pair(value.horizontal, value.vertical) + } } #[derive(Clone, Copy, Debug)] @@ -136,7 +182,7 @@ pub struct ScrollManager { last_autoscroll: Option<(gpui::Point, f32, f32, AutoscrollStrategy)>, show_scrollbars: bool, hide_scrollbar_task: Option>, - dragging_scrollbar: bool, + dragging_scrollbar: AxisPair, visible_line_count: Option, forbid_vertical_scroll: bool, } @@ -150,7 +196,7 @@ impl ScrollManager { autoscroll_request: None, show_scrollbars: true, hide_scrollbar_task: None, - dragging_scrollbar: false, + dragging_scrollbar: axis_pair(false, false), last_autoscroll: None, visible_line_count: None, forbid_vertical_scroll: false, @@ -311,15 +357,18 @@ impl ScrollManager { self.autoscroll_request.map(|(autoscroll, _)| autoscroll) } - pub fn is_dragging_scrollbar(&self) -> bool { - self.dragging_scrollbar + pub fn is_dragging_scrollbar(&self, axis: Axis) -> bool { + self.dragging_scrollbar.along(axis) } - pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext) { - if dragging != self.dragging_scrollbar { - self.dragging_scrollbar = dragging; - cx.notify(); - } + pub fn set_is_dragging_scrollbar( + &mut self, + axis: Axis, + dragging: bool, + cx: &mut ViewContext, + ) { + self.dragging_scrollbar = self.dragging_scrollbar.apply_along(axis, |_| dragging); + cx.notify(); } pub fn clamp_scroll_left(&mut self, max: f32) -> bool { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b8eee4fe53..13af840854 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -5,18 +5,16 @@ use crate::{ }; use collections::{HashMap, HashSet}; use editor::{ - actions::SelectAll, - items::active_match_index, - scroll::{Autoscroll, Axis}, - Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer, - MAX_TAB_TITLE_LEN, + actions::SelectAll, items::active_match_index, scroll::Autoscroll, Anchor, Editor, + EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer, MAX_TAB_TITLE_LEN, }; use futures::StreamExt; use gpui::{ - actions, div, Action, AnyElement, AnyView, AppContext, Context as _, EntityId, EventEmitter, - FocusHandle, FocusableView, Global, Hsla, InteractiveElement, IntoElement, KeyContext, Model, - ModelContext, ParentElement, Point, Render, SharedString, Styled, Subscription, Task, - TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, + actions, div, Action, AnyElement, AnyView, AppContext, Axis, Context as _, EntityId, + EventEmitter, FocusHandle, FocusableView, Global, Hsla, InteractiveElement, IntoElement, + KeyContext, Model, ModelContext, ParentElement, Point, Render, SharedString, Styled, + Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel, + WeakView, WindowContext, }; use language::Buffer; use menu::Confirm; diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 3aefae4c1f..08ebe4dc3c 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -534,7 +534,11 @@ List of `string` values "git_diff": true, "search_results": true, "selected_symbol": true, - "diagnostics": true + "diagnostics": true, + "axes": { + "horizontal": true, + "vertical": true, + }, }, ``` @@ -628,6 +632,41 @@ List of `string` values `boolean` values +### Axes + +- Description: Forcefully enable or disable the scrollbar for each axis +- Setting: `axes` +- Default: + +```json +"scrollbar": { + "axes": { + "horizontal": true, + "vertical": true, + }, +} +``` + +#### Horizontal + +- Description: When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. +- Setting: `horizontal` +- Default: `true` + +**Options** + +`boolean` values + +#### Vertical + +- Description: When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. +- Setting: `vertical` +- Default: `true` + +**Options** + +`boolean` values + ## Editor Tab Bar - Description: Settings related to the editor's tab bar.