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.
"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,

View file

@ -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<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
/// to both the horizontal and vertical delta values while scrolling.
///
@ -328,6 +349,22 @@ pub struct ScrollbarContent {
///
/// Default: true
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

View file

@ -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<Editor>, 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<Pixels>,
scrollbar_range_data: ScrollbarRangeData,
scroll_position: gpui::Point<f32>,
rows_per_page: f32,
non_visible_cursors: bool,
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 show_scrollbars = match scrollbar_settings.show {
ShowScrollbar::Auto => {
@ -1197,45 +1217,139 @@ impl EditorElement {
ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
};
let axes: AxisPair<bool> = 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<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 {
type Element = Self;
@ -6212,7 +6556,7 @@ pub struct EditorLayout {
gutter_hitbox: Hitbox,
gutter_dimensions: GutterDimensions,
content_origin: gpui::Point<Pixels>,
scrollbar_layout: Option<ScrollbarLayout>,
scrollbars_layout: AxisPair<Option<ScrollbarLayout>>,
mode: EditorMode,
wrap_guides: SmallVec<[(Pixels, bool); 2]>,
indent_guides: Option<Vec<IndentGuideLayout>>,
@ -6256,29 +6600,43 @@ struct ColoredRange<T> {
#[derive(Clone)]
struct ScrollbarLayout {
hitbox: Hitbox,
visible_row_range: Range<f32>,
visible_range: Range<f32>,
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<Pixels> {
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,

View file

@ -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<T: Clone> {
pub vertical: T,
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)]
@ -136,7 +182,7 @@ pub struct ScrollManager {
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
dragging_scrollbar: bool,
dragging_scrollbar: AxisPair<bool>,
visible_line_count: Option<f32>,
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<Editor>) {
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<Editor>,
) {
self.dragging_scrollbar = self.dragging_scrollbar.apply_along(axis, |_| dragging);
cx.notify();
}
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {

View file

@ -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;

View file

@ -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.