diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d1dfed3c85..bff6a4054a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -76,6 +76,7 @@ use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ItemNavHistory, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); +const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; @@ -238,6 +239,9 @@ pub enum Direction { Next, } +#[derive(Default)] +struct ScrollbarAutoHide(bool); + pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::new_file); cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); @@ -427,6 +431,8 @@ pub struct Editor { focused: bool, show_local_cursors: bool, show_local_selections: bool, + show_scrollbars: bool, + hide_scrollbar_task: Option>, blink_epoch: usize, blinking_paused: bool, mode: EditorMode, @@ -1029,6 +1035,8 @@ impl Editor { focused: false, show_local_cursors: false, show_local_selections: true, + show_scrollbars: true, + hide_scrollbar_task: None, blink_epoch: 0, blinking_paused: false, mode, @@ -1061,10 +1069,16 @@ impl Editor { ], }; this.end_selection(cx); + this.make_scrollbar_visible(cx); let editor_created_event = EditorCreated(cx.handle()); cx.emit_global(editor_created_event); + if mode == EditorMode::Full { + let should_auto_hide_scrollbars = cx.platform().should_auto_hide_scrollbars(); + cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); + } + this.report_event("open editor", cx); this } @@ -1181,6 +1195,7 @@ impl Editor { self.scroll_top_anchor = anchor; } + self.make_scrollbar_visible(cx); self.autoscroll_request.take(); hide_hover(self, cx); @@ -5952,6 +5967,31 @@ impl Editor { self.show_local_cursors && self.focused } + pub fn show_scrollbars(&self) -> bool { + self.show_scrollbars + } + + fn make_scrollbar_visible(&mut self, cx: &mut ViewContext) { + if !self.show_scrollbars { + self.show_scrollbars = true; + cx.notify(); + } + + if cx.default_global::().0 { + self.hide_scrollbar_task = Some(cx.spawn_weak(|this, mut cx| async move { + Timer::after(SCROLLBAR_SHOW_INTERVAL).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.show_scrollbars = false; + cx.notify(); + }); + } + })); + } else { + self.hide_scrollbar_task = None; + } + } + fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { cx.notify(); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a3b5ad43f6..96ce28f0d8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -43,7 +43,7 @@ use std::{ cmp::{self, Ordering}, fmt::Write, iter, - ops::Range, + ops::{DerefMut, Range}, sync::Arc, }; use theme::DiffStyle; @@ -454,7 +454,6 @@ impl EditorElement { let bounds = gutter_bounds.union_rect(text_bounds); let scroll_top = layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height; - let editor = self.view(cx.app); cx.scene.push_quad(Quad { bounds: gutter_bounds, background: Some(self.style.gutter_background), @@ -468,7 +467,7 @@ impl EditorElement { corner_radius: 0., }); - if let EditorMode::Full = editor.mode { + if let EditorMode::Full = layout.mode { let mut active_rows = layout.active_rows.iter().peekable(); while let Some((start_row, contains_non_empty_selection)) = active_rows.next() { let mut end_row = *start_row; @@ -909,6 +908,125 @@ impl EditorElement { cx.scene.pop_layer(); } + fn paint_scrollbar(&mut self, bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { + enum ScrollbarMouseHandlers {} + if layout.mode != EditorMode::Full { + return; + } + + let view = self.view.clone(); + let style = &self.style.theme.scrollbar; + let min_thumb_height = + style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size); + + let top = bounds.min_y(); + let bottom = bounds.max_y(); + let right = bounds.max_x(); + let left = right - style.width; + let height = bounds.height(); + let row_range = &layout.scrollbar_row_range; + let max_row = layout.max_row + ((row_range.end - row_range.start) as u32); + let scrollbar_start = row_range.start as f32 / max_row as f32; + let scrollbar_end = row_range.end as f32 / max_row as f32; + + let mut thumb_top = top + scrollbar_start * height; + let mut thumb_bottom = top + scrollbar_end * height; + let thumb_center = (thumb_top + thumb_bottom) / 2.0; + + if thumb_bottom - thumb_top < min_thumb_height { + thumb_top = thumb_center - min_thumb_height / 2.0; + thumb_bottom = thumb_center + min_thumb_height / 2.0; + if thumb_top < top { + thumb_top = top; + thumb_bottom = top + min_thumb_height; + } + if thumb_bottom > bottom { + thumb_bottom = bottom; + thumb_top = bottom - min_thumb_height; + } + } + + let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom)); + let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom)); + + if layout.show_scrollbars { + cx.scene.push_quad(Quad { + bounds: track_bounds, + border: style.track.border, + background: style.track.background_color, + ..Default::default() + }); + cx.scene.push_quad(Quad { + bounds: thumb_bounds, + border: style.thumb.border, + background: style.thumb.background_color, + corner_radius: style.thumb.corner_radius, + }); + } + + cx.scene.push_cursor_region(CursorRegion { + bounds: track_bounds, + style: CursorStyle::Arrow, + }); + cx.scene.push_mouse_region( + MouseRegion::new::(view.id(), view.id(), track_bounds) + .on_move({ + let view = view.clone(); + move |_, cx| { + if let Some(view) = view.upgrade(cx.deref_mut()) { + view.update(cx.deref_mut(), |view, cx| { + view.make_scrollbar_visible(cx); + }); + } + } + }) + .on_down(MouseButton::Left, { + let view = view.clone(); + let row_range = row_range.clone(); + move |e, cx| { + let y = e.position.y(); + if let Some(view) = view.upgrade(cx.deref_mut()) { + view.update(cx.deref_mut(), |view, cx| { + if y < thumb_top || thumb_bottom < y { + let center_row = + ((y - top) * max_row as f32 / height).round() as u32; + let top_row = center_row.saturating_sub( + (row_range.end - row_range.start) as u32 / 2, + ); + let mut position = view.scroll_position(cx); + position.set_y(top_row as f32); + view.set_scroll_position(position, cx); + } else { + view.make_scrollbar_visible(cx); + } + }); + } + } + }) + .on_drag(MouseButton::Left, { + let view = view.clone(); + move |e, cx| { + let y = e.prev_mouse_position.y(); + let new_y = e.position.y(); + if thumb_top < y && y < thumb_bottom { + if let Some(view) = view.upgrade(cx.deref_mut()) { + view.update(cx.deref_mut(), |view, cx| { + let mut position = view.scroll_position(cx); + position.set_y( + position.y() + (new_y - y) * (max_row as f32) / height, + ); + if position.y() < 0.0 { + position.set_y(0.); + } + view.set_scroll_position(position, cx); + }); + } + } + } + }), + ); + } + #[allow(clippy::too_many_arguments)] fn paint_highlighted_range( &self, @@ -1469,13 +1587,11 @@ impl Element for EditorElement { // The scroll position is a fractional point, the whole number of which represents // the top of the window in terms of display rows. let start_row = scroll_position.y() as u32; - let scroll_top = scroll_position.y() * line_height; + let visible_row_count = (size.y() / line_height).ceil() as u32; + let max_row = snapshot.max_point().row(); // Add 1 to ensure selections bleed off screen - let end_row = 1 + cmp::min( - ((scroll_top + size.y()) / line_height).ceil() as u32, - snapshot.max_point().row(), - ); + let end_row = 1 + cmp::min(start_row + visible_row_count, max_row); let start_anchor = if start_row == 0 { Anchor::min() @@ -1484,7 +1600,7 @@ impl Element for EditorElement { .buffer_snapshot .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left)) }; - let end_anchor = if end_row > snapshot.max_point().row() { + let end_anchor = if end_row > max_row { Anchor::max() } else { snapshot @@ -1496,6 +1612,7 @@ impl Element for EditorElement { let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; let mut highlighted_ranges = Vec::new(); + let mut show_scrollbars = false; self.update_view(cx.app, |view, cx| { let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -1556,6 +1673,8 @@ impl Element for EditorElement { .collect(), )); } + + show_scrollbars = view.show_scrollbars(); }); let line_number_layouts = @@ -1566,6 +1685,9 @@ impl Element for EditorElement { .git_diff_hunks_in_range(start_row..end_row) .collect(); + let scrollbar_row_range = + scroll_position.y()..(scroll_position.y() + visible_row_count as f32); + let mut max_visible_line_width = 0.0; let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx); for line in &line_layouts { @@ -1599,7 +1721,6 @@ impl Element for EditorElement { cx, ); - let max_row = snapshot.max_point().row(); let scroll_max = vec2f( ((scroll_width - text_size.x()) / em_width).max(0.0), max_row.saturating_sub(1) as f32, @@ -1629,6 +1750,7 @@ impl Element for EditorElement { let mut context_menu = None; let mut code_actions_indicator = None; let mut hover = None; + let mut mode = EditorMode::Full; cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| { let newest_selection_head = view .selections @@ -1650,6 +1772,7 @@ impl Element for EditorElement { let visible_rows = start_row..start_row + line_layouts.len() as u32; hover = view.hover_state.render(&snapshot, &style, visible_rows, cx); + mode = view.mode; }); if let Some((_, context_menu)) = context_menu.as_mut() { @@ -1697,6 +1820,7 @@ impl Element for EditorElement { ( size, LayoutState { + mode, position_map: Arc::new(PositionMap { size, scroll_max, @@ -1709,6 +1833,9 @@ impl Element for EditorElement { gutter_size, gutter_padding, text_size, + scrollbar_row_range, + show_scrollbars, + max_row, gutter_margin, active_rows, highlighted_rows, @@ -1756,11 +1883,12 @@ impl Element for EditorElement { } self.paint_text(text_bounds, visible_bounds, layout, cx); + cx.scene.push_layer(Some(bounds)); if !layout.blocks.is_empty() { - cx.scene.push_layer(Some(bounds)); self.paint_blocks(bounds, visible_bounds, layout, cx); - cx.scene.pop_layer(); } + self.paint_scrollbar(bounds, layout, cx); + cx.scene.pop_layer(); cx.scene.pop_layer(); } @@ -1846,12 +1974,16 @@ pub struct LayoutState { gutter_padding: f32, gutter_margin: f32, text_size: Vector2F, + mode: EditorMode, active_rows: BTreeMap, highlighted_rows: Option>, line_number_layouts: Vec>, blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, selections: Vec<(ReplicaId, Vec)>, + scrollbar_row_range: Range, + show_scrollbars: bool, + max_row: u32, context_menu: Option<(DisplayPoint, ElementBox)>, diff_hunks: Vec>, code_actions_indicator: Option<(u32, ElementBox)>, diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 25c1d5ac8e..9fc2c16497 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -65,6 +65,7 @@ pub trait Platform: Send + Sync { fn delete_credentials(&self, url: &str) -> Result<()>; fn set_cursor_style(&self, style: CursorStyle); + fn should_auto_hide_scrollbars(&self) -> bool; fn local_timezone(&self) -> UtcOffset; diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index f35d5d6935..a27220cf2e 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -709,6 +709,16 @@ impl platform::Platform for MacPlatform { } } + fn should_auto_hide_scrollbars(&self) -> bool { + #[allow(non_upper_case_globals)] + const NSScrollerStyleOverlay: NSInteger = 1; + + unsafe { + let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle]; + style == NSScrollerStyleOverlay + } + } + fn local_timezone(&self) -> UtcOffset { unsafe { let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone]; diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index c3f037fe86..3c2e23bbd3 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -181,6 +181,10 @@ impl super::Platform for Platform { *self.cursor.lock() = style; } + fn should_auto_hide_scrollbars(&self) -> bool { + false + } + fn local_timezone(&self) -> UtcOffset { UtcOffset::UTC } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 37ec279d02..6e411c010f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -554,6 +554,15 @@ pub struct Editor { pub link_definition: HighlightStyle, pub composition_mark: HighlightStyle, pub jump_icon: Interactive, + pub scrollbar: Scrollbar, +} + +#[derive(Clone, Deserialize, Default)] +pub struct Scrollbar { + pub track: ContainerStyle, + pub thumb: ContainerStyle, + pub width: f32, + pub min_height_factor: f32, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 04a5bafbd5..6a333930db 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -1,4 +1,5 @@ import Theme from "../themes/common/theme"; +import { withOpacity } from "../utils/color"; import { backgroundColor, border, @@ -170,6 +171,24 @@ export default function editor(theme: Theme) { background: backgroundColor(theme, "on500"), }, }, + scrollbar: { + width: 12, + minHeightFactor: 1.0, + track: { + border: { + left: true, + width: 1, + color: borderColor(theme, "secondary"), + }, + }, + thumb: { + background: withOpacity(borderColor(theme, "secondary"), 0.5), + border: { + width: 1, + color: withOpacity(borderColor(theme, 'muted'), 0.5), + } + } + }, compositionMark: { underline: { thickness: 1.0, diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index cd6d46a771..1c4a5e4076 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -123,7 +123,7 @@ export function createTheme( const borderColor = { primary: sample(ramps.neutral, isLight ? 1.5 : 0), secondary: sample(ramps.neutral, isLight ? 1.25 : 1), - muted: sample(ramps.neutral, isLight ? 1 : 3), + muted: sample(ramps.neutral, isLight ? 1.25 : 3), active: sample(ramps.neutral, isLight ? 4 : 3), onMedia: withOpacity(darkest, 0.1), ok: sample(ramps.green, 0.3),