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>
This commit is contained in:
Carlos Kieliszewski 2024-12-17 11:24:59 -05:00 committed by GitHub
parent 6fa5a17586
commit ed3e647ed7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 696 additions and 203 deletions

View file

@ -254,7 +254,14 @@
// Whether to show selected symbol occurrences in the scrollbar. // Whether to show selected symbol occurrences in the scrollbar.
"selected_symbol": true, "selected_symbol": true,
// Whether to show diagnostic indicators in the scrollbar. // 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. // Enable middle-click paste on Linux.
"middle_click_paste": true, "middle_click_paste": true,
@ -304,6 +311,8 @@
"vertical_scroll_margin": 3, "vertical_scroll_margin": 3,
// Whether to scroll when clicking near the edge of the visible text area. // Whether to scroll when clicking near the edge of the visible text area.
"autoscroll_on_clicks": false, "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 // Scroll sensitivity multiplier. This multiplier is applied
// to both the horizontal and vertical delta values while scrolling. // to both the horizontal and vertical delta values while scrolling.
"scroll_sensitivity": 1.0, "scroll_sensitivity": 1.0,

View file

@ -18,6 +18,7 @@ pub struct EditorSettings {
pub scroll_beyond_last_line: ScrollBeyondLastLine, pub scroll_beyond_last_line: ScrollBeyondLastLine,
pub vertical_scroll_margin: f32, pub vertical_scroll_margin: f32,
pub autoscroll_on_clicks: bool, pub autoscroll_on_clicks: bool,
pub horizontal_scroll_margin: f32,
pub scroll_sensitivity: f32, pub scroll_sensitivity: f32,
pub relative_line_numbers: bool, pub relative_line_numbers: bool,
pub seed_search_query_from_cursor: SeedQuerySetting, pub seed_search_query_from_cursor: SeedQuerySetting,
@ -105,6 +106,7 @@ pub struct Scrollbar {
pub search_results: bool, pub search_results: bool,
pub diagnostics: bool, pub diagnostics: bool,
pub cursors: bool, pub cursors: bool,
pub axes: ScrollbarAxes,
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@ -132,6 +134,21 @@ pub enum ShowScrollbar {
Never, 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 /// The key to use for adding multiple cursors
/// ///
/// Default: alt /// Default: alt
@ -219,6 +236,10 @@ pub struct EditorSettingsContent {
/// ///
/// Default: false /// Default: false
pub autoscroll_on_clicks: Option<bool>, pub autoscroll_on_clicks: Option<bool>,
/// The number of characters to keep on either side when scrolling with the mouse.
///
/// Default: 5.
pub horizontal_scroll_margin: Option<f32>,
/// Scroll sensitivity multiplier. This multiplier is applied /// Scroll sensitivity multiplier. This multiplier is applied
/// to both the horizontal and vertical delta values while scrolling. /// to both the horizontal and vertical delta values while scrolling.
/// ///
@ -328,6 +349,22 @@ pub struct ScrollbarContent {
/// ///
/// Default: true /// Default: true
pub cursors: Option<bool>, pub cursors: Option<bool>,
/// Forcefully enable or disable the scrollbar for each axis
pub axes: Option<ScrollbarAxesContent>,
}
/// 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<bool>,
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
///
/// Default: true
vertical: Option<bool>,
} }
/// Gutter related settings /// Gutter related settings

View file

@ -16,7 +16,7 @@ use crate::{
hunk_status, hunk_status,
items::BufferSearchHighlights, items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu}, mouse_context_menu::{self, MenuPosition, MouseContextMenu},
scroll::scroll_amount::ScrollAmount, scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow, BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
@ -31,7 +31,7 @@ use file_icons::FileIcons;
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
use gpui::{ use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg, 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, ContentMask, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler,
Entity, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length, Entity, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
@ -154,7 +154,7 @@ pub struct EditorElement {
type DisplayRowDelta = u32; type DisplayRowDelta = u32;
impl EditorElement { impl EditorElement {
pub(crate) const SCROLLBAR_WIDTH: Pixels = px(13.); pub(crate) const SCROLLBAR_WIDTH: Pixels = px(15.);
pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self { pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self {
Self { Self {
@ -714,9 +714,24 @@ impl EditorElement {
scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); 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); // We need horizontal width of text
let left = text_bounds.origin.x + horizontal_margin; let style = editor.style.clone().unwrap_or_default();
let right = text_bounds.top_right().x - horizontal_margin; 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 { if event.position.x < left {
scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);
} }
@ -1161,15 +1176,20 @@ impl EditorElement {
cursor_layouts cursor_layouts
} }
fn layout_scrollbar( fn layout_scrollbars(
&self, &self,
snapshot: &EditorSnapshot, snapshot: &EditorSnapshot,
bounds: Bounds<Pixels>, scrollbar_range_data: ScrollbarRangeData,
scroll_position: gpui::Point<f32>, scroll_position: gpui::Point<f32>,
rows_per_page: f32,
non_visible_cursors: bool, non_visible_cursors: bool,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Option<ScrollbarLayout> { ) -> AxisPair<Option<ScrollbarLayout>> {
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 scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
let show_scrollbars = match scrollbar_settings.show { let show_scrollbars = match scrollbar_settings.show {
ShowScrollbar::Auto => { ShowScrollbar::Auto => {
@ -1197,45 +1217,139 @@ impl EditorElement {
ShowScrollbar::Always => true, ShowScrollbar::Always => true,
ShowScrollbar::Never => false, ShowScrollbar::Never => false,
}; };
let axes: AxisPair<bool> = scrollbar_settings.axes.into();
if snapshot.mode != EditorMode::Full { 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, // If a drag took place after we started dragging the scrollbar,
// cancel the scrollbar drag. // cancel the scrollbar drag.
if cx.has_active_drag() { if cx.has_active_drag() {
self.editor.update(cx, |editor, cx| { 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( let text_bounds = scrollbar_range_data.scrollbar_bounds;
point(self.scrollbar_left(&bounds), bounds.origin.y),
point(bounds.bottom_right().x, bounds.bottom_left().y), 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_range_size = scrollbar_range_data.scroll_range.size;
let scroll_beyond_last_line: f32 = match settings.scroll_beyond_last_line { let total_text_units = axis_pair(
ScrollBeyondLastLine::OnePage => rows_per_page, Some(scroll_range_size.width / letter_size.width),
ScrollBeyondLastLine::Off => 1.0, Some(scroll_range_size.height / letter_size.height),
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.);
Some(ScrollbarLayout { let thumb_size = axis_pair(
hitbox: cx.insert_hitbox(track_bounds, false), total_text_units
visible_row_range, .horizontal
row_height, .zip(track_bounds.horizontal)
visible: show_scrollbars, .map(|(total_text_units_x, track_bounds_x)| {
thumb_height, 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)] #[allow(clippy::too_many_arguments)]
@ -3419,10 +3533,13 @@ impl EditorElement {
+ layout.position_map.em_width / 2.) + layout.position_map.em_width / 2.)
- scroll_left; - scroll_left;
let show_scrollbars = layout let show_scrollbars = {
.scrollbar_layout let (scrollbar_x, scrollbar_y) = &layout.scrollbars_layout.as_xy();
.as_ref()
.map_or(false, |scrollbar| scrollbar.visible); 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 if x < layout.text_hitbox.origin.x
|| (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds)) || (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) { fn paint_scrollbars(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
let Some(scrollbar_layout) = layout.scrollbar_layout.as_ref() else { let (scrollbar_x, scrollbar_y) = layout.scrollbars_layout.as_xy();
return;
};
let thumb_bounds = scrollbar_layout.thumb_bounds(); if let Some(scrollbar_layout) = scrollbar_x {
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();
let hitbox = scrollbar_layout.hitbox.clone(); let hitbox = scrollbar_layout.hitbox.clone();
let mut mouse_position = cx.mouse_position(); let text_unit_size = scrollbar_layout.text_unit_size;
move |event: &MouseMoveEvent, phase, cx| { let visible_range = scrollbar_layout.visible_range.clone();
if phase == DispatchPhase::Capture { let thumb_bounds = scrollbar_layout.thumb_bounds();
return;
}
editor.update(cx, |editor, cx| { if scrollbar_layout.visible {
if event.pressed_button == Some(MouseButton::Left) cx.paint_layer(hitbox.bounds, |cx| {
&& editor.scroll_manager.is_dragging_scrollbar() cx.paint_quad(quad(
{ hitbox.bounds,
let y = mouse_position.y; Corners::default(),
let new_y = event.position.y; cx.theme().colors().scrollbar_track_background,
if (hitbox.top()..hitbox.bottom()).contains(&y) { Edges {
let mut position = editor.scroll_position(cx); top: Pixels::ZERO,
position.y += (new_y - y) / row_height; right: Pixels::ZERO,
if position.y < 0.0 { bottom: Pixels::ZERO,
position.y = 0.0; left: Pixels::ZERO,
} },
editor.set_scroll_position(position, cx); cx.theme().colors().scrollbar_track_border,
} ));
cx.stop_propagation(); cx.paint_quad(quad(
} else { thumb_bounds,
editor.scroll_manager.set_is_dragging_scrollbar(false, cx); Corners::default(),
if hitbox.is_hovered(cx) { cx.theme().colors().scrollbar_thumb_background,
editor.scroll_manager.show_scrollbar(cx); Edges {
} top: Pixels::ZERO,
} right: Pixels::ZERO,
mouse_position = event.position; 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({ cx.on_mouse_event({
let editor = self.editor.clone(); 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 { if phase == DispatchPhase::Capture {
return; return;
} }
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.scroll_manager.set_is_dragging_scrollbar(false, cx); if event.pressed_button == Some(MouseButton::Left)
cx.stop_propagation(); && 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({ cx.on_mouse_event({
let editor = self.editor.clone(); let editor = self.editor.clone();
let hitbox = scrollbar_layout.hitbox.clone();
move |event: &MouseDownEvent, phase, cx| { let hitbox = hitbox.clone();
if phase == DispatchPhase::Capture || !hitbox.is_hovered(cx) {
let mut mouse_position = cx.mouse_position();
move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture {
return; return;
} }
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.scroll_manager.set_is_dragging_scrollbar(true, cx); if event.pressed_button == Some(MouseButton::Left)
&& editor.scroll_manager.is_dragging_scrollbar(Axis::Vertical)
let y = event.position.y; {
if y < thumb_bounds.top() || thumb_bounds.bottom() < y { let y = mouse_position.y;
let center_row = ((y - hitbox.top()) / row_height).round() as u32; let new_y = event.position.y;
let top_row = center_row if (hitbox.top()..hitbox.bottom()).contains(&y) {
.saturating_sub((row_range.end - row_range.start) as u32 / 2); let mut position = editor.scroll_position(cx);
let mut position = editor.scroll_position(cx); position.y += (new_y - y) / text_unit_size;
position.y = top_row as f32; if position.y < 0.0 {
editor.set_scroll_position(position, cx); position.y = 0.0;
}
editor.set_scroll_position(position, cx);
}
} else { } 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() .unwrap()
.width; .width;
let letter_size = size(em_width, line_height);
let gutter_dimensions = snapshot.gutter_dimensions( let gutter_dimensions = snapshot.gutter_dimensions(
font_id, font_id,
font_size, font_size,
@ -5433,15 +5721,7 @@ impl Element for EditorElement {
); );
let text_width = bounds.size.width - gutter_dimensions.width; let text_width = bounds.size.width - gutter_dimensions.width;
let right_margin = if snapshot.mode == EditorMode::Full { let editor_width = text_width - gutter_dimensions.margin - em_width;
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;
snapshot = self.editor.update(cx, |editor, cx| { snapshot = self.editor.update(cx, |editor, cx| {
editor.last_bounds = Some(bounds); editor.last_bounds = Some(bounds);
@ -5492,8 +5772,15 @@ impl Element for EditorElement {
let content_origin = let content_origin =
text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO); 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(); 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 { .. }) { let max_scroll_top = if matches!(snapshot.mode, EditorMode::AutoHeight { .. }) {
(max_row - height_in_lines + 1.).max(0.) (max_row - height_in_lines + 1.).max(0.)
} else { } else {
@ -5508,6 +5795,7 @@ impl Element for EditorElement {
} }
}; };
// TODO: Autoscrolling for both axes
let mut autoscroll_request = None; let mut autoscroll_request = None;
let mut autoscroll_containing_element = false; let mut autoscroll_containing_element = false;
let mut autoscroll_horizontally = false; let mut autoscroll_horizontally = false;
@ -5515,6 +5803,7 @@ impl Element for EditorElement {
autoscroll_request = editor.autoscroll_request(); autoscroll_request = editor.autoscroll_request();
autoscroll_containing_element = autoscroll_containing_element =
autoscroll_request.is_some() || editor.has_pending_selection(); autoscroll_request.is_some() || editor.has_pending_selection();
// TODO: Is this horizontal or vertical?!
autoscroll_horizontally = autoscroll_horizontally =
editor.autoscroll_vertically(bounds, line_height, max_scroll_top, cx); editor.autoscroll_vertically(bounds, line_height, max_scroll_top, cx);
snapshot = editor.snapshot(cx); snapshot = editor.snapshot(cx);
@ -5648,8 +5937,18 @@ impl Element for EditorElement {
cx, cx,
) )
.width; .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| { let blocks = cx.with_element_namespace("blocks", |cx| {
self.render_blocks( self.render_blocks(
@ -5685,7 +5984,7 @@ impl Element for EditorElement {
MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
let scroll_max = point( 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(), max_row.as_f32(),
); );
@ -5770,7 +6069,7 @@ impl Element for EditorElement {
); );
let scroll_max = point( 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, max_scroll_top,
); );
@ -5839,11 +6138,10 @@ impl Element for EditorElement {
cx, cx,
); );
let scrollbar_layout = self.layout_scrollbar( let scrollbars_layout = self.layout_scrollbars(
&snapshot, &snapshot,
bounds, scrollbar_range_data,
scroll_position, scroll_position,
height_in_lines,
non_visible_cursors, non_visible_cursors,
cx, cx,
); );
@ -6075,7 +6373,7 @@ impl Element for EditorElement {
gutter_dimensions, gutter_dimensions,
display_hunks, display_hunks,
content_origin, content_origin,
scrollbar_layout, scrollbars_layout,
active_rows, active_rows,
highlighted_rows, highlighted_rows,
highlighted_ranges, 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_inline_completion_popover(layout, cx);
self.paint_mouse_context_menu(layout, cx); self.paint_mouse_context_menu(layout, cx);
}); });
@ -6197,6 +6495,52 @@ pub(super) fn gutter_bounds(
} }
} }
struct ScrollbarRangeData {
scrollbar_bounds: Bounds<Pixels>,
scroll_range: Bounds<Pixels>,
letter_size: Size<Pixels>,
}
impl ScrollbarRangeData {
pub fn new(
scrollbar_bounds: Bounds<Pixels>,
letter_size: Size<Pixels>,
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 { impl IntoElement for EditorElement {
type Element = Self; type Element = Self;
@ -6212,7 +6556,7 @@ pub struct EditorLayout {
gutter_hitbox: Hitbox, gutter_hitbox: Hitbox,
gutter_dimensions: GutterDimensions, gutter_dimensions: GutterDimensions,
content_origin: gpui::Point<Pixels>, content_origin: gpui::Point<Pixels>,
scrollbar_layout: Option<ScrollbarLayout>, scrollbars_layout: AxisPair<Option<ScrollbarLayout>>,
mode: EditorMode, mode: EditorMode,
wrap_guides: SmallVec<[(Pixels, bool); 2]>, wrap_guides: SmallVec<[(Pixels, bool); 2]>,
indent_guides: Option<Vec<IndentGuideLayout>>, indent_guides: Option<Vec<IndentGuideLayout>>,
@ -6256,29 +6600,43 @@ struct ColoredRange<T> {
#[derive(Clone)] #[derive(Clone)]
struct ScrollbarLayout { struct ScrollbarLayout {
hitbox: Hitbox, hitbox: Hitbox,
visible_row_range: Range<f32>, visible_range: Range<f32>,
visible: bool, visible: bool,
row_height: Pixels, text_unit_size: Pixels,
thumb_height: Pixels, thumb_size: Pixels,
axis: Axis,
} }
impl ScrollbarLayout { impl ScrollbarLayout {
const BORDER_WIDTH: Pixels = px(1.0); const BORDER_WIDTH: Pixels = px(1.0);
const LINE_MARKER_HEIGHT: Pixels = px(2.0); const LINE_MARKER_HEIGHT: Pixels = px(2.0);
const MIN_MARKER_HEIGHT: Pixels = px(5.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<Pixels> { fn thumb_bounds(&self) -> Bounds<Pixels> {
let thumb_top = self.y_for_row(self.visible_row_range.start); match self.axis {
let thumb_bottom = thumb_top + self.thumb_height; Axis::Vertical => {
Bounds::from_corners( let thumb_top = self.y_for_row(self.visible_range.start);
point(self.hitbox.left(), thumb_top), let thumb_bottom = thumb_top + self.thumb_size;
point(self.hitbox.right(), thumb_bottom), 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 { 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( 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 let mut pixel_ranges = row_ranges
.into_iter() .into_iter()
.map(|range| { .map(|range| {
let start_y = row_to_y(range.start); let start_y = row_to_y(range.start);
let end_y = row_to_y(range.end) 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 { ColoredRange {
start: start_y, start: start_y,
end: end_y, end: end_y,

View file

@ -2,7 +2,7 @@ mod actions;
pub(crate) mod autoscroll; pub(crate) mod autoscroll;
pub(crate) mod scroll_amount; pub(crate) mod scroll_amount;
use crate::editor_settings::ScrollBeyondLastLine; use crate::editor_settings::{ScrollBeyondLastLine, ScrollbarAxes};
use crate::{ use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint}, display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover, hover_popover::hide_hover,
@ -11,7 +11,10 @@ use crate::{
InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint, InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint,
}; };
pub use autoscroll::{Autoscroll, AutoscrollStrategy}; 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}; use language::{Bias, Point};
pub use scroll_amount::ScrollAmount; pub use scroll_amount::ScrollAmount;
use settings::Settings; use settings::Settings;
@ -60,10 +63,53 @@ impl ScrollAnchor {
} }
} }
#[derive(Copy, Clone, PartialEq, Eq, Debug)] #[derive(Debug, Clone)]
pub enum Axis { pub struct AxisPair<T: Clone> {
Vertical, pub vertical: T,
Horizontal, pub horizontal: T,
}
pub fn axis_pair<T: Clone>(horizontal: T, vertical: T) -> AxisPair<T> {
AxisPair {
vertical,
horizontal,
}
}
impl<T: Clone> AxisPair<T> {
pub fn as_xy(&self) -> (&T, &T) {
(&self.horizontal, &self.vertical)
}
}
impl<T: Clone> Along for AxisPair<T> {
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<ScrollbarAxes> for AxisPair<bool> {
fn from(value: ScrollbarAxes) -> Self {
axis_pair(value.horizontal, value.vertical)
}
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -136,7 +182,7 @@ pub struct ScrollManager {
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>, last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool, show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>, hide_scrollbar_task: Option<Task<()>>,
dragging_scrollbar: bool, dragging_scrollbar: AxisPair<bool>,
visible_line_count: Option<f32>, visible_line_count: Option<f32>,
forbid_vertical_scroll: bool, forbid_vertical_scroll: bool,
} }
@ -150,7 +196,7 @@ impl ScrollManager {
autoscroll_request: None, autoscroll_request: None,
show_scrollbars: true, show_scrollbars: true,
hide_scrollbar_task: None, hide_scrollbar_task: None,
dragging_scrollbar: false, dragging_scrollbar: axis_pair(false, false),
last_autoscroll: None, last_autoscroll: None,
visible_line_count: None, visible_line_count: None,
forbid_vertical_scroll: false, forbid_vertical_scroll: false,
@ -311,15 +357,18 @@ impl ScrollManager {
self.autoscroll_request.map(|(autoscroll, _)| autoscroll) self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
} }
pub fn is_dragging_scrollbar(&self) -> bool { pub fn is_dragging_scrollbar(&self, axis: Axis) -> bool {
self.dragging_scrollbar self.dragging_scrollbar.along(axis)
} }
pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext<Editor>) { pub fn set_is_dragging_scrollbar(
if dragging != self.dragging_scrollbar { &mut self,
self.dragging_scrollbar = dragging; axis: Axis,
cx.notify(); dragging: bool,
} cx: &mut ViewContext<Editor>,
) {
self.dragging_scrollbar = self.dragging_scrollbar.apply_along(axis, |_| dragging);
cx.notify();
} }
pub fn clamp_scroll_left(&mut self, max: f32) -> bool { pub fn clamp_scroll_left(&mut self, max: f32) -> bool {

View file

@ -5,18 +5,16 @@ use crate::{
}; };
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use editor::{ use editor::{
actions::SelectAll, actions::SelectAll, items::active_match_index, scroll::Autoscroll, Anchor, Editor,
items::active_match_index, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer, MAX_TAB_TITLE_LEN,
scroll::{Autoscroll, Axis},
Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer,
MAX_TAB_TITLE_LEN,
}; };
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{
actions, div, Action, AnyElement, AnyView, AppContext, Context as _, EntityId, EventEmitter, actions, div, Action, AnyElement, AnyView, AppContext, Axis, Context as _, EntityId,
FocusHandle, FocusableView, Global, Hsla, InteractiveElement, IntoElement, KeyContext, Model, EventEmitter, FocusHandle, FocusableView, Global, Hsla, InteractiveElement, IntoElement,
ModelContext, ParentElement, Point, Render, SharedString, Styled, Subscription, Task, KeyContext, Model, ModelContext, ParentElement, Point, Render, SharedString, Styled,
TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel,
WeakView, WindowContext,
}; };
use language::Buffer; use language::Buffer;
use menu::Confirm; use menu::Confirm;

View file

@ -534,7 +534,11 @@ List of `string` values
"git_diff": true, "git_diff": true,
"search_results": true, "search_results": true,
"selected_symbol": true, "selected_symbol": true,
"diagnostics": true "diagnostics": true,
"axes": {
"horizontal": true,
"vertical": true,
},
}, },
``` ```
@ -628,6 +632,41 @@ List of `string` values
`boolean` 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 ## Editor Tab Bar
- Description: Settings related to the editor's tab bar. - Description: Settings related to the editor's tab bar.